From 58b320d1c53f37ae2443c04f84a7fd397d16043d Mon Sep 17 00:00:00 2001 From: Ryan D'Onofrio <97113725+Ryandonofrio3@users.noreply.github.com> Date: Sat, 13 Dec 2025 17:55:27 -0800 Subject: [PATCH 01/19] first commit --- bun.lockb | Bin 0 -> 244983 bytes package.json | 4 +- plan.md | 332 ++++++++++++ pnpm-lock.yaml | 715 +------------------------- src/commands/doctor.ts | 28 +- src/commands/search.ts | 124 ----- src/commands/serve.ts | 1 - src/commands/setup.ts | 52 +- src/commands/skeleton.ts | 321 ------------ src/commands/trace.ts | 38 -- src/commands/verify.ts | 169 ------ src/config.ts | 21 - src/index.ts | 4 - src/lib/graph/graph-builder.ts | 129 ----- src/lib/index/syncer.ts | 27 +- src/lib/native/index.ts | 238 +++++++++ src/lib/output/formatter.ts | 48 -- src/lib/output/json-formatter.ts | 6 - src/lib/search/intent.ts | 34 -- src/lib/search/searcher.ts | 196 +------ src/lib/setup/model-loader.ts | 68 --- src/lib/setup/setup-helpers.ts | 21 +- src/lib/skeleton/body-fields.ts | 169 ------ src/lib/skeleton/index.ts | 24 - src/lib/skeleton/retriever.ts | 27 - src/lib/skeleton/skeletonizer.ts | 562 -------------------- src/lib/skeleton/summary-formatter.ts | 121 ----- src/lib/utils/exit.ts | 9 - src/lib/workers/colbert-math.ts | 92 ---- src/lib/workers/colbert-tokenizer.ts | 130 ----- src/lib/workers/download-worker.ts | 150 ------ src/lib/workers/embeddings/colbert.ts | 199 ------- src/lib/workers/embeddings/granite.ts | 175 ------- src/lib/workers/orchestrator.ts | 288 ++++------- src/lib/workers/pool.ts | 450 ---------------- src/lib/workers/process-child.ts | 70 --- src/lib/workers/worker.ts | 29 -- 37 files changed, 750 insertions(+), 4321 deletions(-) create mode 100755 bun.lockb create mode 100644 plan.md delete mode 100644 src/commands/skeleton.ts delete mode 100644 src/commands/trace.ts delete mode 100644 src/commands/verify.ts delete mode 100644 src/lib/graph/graph-builder.ts create mode 100644 src/lib/native/index.ts delete mode 100644 src/lib/search/intent.ts delete mode 100644 src/lib/setup/model-loader.ts delete mode 100644 src/lib/skeleton/body-fields.ts delete mode 100644 src/lib/skeleton/index.ts delete mode 100644 src/lib/skeleton/retriever.ts delete mode 100644 src/lib/skeleton/skeletonizer.ts delete mode 100644 src/lib/skeleton/summary-formatter.ts delete mode 100644 src/lib/workers/colbert-math.ts delete mode 100644 src/lib/workers/colbert-tokenizer.ts delete mode 100644 src/lib/workers/download-worker.ts delete mode 100644 src/lib/workers/embeddings/colbert.ts delete mode 100644 src/lib/workers/embeddings/granite.ts delete mode 100644 src/lib/workers/pool.ts delete mode 100644 src/lib/workers/process-child.ts delete mode 100644 src/lib/workers/worker.ts diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..fed97fd65488a2e85c94685a1c1a107ac3bf37e2 GIT binary patch literal 244983 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%0ALf?6k6sOLQ5b(ho<31 zlw8iOOp#9PNMMAz2uo1CLzv#&z<%x`r~X4Ba>+x4`%!oljW><X-}REa#O*}W!@J_R z-N)y}GZgD$z^tRPEnX5}n2*j6>IFb;NT7-b=4wijIiK)m_7`)Q?t<f15ki1jP=qeb zw0k<|Rs{Z{H1B)jfS+T&zntR=8*Bxu=5)M|b8RLzxGp(?Ask+ig`r?Am*#HNh{3dt zK2)k^Ih*0)Tb;Yf*;X7Bv?bF_0i!=}a}X0_Fe&OV*_3U6bmPcC)mC)l)uQ%Nd;++N zb<&y;n9(t-<*z`P$=)!t!S!7j7-zcBa@`gLqBtSU?in0-Ql}b>db9|3LD5_#?ysNa zc<Ln3LOvQXfemGDMH!VNJJ@UoCtAWtrzB5$<D8Z-uq@;eF#=(*SI#o5ShsYb&0A?a zhcyD~4G|fMCD<_RNV6Y<GzT|~q#Z2{Q_a}Sv>+}BY;i)sn8Bfz96+FAvV;)($qcmh zoCio70ta}1Ge4A_uQ`CZplH#O5T5j9*$prol0jeM_dz}AEk_F;eY&8)m99<p;p$$* ziocy8_PJBOKMKl#)=Ry8T$F~a;Ugi#$fP9jBq`_)u13LFv!Z_pcTb^;=VIoCr-tTm zFQ1)I<ASIlPLKwcIxemnIN}80u)2WLYPY%AmLjeI11aL%o|hu`!XQ9hP_##>U(FH$ zP$q!7tjXGbuc@)CkiCI1tn2!oOSI;&A+)(ns>jS)T`hp*V97T!b^QPYX%Z>0nuE?F z@D(Z^E_5-vl$cI3;<s8jsmNeZRb`NADu6~uMXAUzuba!p3LlpC!`^84aBZR)yh%x* zPEs%fTGcIk1q0~O3_4#1h<un!5h~(*ugs@#+W|27rRw6E3LvJbi=$!F%;48%F@P{( zvB=#LI%Kf7c6jl-H;3a9c&0D|2v3Gr6#ophEu}#~OKPV%D5gX>ImWb&Sy)IzKbIF% zs$oE&Vx4q(U=VBf+(OM%B6;=)HUl<9glg#r==mXZHa6pCSc?p^mreMH9ns*2;%%nj zOh>HXmc}Rb2_}&uTl}E6=s9{$x$?5KomLPF@sWZTwx|2oC@Oe;EobtoN}*~M6W*0X zcU|2;wJuBeDpLclWW~)t%`j@UFzQ%M6znCc+qf9`9x+$AAX0$3<Yq~ZnZAsn&6Ed< z@^!O)SOM3)3E*r9x~(ida9DybIO+}?(i%}bk{FF(_U2|;_O2F}Myw`xTH$+zEJ&<! zmZeOob8t)!7#^1MJ#-P2y<yYq^_$&6Q#tp5nWk88nm}ShqQgq7K_aGaOn|u{m}zIi zOl^)J3&-uBuw9se>mN-|CTyt*gpSL=wD1846j!8cjG_Sc(X@9^jm8K2P!F}Z3u<^% z^T1{K)cmDQVn8*5h~7|SG$c-hoU0;Sed-@$!GK)=tR~^?1{LCw_mjGJT#e?-N_@)h z2}Z%E4~SNiaVnz{{c%`ZT1&|oS{?N%cz;fl<9Z<17kB*d3Ftq|nN2Xs@nDS0oEUD1 z&7W*JTs~n_x;&H3!61c4C2fr@rVDM{6jOW@NU}o8mbfD3YeDRUFBWOEFHV5cFK_n8 zg9JF@1mLhvfJ&pyy9_H9r01}VgqGkzW2p$Vt!=qo#6hX{GgYcmz^-Dl@GE^o3N_vl znIq39U^}#f3U3anwqzQ$K~DSppu);=?2@o2&Am&nK7EGQqnTFv!WIiCO_LyY?g%u^ zS}m*Pb+2=As>E_(Kj1ehy8-5IRCv{z;c5p0J(@O-`^>=mv(@JHZd+|W^Qo=P>9Mq9 z6UE)(JS@eqDt2Bp*0;|n_ot+`pyJ!A3%O4fKlPx>)F=$^R3{E#vGod6CIHpQRek<* z)cffKm=B{i{sZJ5H6GWggJ!2$^cFgPf|CG#!14k*ypoQGxMm|C1{t9ah1g;bAuGu% ztPt)_i%5JK;P7GS@ABTqK2$3Fq~fzj0)eaW={SO?+{s~X8zU4Vvpj?>z-1W>cam~i zU&Is_1hzN<J!kiVaByUfXfr(ZWq`+rWjm?(6;k86)Omo7C4gzjo~~tTQ#}qq6$8;+ zh2+Hz^by2RD!~5e99fvHn3kzM6f!6*giPM0PrX!jfdNuYDVF7vh@UP4C!6pgnmg2L zDQ%EiP$5gEbzCv#FzJm(b@o1VGTbaS3_PGJ`*+l}08~e%i6RehaImcLWsA|e=ruVf zvPnr`Oi~1CX~T<J<vJU}UqIY#y`5%yd0sCYY%8(}CPc9E=GJt92WJ6vAOSjWp-#vP zB=?5V&9bZE!^Rndk!9!EnBTJA&2ffy9*X&LASQ|%IW>an5aV-LJa}BHjuzvQ1n1|C z8)O{7%y5LAsqGXO9;wgdmp)SLE{=&CWHfK;Q}X5&UDwi43EWIo5N&MOP3O67u$8jX zB?)||!IuyLJV{C?0oTp)tv0Q!;Pi;c;Q8*h8&vKnPDe2ZS#g1q87xO5DTG~R*bIo% zp-eZdfdGkXa%AQysb^(S223gj3(fKtH~rF%dIww&xFBjR@A5%lmo$r#UztW^3ntTg zZv+S^AMP{(o!)(gn~?mGIZt$Orl!I`0jfd?F<2>06b45?*5yz=h2dmas%QK#>KH!^ z$>-S*1h)qVqBO$AOHFob9Q%7%%;C-B36EJYh%?MlrGQt(pgrxEJ?xHTml1YH7t5Fw z7I{##Ft!x+fK)NmU*3|A8VvX--f1h>Yh7qmAkb0TW8(F{LIbcjdOXCc9KbFp3)2!D zPrIQ_XMl4-A-J>{ifw8xxifO{s(-%U+~AbsBl%rZ?T{a2=xBu}t?lFq;~6+|r;%AB zhj06kt>t*HIqB~n)gv4?HFgT*#~j`JAI1sG2Ai$cJ^o6`wsZl=2-;d0c7d6h4Jm?1 zR76P`wV||=WQ37vK-hVOpy+5Pj$>FP1zpxVt@g3UV~nL1HQc(xzF{HO6r<kh6u#fq zKW%{H+#xhKDzPDG((>@p$O)?I3WJCC^;FPVD}hi5ctS*$_a%)Mnhw|y5o_X3qX91r z7-j)J<{l2W`Kgef-dB2<XtW&pUYC~O;PU`qZ0MbSZTy7q^kIQD>ch+-(?e-dRFWov z=jA@r)dPo)c3K|<1_L@o09#%xuSyxYTjH-)6{yoH4Rj5ryOOz;(l`KBF@Tr%Z<OjH ztAZk{_}UP*fJ;i+>IHsa0fB7fV#SD$5xD(>P%ZJP*Dt#g)Wv{CM=9iGzOQ{!1=#Sg z3Z_c5N7}-NlrEVU6?e|{uHC5kNJ|gT=ZH60d^4O(%nRq!<-8AjDj2(3kASmdxSZ$c z8P(X(P=)vj3bPZKUc=a~!7c@^fQ*KCJAx0XU<<-nWZ;Y!PFMN^jQwML7pcU|m{`St z2CXJ?1!^DRQ0>d$U8NOxR}-Nl%l1eXOd;^5TJj=cy_}C=(^ngZl30k{#FFdaV7@;+ zz`D$^&b++AFEtWv&*)(bgTXs&j_L#a_C`neqQr-m>?c`3u>A&2>3WM{zYlkvLqst9 z@+FR1c3-qDh3iZ>6~+ViL%#ZvVNpwIG_fTW!ezIWGT2s#0j`xa`B*C{i?o?SRNQik zJO1UBx@H}4=qTtYf6z2=ODS+r_N=c&H#-SFRJn$9L1}M^PTk#r)lsRpG<hm10=SBy z_VT_QUQNP%JUmo{;bDWQwZlh<7kEGh>6Zzp)M6<v$MrUZ8ZVV+X*F_Cr-})ig-uOu z-kYLr+jOwNBN9+<!@)4S8KR2;v5tbUvgpT^0z;f2omdhcShG^t_#>?;mB;1l<_G}t z+X({eNLs-22Nae49oqM4sImwa6qxX&G;MmtNLW7GN+sEePEnXB3_uq|jrLq2qkyyq z1+0#u-m*YD#YfQrypBqXOONCzZ4Yo2L+xd2S={KV2m??T>@<{?li5({H36ZtBs=@3 zIA14?{FPN-X~2>v)sQeYBt0o3ZjAd<J(>A(`;Dp;kf<04E-!wrv*zm!kIAi;XfUw3 zRKb;<+}4IJ1{69PDz-ZY!$~v4#%5H8_lTneyrUb%X-<K(j99i6Pf4*@r1PXJxa|sY zz%r4$oiM6}VQn+a*(XHRfTFYtBnkTBWMa%QEi-^6<_@`5erqZs8aSdjP}ol#CS$d< zn9>N-Ud3|Hj?p9wR-%>L&RI;y4OX#7ff?MU1e5qF)|<^nr<MJx$n?b;2vE2tC-g`L zYeX_|MR78$xKc?P1{_I>U?~mjgj>LpqzIOh0I0qI1{Lc>gYQzn`Y<5vBzgde#&Lq_ z^z~aAx)>1YXuy@}5l}rn;Rdk@h_WiFxG>}Z7a~$$Y40c+eAF30v63d3N_`kU9)Kc5 zqy_T5iI2GxUdBAY10DFp$nWm?GC=0Tprd?}=Boju595QoybA91`!(JQ1)L2D?b7Z^ zeK=hVdpJAF^j#<f6d}@ydvolN$%&i^pk_-iPE){4?vL}?Mt-u&d3#Q8#|1%4oPeX0 z@<vUl7Vse=o5LkZFkeA{v!6rUh%U+S4H&Tkf<NF_S?^5oYAed~2j$BPaP9%lSc3^j zpOk1?-9`ZXYN9N5N{{o!8x+73C&)f;Nx!M86GGUZrOha{i2`U9!$yLAssms1`!ERM zwGy5XXGA?I34}>X7&<Pa<kTT5rw|(R61K1;cOvGvjR9a6lqI6@RkpuQj<6Ehk%w(V zJkv}m4|MV?O)AL@#_3H8ru+sbWa?1+T!2$Ck*ktSpuB~`XOX=aCq_*Nb&b86>&xEA zooElBM(hP9wZVmB_a0wS$?g8O6-TXY$=#HlJ&#uu0bL87omieU7nqV15rWcJpTOXO zTnEm#a=Yv*Uz;}UZ~kzRZ#FVB-3=c#oMHHb*7056{8mu<Ks8|PQ`MZ)>{-sd9*B=0 zyb%NgibXF4QnuXdvc%?YB{Nu5;@B*cD<1!nr?gW~!)dlRbu_M_)?hcbfEoO@09-)c ze;+RPK~3EClKad7V5S>ZYBQFq0h!`+nxz=ONe#Na&T*W1e4Gbuc;hrQ^(m;qdf;lQ zfNr>g=xox0V9m>yQCBzwvtxNObxZ6))>^qE*y-nOJK6*GHqH?F_IydqTHW~6$y-uw zQGh(f?U2|FYpNZ9DorAy;u>x5XnBa2%nk=QJKV*zF=?>A#zwhpuBa&Vrro29Ag&E3 z*LqmlVPT7%{~J5!&z-+??&7(t)s?G*E9cK$-tS%1_nPrseMUAL_;rz;0{(*f3)WZN z-f(;f<vPL%kp!ijQ-3nRR+GV9l?c<{8^alHPMVjBQ<x;X9oCQM7Q8;MwN{fWjyc!D zT34fM^Tl82+T0Aq9C!BRVzSZj&NWyllM*QgtGl8ay*^t>U2Pl7wzY}rt8HOw3k7Rv zN9h)tji*vYno{vAsq{U%k_ZYi(%DuVqs}liXwe|Ho8Tfn2{Lhyx*ax=?*yHhh&IM2 z-g*Nn<GgoMERmRbWKpUb?%nv-D#8l48Srr(`~)p+o~UjPz%fIU^rwLI6n{#*>AY>o z=de;-u><XFBqul(vCP5O!sc8yIw2v{_(<Md*fc?cL*ZS?+m2kT+7x1P*7=J*8_BD< zHPNn)t+*_DIw9IPj6NI5DPB#m!M_!`>98P=%}_WSxhY;v?iq#}Koi~~<%~lu#+eYh zx$GJQVX>*`O|#C3T==*Sy<~1$K?~I(X=66)rJyHMCvJeztWAN%1kf<Mv9)GNoSEU0 z)iQ7<DdbvawauLeInRKB?6u}_IfO+NPD_n{laBRrt8mKm3SG5H>`;5Al7t-8kfaFb zN|zs$)f_OWSOuFb7{Zlyg#vp39wK6UC98wIkht4}UmV$~*NMh|lun^V!66<&R*<r@ zTf-_1X8<~VP{aL5lmM3#3!PvcRq7<}eMWJ3KGE^zVsItHc(_lau?2syD_y=$*=A)3 z+a21j=6ANVB_S%*QSetDKh7ga8$~2&8w8_#F0Wx{U=X_(KbymX*@WfUOqX*DU9ass zfZNc<wGSu`fR|rD^DDkP&qTTiO)1;BZZGY?;KLj9KubOl>FCB3*?|Q#HYD4}B{9@J zVg@S^Sg&uLdQ|U<$Q}pa<rlL82T-9y`Y^>lc8=%)N*#hzNC3-|(tzfLU-6uYn=uXj zyTYJi&w$b6d1frDQo<G7fg??&gOpGWph83hth8Sph-F(m<<gmQm~jJ+gqy|AIBSt_ z3IT%&0GfO2;KgW)1MX^ee7G1+PWNG+b;J_luB``Vz#%#=x9(Z+W_CL69S-~U)Wapy zlcKn1pJC&b{2aL)n9IVB#>H@%6mE50tieM-&Czsma&!uNBB`-r32cZR@ocd=n2uf9 z<?uo5Kyxelkw@;j|2|k<<DbjipHEL<(YaT{LWUR?Fh|_Ik@Gg$QYBc^k_v&hN4qfN z#Onq4e?~fd8TPU(4*@A_5!mq#Cxa>BOh`srNvs?L9485GnHoA6cG`=@skQvh5FeET zrYK&Wzv?cS?OlVk?KWcJo%1Xfl_O;af|xFwLA=)tCm40Zxmc3nWrz%QD`kO^Yb!v3 z!LYwCUHcO!wZ<_$KvfKLa@mprce3D^PORQskawcW-U9S+LA^Q(x-Woq8HIb-=0iyD zNj)0vHe!rVBQ0v(oS|0+iVTnOKq4*`563v|^7Ku90l_cFg1WV;!HjKynm~}hT&G%C z(H{Vn-v9|Yo5K#73aFkR3<ra%!8s>XRMwwiTo72}1SNkN%A%@)BZ|v8vP?sEsb+FB z=%R7~9oH8nxy8o2*mTOTs3WmUpg|Rl%c_Ed=%^fOj^d(ZN*zp{j)gQ}ZAeTjL^YjG zarwKF3;mhKt5QIsV(94(XCzie%uGVw$&?bLBvZ53k7B5T$*U6-Cqk4(RW<}Rm5p{` zfGRhh@dKM4yCi_h>mOI*R8fp&jjt$N$fZ|KSm#SlrU6Mwkd&mLLFJ{ns+x_W{jPOd z#VY}&Nk+8+&?<(yp}`93!CFo5bHD}VE@5dANd<I6ZZO)Aw6tWr!Mpf=4T378IJ%MC zf;zj_!0hX|p5&%4Uj}G=SYD_p5fPS601Xkb5|d`jom{T1ay`#{50BG7D^VN`vQ+A@ z-ji~`;bDUKlLRK`Z3bDw!PCR9nX5+dI<ep?t=}{a<gu#p3ey415HrmgG{)8n*j!L_ zSxJ0wb!mZW0A@qrA}=|k6V3Eiah}Yse4}ze6ve?2JG9A>NpFH%B0K#oK2r#YOaPMs zRskXtL{T^5wlc#CTLK7di0c>Yxoyv7_J%hr*}L^Aa_+$m*d&D~cMhsqPNt2V<+&7p zH0H~MT|1v-P|K;DgPYWg{pO$rRa6QlZF4XzqRORdy434{Nu{zEnWVL~+nicD>fqIQ z(oSBCPwy!DN*sZ4QAc8&YB)2RV(Uh+*`(UD0FT$Cj&ag``C|4W!Wsw&M2teoMMj(w zY~lPOqH-%-w%wjE8{ZID#982q;v3?M`VP3F_=dQ&Z~&E#Ziq>pbpw%#ZHPySHSnm| zhImZRyTdmX+X|06UNU&hfl|;|-6c~-7TRU-NNhuToaC5ORTbM1kCPgMN5!_pgSMbu z4i91*;xQ6qtq?+FLo7jMgP0K65R2&@OG}7sh$T!ipd~~$#9~-sddY`3#Nnqz5aPod z;xKG5bolUAIE<ti9GlCDUldT<+yX(-fvesNF~y!wN26u#LW3y)G$z0g7U!(dX4wp_ z75CySCGVIU<2=(_XrVP=OtRh3l>$kgXO1x-%;eGxDalqenfJ6~vOMnaG;e|+lpyxH zw?8}@Bt_H!rO{@RYY@1Z5*CoH`Oc1@l;6@sQ-M*#>%fie#%gXDi3wF<bgcq~yq4y= zp2XltP#QXsPF}CgYiS_oJ=h@P5EPyhC_%X?Chp%A&>-O}SxV-+R}dOS!UOg={fwx= zuQtp`-U_kmqJUN<ZiO@!ihwsxKO^dh5drhIj0n;n=nZLG(Q5kL54V6f$v6XYKhjg= zKI06?ogQkFW}WvGR1tNiVUMd}+YU8Yl&(!#q_VbZfsr{bhQtg%pieTkLT;#shO5YZ zGu4C|+H%iW4cNTC7N5CQCF#5y+L7jLh2ArR%ZV1nE7OGJoEd$Km>TTc3aQhyf+g@J zO5TcUBZIc-=Jtr04ed3dkh2wf)p26PYH>%&TOrm<%D@ku+6^rpW&t?<;+7b!TS^<s z{&*WeK8!uctHlj)e?ksOAI7rVI*Y>5xv}!fEF*jwpzvYrJ(kZ8;d*3%GfuYDgitX( zw&V}x;$u7>Z<aP%$D<KXJop2;c+{S+VJU~z_#5_=GwmnCtx=E*q8(R?7ER6KpfdiL zZHPArd7hpp4mU>oeC=0$#nG+o&%(%nCyKo>55Y{xc%?Go$!tvP0XW-Q933rjU)Olo za+VF^M5db5C(uSY6A^Rf5S0cgEva&hSZYc{2=GMla1xi{y>1o|=%|>Zl_(%P_#_)( zT@VAb+zFbsfYnh9yE1&}BtVBq=*~==eNO|_hdc2bPo0YS2<<N=1np9$6|Spb6f@v~ zjSB`I@mOHNBR|{<&d|(rIb$gbEFpqEx@tzR`@F}Q4Xl|=QI}?;IM;*O1|LE_Fs0;D zcK-yfWzE(=g|N!0o3YP9Smm^$SNU>VZ3b81)pRmC^+<d&isssC%zfBUYxsjvZJ(bW z_m1sTn0<^@oOao3%zJQK$QQ#8#qr|Z=aTsOBz`f8U%qigUUwwP@}?J#wfFb>aE`a2 znT0+Uvd>D3qe|L?Nq{$;GA+ttLOa!|lSMBIQ1p^E;{XHUfB}bEc!C_qY>QcQ?=CJE z9-{x|r#l$D*P5N_{BU<I40)<?pqdL{Efl5(fyc1v2+jm?DZ67O3IbKA7*%r7N~DeX zK+~2i!?bk9nszR5QPl6gjOnOsD4Rg@j*dX{`b>GMOrUvnOrXyTv^fgQF*T862Y$|g zm&`HZK>^@60Z8m2cZP&8!~h>6faN30`MH(WG=LJL8cqWxdf_b>#qk#kv3!~Tlq&Re zA~NOAR7Hs$p3d?*#gzgA7p&ma)fzZp6(JKlyh+TU*<m9Ht4EsiO$UQRju&|AuLf`{ z1?6MRcWHyja+6cOC$fJ^HE!HpWK~=2;iAjr5qr4cXevp6APS8Q5e#*-w~IX~TuSxD z%G$k@BePX$jW~dVa#YA4%!*~JR~X**tI?GG?}FmdK~@}c-i}8YOPxBS<<SHM#rC4K zSn2KW&uekuA%QRnmqvr`b5yt@2bCCL2#5Bdh4HY#uDmi$ymmgpo5DTFOTTdsY#Kod zvC*<Q@-e#V<4Tg&$&dida!~LYDw{@JY4E3l+qLqVBtmSuAXFqPu#%VwNr04J2p2|E ziT^NY601c(xu8^460H@sun*u4R~{#Itb%?jUv6|z8UfEXN_uVe4c1PJTu0-8ovZ7V z1X1aw0qH0<;u7JIdjuS4!gpV=!E50;Ya4O0iS+|;xgfib?)vh|9qIsjk~Z7`4G{oa zliiGf#?<L%2)V+FQvGeC37FcFF|^C#h=pDc7HdFZLww=I&hTy+4;u9xt$*>=w+8nF z=m};y8N$v{9=|DATqY$^cajpm+H6KMTIQB|s#HKmnDA*b953hiA6(oWWqL@J0wNU? z;V5mO%(yBAoc!WGQF)n_@({pjQYSs+I|T@QxWYoKtPSi#Ve(2?(3+$|ra<|iZTic3 zR`se<Y(Kw;4NNEMePy=!QZXMBvGcrpB3Yi3!}Db`+NNinb+3T{{sc#mG!KI3d|*F) zfWE{EAR{a70IVXyAQNn99#P{92vkg#`X!wZ#;F1J>M%#Wo}vLJQ*~Gjm?*C#iDtn} zNiXfVLTKL9pnpJ@&`kTs;xN!9WNKY0i<(!`WdEWxB?y#<rdKO5`i5;Q9X-PDJ{GF; zMP=S$$x)<=5uJ*5(x0J1K!=Et-qKnia2dcM!mEhIjX!CV?gyY)NfRYhn%4={f=DL` zp3--W`bYpRDkh99e)Y9{b^uLFs(kvC#)SWxArALt18rn&Su+x0Zz3#lHyN<}k#)6f zD04}^Kb($pyPJDb0FNWdFHp!Ys<xhheLml;Gv_w64RBCQ^(VPeYB-6FlGV{ql&(%c zqI7+Z5M_8;<eaD_%bu6ucqzv|-4I2aJC+Kq4(I}NHbYF?347WV(7MFUk!sGM--uEt zZH`cNfyJj%Hb<wpY0;^a&CvyUiW^G?dTH(2AzoyohHiz*%VE^W&ZzK(c{HEm8fb;< zCb&Gegzs~sTk-OXpp27bX?MAFtA9JcI~TAAkpG^D@GAcq6l-JL1CrNy#WEwP(|(?I zuB9AQDdZ{@EV*z-($QTCciJ#^=7wbJk?1k&#I1?eNIYieze#6qO0pWA3UA~kW_vE@ zgu~=*O0I3H?zxb3R_<!732(O$2CZ#cJe+OFRrW|spH11i(OQMI3|{Mps{DNzX$LDf btD<y9GNDBjPBm01Mj}L_#BTZj^uPZH4I@A( literal 0 HcmV?d00001 diff --git a/package.json b/package.json index fabd64ef..32251ad0 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,8 @@ "license": "Apache-2.0", "description": "Local grep-like search tool for your codebase.", "dependencies": { + "osgrep-core": "file:../osgrep_v2", "@clack/prompts": "^0.11.0", - "@huggingface/transformers": "^3.8.0", "@lancedb/lancedb": "^0.22.3", "@modelcontextprotocol/sdk": "^1.24.3", "apache-arrow": "^18.1.0", @@ -60,9 +60,7 @@ "fast-glob": "^3.3.3", "ignore": "^5.0.0", "lmdb": "^3.4.4", - "onnxruntime-node": "1.21.0", "ora": "^5.4.1", - "piscina": "^5.1.4", "simsimd": "^6.5.5", "uuid": "^9.0.1", "web-tree-sitter": "^0.25.10", diff --git a/plan.md b/plan.md new file mode 100644 index 00000000..a2f437bf --- /dev/null +++ b/plan.md @@ -0,0 +1,332 @@ +# osgrep v3 Simplification Plan + +## Current State (Post-Migration) + +We've completed the major architectural change: +- **Rust core (osgrep-core)**: ONNX Runtime for dense + ColBERT embeddings +- **TypeScript orchestration**: syncer, watcher, chunker, searcher, LanceDB +- **Bridge**: `src/lib/native/index.ts` connects TS to Rust + +**Results so far:** +- Code reduced from ~6,600 to ~4,400 lines (34% reduction) +- Binary size reduced from 58MB to 27MB (53% smaller) +- Removed: worker pool, skeleton, trace, graph modules + +--- + +## Phase 1: Dependency Cleanup + +### 1.1 Remove Dead Dependencies from package.json + +**File:** `package.json` + +Remove these dependencies that are no longer used: + +```json +// REMOVE from dependencies: +"onnxruntime-node": "1.21.0", // Now in Rust +"@huggingface/transformers": "^3.8.0", // Now in Rust +"piscina": "^5.1.4", // Worker pool deleted +``` + +**Why:** These were only used by the old TS embedding pipeline which is now handled by Rust. + +### 1.2 Verify No Imports Remain + +Search for any remaining imports of removed packages: +```bash +grep -r "onnxruntime" src/ +grep -r "@huggingface/transformers" src/ +grep -r "piscina" src/ +``` + +--- + +## Phase 2: Simplify Search Pipeline + +### 2.1 Delete Intent Detection + +**Delete file:** `src/lib/search/intent.ts` + +The intent detection system adds complexity without clear value: +- Regex-based query classification ("where is" → DEFINITION, "how does" → FLOW) +- Intent-based boosting in search results +- Not actually improving search quality in practice + +### 2.2 Simplify Searcher + +**File:** `src/lib/search/searcher.ts` + +**Current complexity to remove:** + +1. **Intent-based boosting** (lines 186-196): + ```typescript + // DELETE THIS BLOCK + if (intent) { + if (intent.type === "DEFINITION" && record.role === "DEFINITION") { + boostFactor *= 1.2; + } + // ... more intent conditions + } + ``` + +2. **Role-based boosting** (lines 165-180): + ```typescript + // SIMPLIFY: Remove or reduce this complexity + if (record.role === "ORCHESTRATION") { + boostFactor *= 1.5; + } else if (record.role === "DEFINITION") { + boostFactor *= 1.2; + } + // ... + ``` + +3. **Reference count boosting** (lines 174-180): + ```typescript + // DELETE: Over-engineered + if (refs > 5) boostFactor *= 1.1; + if (refs > 15) boostFactor *= 1.25; + ``` + +**Simplified `applyStructureBoost` function:** + +```typescript +private applyStructureBoost( + record: Partial<VectorRecord>, + score: number, +): number { + let adjusted = score; + + // Anchor penalty (anchors are recall helpers, not results) + if (record.is_anchor) { + adjusted *= 0.99; + } + + // Test file penalty + const pathStr = (record.path || "").toLowerCase(); + const isTestPath = + /(^|\/)(__tests__|tests?|specs?|benchmark)(\/|$)/i.test(pathStr) || + /\.(test|spec)\.[cm]?[jt]sx?$/i.test(pathStr); + if (isTestPath) { + adjusted *= 0.5; + } + + // Docs/config penalty + if ( + pathStr.endsWith(".md") || + pathStr.endsWith(".json") || + pathStr.endsWith(".lock") || + pathStr.includes("/docs/") + ) { + adjusted *= 0.6; + } + + return adjusted; +} +``` + +### 2.3 Remove Intent from Search Signature + +**Current:** +```typescript +async search( + query: string, + top_k?: number, + _search_options?: { rerank?: boolean }, + _filters?: SearchFilter, + pathPrefix?: string, + intent?: SearchIntent, // REMOVE + signal?: AbortSignal, +): Promise<SearchResponse> +``` + +**After:** +```typescript +async search( + query: string, + top_k?: number, + options?: { rerank?: boolean }, + filters?: SearchFilter, + pathPrefix?: string, + signal?: AbortSignal, +): Promise<SearchResponse> +``` + +### 2.4 Update Search Command + +**File:** `src/commands/search.ts` + +Remove any intent detection calls and simplify the search invocation. + +--- + +## Phase 3: Code Quality Cleanup + +### 3.1 Remove Environment Variable Overrides + +The searcher has many env var overrides that add cognitive load: + +```typescript +// Consider removing or documenting these: +OSGREP_ANCHOR_PENALTY +OSGREP_TEST_PENALTY +OSGREP_DOC_PENALTY +OSGREP_PRE_K +OSGREP_STAGE1_K +OSGREP_STAGE2_K +OSGREP_RERANK_TOP +OSGREP_RERANK_BLEND +OSGREP_MAX_PER_FILE +``` + +**Decision:** Either: +1. Remove all env vars and use fixed reasonable defaults +2. Keep only 2-3 most useful ones (e.g., `OSGREP_TOP_K`, `OSGREP_RERANK`) + +### 3.2 Audit Remaining Files + +Check for dead code in: +- `src/lib/store/` - vector-db.ts, types.ts +- `src/lib/core/` - chunker, languages +- `src/lib/utils/` - filter-builder, etc. + +### 3.3 Type Cleanup + +Remove unused types from `src/lib/store/types.ts` related to deleted features. + +--- + +## Phase 4: Testing & Validation + +### 4.1 Manual Testing + +```bash +# Clean slate +rm -rf ~/.osgrep/indices/* + +# Index a test repo +bun src/index.ts index --path . --reset + +# Search queries +bun src/index.ts "how does embedding work" +bun src/index.ts "where is the config" +bun src/index.ts "vector search implementation" +``` + +### 4.2 Run Existing Tests + +```bash +bun test +``` + +### 4.3 Typecheck + +```bash +bunx tsc --noEmit +``` + +--- + +## Phase 5: Optional Further Simplifications + +### 5.1 Consider Removing Two-Stage Rerank + +The current pipeline has: +1. Vector search → top 500 +2. RRF fusion (vector + FTS) +3. Stage 1: Pooled cosine filter → top 200 +4. Stage 2: ColBERT MaxSim rerank → top 40 +5. Structure boost + diversification + +**Simpler alternative:** +1. Vector search → top 100 +2. ColBERT rerank → top 20 +3. Basic penalties (test/docs) + +### 5.2 Consider Removing FTS Fallback + +The FTS (full-text search) adds code but may not improve results significantly. Consider making it optional or removing entirely. + +### 5.3 Simplify Output Formatting + +`src/lib/output/formatter.ts` has complex role coloring and breadcrumb logic. Could be simplified to just show: +- File path + line number +- Code snippet +- Score + +--- + +## File Inventory (Post-Simplification) + +### Keep (Core) +``` +src/ +├── index.ts # CLI entry +├── config.ts # Constants +├── commands/ +│ ├── index.ts # Index command +│ ├── search.ts # Search command +│ └── serve.ts # MCP server +├── lib/ +│ ├── native/ +│ │ └── index.ts # Rust bridge +│ ├── index/ +│ │ ├── syncer.ts # File sync +│ │ └── watcher.ts # File watcher +│ ├── search/ +│ │ └── searcher.ts # Search logic (simplified) +│ ├── store/ +│ │ ├── vector-db.ts # LanceDB wrapper +│ │ └── types.ts # Types +│ ├── core/ +│ │ ├── chunker.ts # Tree-sitter chunking +│ │ └── languages.ts # Language configs +│ ├── output/ +│ │ ├── formatter.ts # Human output +│ │ └── json-formatter.ts # JSON output +│ ├── utils/ +│ │ └── ... # Utilities +│ └── workers/ +│ └── orchestrator.ts # Embedding orchestrator +``` + +### Delete (This Phase) +``` +src/lib/search/intent.ts # Intent detection +``` + +### Already Deleted (Previous Phase) +``` +src/lib/skeleton/ # Skeleton feature +src/lib/graph/ # Call graph +src/lib/workers/pool.ts # Worker pool +src/lib/workers/embeddings/ # TS embedding code +src/commands/skeleton.ts # Skeleton command +src/commands/trace.ts # Trace command +``` + +--- + +## Execution Checklist + +- [ ] Phase 1.1: Remove dead deps from package.json +- [ ] Phase 1.2: Verify no imports remain +- [ ] Phase 2.1: Delete intent.ts +- [ ] Phase 2.2: Simplify applyStructureBoost +- [ ] Phase 2.3: Remove intent from search signature +- [ ] Phase 2.4: Update search command +- [ ] Phase 3.1: Decide on env var strategy +- [ ] Phase 4.1: Manual testing +- [ ] Phase 4.2: Run tests +- [ ] Phase 4.3: Typecheck +- [ ] Phase 5: Evaluate optional simplifications + +--- + +## Success Criteria + +1. **Code size:** < 4,000 lines of TypeScript +2. **Dependencies:** Remove 3 unused packages +3. **Complexity:** No intent detection, simplified boosting +4. **Functionality:** Index and search work correctly +5. **Maintainability:** Easy to understand search pipeline diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f75cda9..4351871b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ dependencies: '@clack/prompts': specifier: ^0.11.0 version: 0.11.0 - '@huggingface/transformers': - specifier: ^3.8.0 - version: 3.8.1 '@lancedb/lancedb': specifier: ^0.22.3 version: 0.22.3(apache-arrow@18.1.0) @@ -44,15 +41,12 @@ dependencies: lmdb: specifier: ^3.4.4 version: 3.4.4 - onnxruntime-node: - specifier: 1.21.0 - version: 1.21.0 ora: specifier: ^5.4.1 version: 5.4.1 - piscina: - specifier: ^5.1.4 - version: 5.1.4 + osgrep-core: + specifier: file:../osgrep_v2 + version: file:../osgrep_v2 simsimd: specifier: ^6.5.5 version: 6.5.5 @@ -228,14 +222,6 @@ packages: '@jridgewell/trace-mapping': 0.3.9 dev: true - /@emnapi/runtime@1.7.1: - resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} - requiresBuild: true - dependencies: - tslib: 2.8.1 - dev: false - optional: true - /@esbuild/aix-ppc64@0.21.5: resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -443,25 +429,6 @@ packages: dev: true optional: true - /@huggingface/jinja@0.5.3: - resolution: {integrity: sha512-asqfZ4GQS0hD876Uw4qiUb7Tr/V5Q+JZuo2L+BtdrD4U40QU58nIRq3ZSgAzJgT874VLjhGVacaYfrdpXtEvtA==} - engines: {node: '>=18'} - dev: false - - /@huggingface/transformers@3.8.1: - resolution: {integrity: sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==} - dependencies: - '@huggingface/jinja': 0.5.3 - onnxruntime-node: 1.21.0 - onnxruntime-web: 1.22.0-dev.20250409-89f8206ba4 - sharp: 0.34.5 - dev: false - - /@img/colour@1.0.0: - resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} - engines: {node: '>=18'} - dev: false - /@img/sharp-darwin-arm64@0.33.5: resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -473,17 +440,6 @@ packages: dev: true optional: true - /@img/sharp-darwin-arm64@0.34.5: - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 - dev: false - optional: true - /@img/sharp-darwin-x64@0.33.5: resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -495,17 +451,6 @@ packages: dev: true optional: true - /@img/sharp-darwin-x64@0.34.5: - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 - dev: false - optional: true - /@img/sharp-libvips-darwin-arm64@1.0.4: resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} cpu: [arm64] @@ -514,14 +459,6 @@ packages: dev: true optional: true - /@img/sharp-libvips-darwin-arm64@1.2.4: - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - /@img/sharp-libvips-darwin-x64@1.0.4: resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} cpu: [x64] @@ -530,14 +467,6 @@ packages: dev: true optional: true - /@img/sharp-libvips-darwin-x64@1.2.4: - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - /@img/sharp-libvips-linux-arm64@1.0.4: resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] @@ -546,14 +475,6 @@ packages: dev: true optional: true - /@img/sharp-libvips-linux-arm64@1.2.4: - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@img/sharp-libvips-linux-arm@1.0.5: resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] @@ -562,38 +483,6 @@ packages: dev: true optional: true - /@img/sharp-libvips-linux-arm@1.2.4: - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linux-ppc64@1.2.4: - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linux-riscv64@1.2.4: - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linux-s390x@1.2.4: - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@img/sharp-libvips-linux-x64@1.0.4: resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] @@ -602,14 +491,6 @@ packages: dev: true optional: true - /@img/sharp-libvips-linux-x64@1.2.4: - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@img/sharp-libvips-linuxmusl-arm64@1.0.4: resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] @@ -618,14 +499,6 @@ packages: dev: true optional: true - /@img/sharp-libvips-linuxmusl-arm64@1.2.4: - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@img/sharp-libvips-linuxmusl-x64@1.0.4: resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] @@ -634,14 +507,6 @@ packages: dev: true optional: true - /@img/sharp-libvips-linuxmusl-x64@1.2.4: - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@img/sharp-linux-arm64@0.33.5: resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -653,17 +518,6 @@ packages: dev: true optional: true - /@img/sharp-linux-arm64@0.34.5: - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 - dev: false - optional: true - /@img/sharp-linux-arm@0.33.5: resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -675,50 +529,6 @@ packages: dev: true optional: true - /@img/sharp-linux-arm@0.34.5: - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 - dev: false - optional: true - - /@img/sharp-linux-ppc64@0.34.5: - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ppc64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 - dev: false - optional: true - - /@img/sharp-linux-riscv64@0.34.5: - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [riscv64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 - dev: false - optional: true - - /@img/sharp-linux-s390x@0.34.5: - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 - dev: false - optional: true - /@img/sharp-linux-x64@0.33.5: resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -730,17 +540,6 @@ packages: dev: true optional: true - /@img/sharp-linux-x64@0.34.5: - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 - dev: false - optional: true - /@img/sharp-linuxmusl-arm64@0.33.5: resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -752,17 +551,6 @@ packages: dev: true optional: true - /@img/sharp-linuxmusl-arm64@0.34.5: - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - dev: false - optional: true - /@img/sharp-linuxmusl-x64@0.33.5: resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -774,45 +562,6 @@ packages: dev: true optional: true - /@img/sharp-linuxmusl-x64@0.34.5: - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - dev: false - optional: true - - /@img/sharp-wasm32@0.34.5: - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - requiresBuild: true - dependencies: - '@emnapi/runtime': 1.7.1 - dev: false - optional: true - - /@img/sharp-win32-arm64@0.34.5: - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-win32-ia32@0.34.5: - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: false - optional: true - /@img/sharp-win32-x64@0.33.5: resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -822,15 +571,6 @@ packages: dev: true optional: true - /@img/sharp-win32-x64@0.34.5: - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - /@isaacs/balanced-match@4.0.1: resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -848,6 +588,7 @@ packages: engines: {node: '>=18.0.0'} dependencies: minipass: 7.1.2 + dev: true /@jest/schemas@29.6.3: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} @@ -1096,184 +837,6 @@ packages: dev: false optional: true - /@napi-rs/nice-android-arm-eabi@1.1.1: - resolution: {integrity: sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==} - engines: {node: '>= 10'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-android-arm64@1.1.1: - resolution: {integrity: sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-darwin-arm64@1.1.1: - resolution: {integrity: sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-darwin-x64@1.1.1: - resolution: {integrity: sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-freebsd-x64@1.1.1: - resolution: {integrity: sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-linux-arm-gnueabihf@1.1.1: - resolution: {integrity: sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-linux-arm64-gnu@1.1.1: - resolution: {integrity: sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-linux-arm64-musl@1.1.1: - resolution: {integrity: sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-linux-ppc64-gnu@1.1.1: - resolution: {integrity: sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==} - engines: {node: '>= 10'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-linux-riscv64-gnu@1.1.1: - resolution: {integrity: sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==} - engines: {node: '>= 10'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-linux-s390x-gnu@1.1.1: - resolution: {integrity: sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==} - engines: {node: '>= 10'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-linux-x64-gnu@1.1.1: - resolution: {integrity: sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-linux-x64-musl@1.1.1: - resolution: {integrity: sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-openharmony-arm64@1.1.1: - resolution: {integrity: sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [openharmony] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-win32-arm64-msvc@1.1.1: - resolution: {integrity: sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-win32-ia32-msvc@1.1.1: - resolution: {integrity: sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-win32-x64-msvc@1.1.1: - resolution: {integrity: sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice@1.1.1: - resolution: {integrity: sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==} - engines: {node: '>= 10'} - requiresBuild: true - optionalDependencies: - '@napi-rs/nice-android-arm-eabi': 1.1.1 - '@napi-rs/nice-android-arm64': 1.1.1 - '@napi-rs/nice-darwin-arm64': 1.1.1 - '@napi-rs/nice-darwin-x64': 1.1.1 - '@napi-rs/nice-freebsd-x64': 1.1.1 - '@napi-rs/nice-linux-arm-gnueabihf': 1.1.1 - '@napi-rs/nice-linux-arm64-gnu': 1.1.1 - '@napi-rs/nice-linux-arm64-musl': 1.1.1 - '@napi-rs/nice-linux-ppc64-gnu': 1.1.1 - '@napi-rs/nice-linux-riscv64-gnu': 1.1.1 - '@napi-rs/nice-linux-s390x-gnu': 1.1.1 - '@napi-rs/nice-linux-x64-gnu': 1.1.1 - '@napi-rs/nice-linux-x64-musl': 1.1.1 - '@napi-rs/nice-openharmony-arm64': 1.1.1 - '@napi-rs/nice-win32-arm64-msvc': 1.1.1 - '@napi-rs/nice-win32-ia32-msvc': 1.1.1 - '@napi-rs/nice-win32-x64-msvc': 1.1.1 - dev: false - optional: true - /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1315,49 +878,6 @@ packages: semver: 7.7.3 dev: true - /@protobufjs/aspromise@1.1.2: - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - dev: false - - /@protobufjs/base64@1.1.2: - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - dev: false - - /@protobufjs/codegen@2.0.4: - resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} - dev: false - - /@protobufjs/eventemitter@1.1.0: - resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} - dev: false - - /@protobufjs/fetch@1.1.0: - resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/inquire': 1.1.0 - dev: false - - /@protobufjs/float@1.0.2: - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - dev: false - - /@protobufjs/inquire@1.1.0: - resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} - dev: false - - /@protobufjs/path@1.1.2: - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} - dev: false - - /@protobufjs/pool@1.1.0: - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - dev: false - - /@protobufjs/utf8@1.1.0: - resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} - dev: false - /@rollup/rollup-android-arm-eabi@4.53.3: resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} cpu: [arm] @@ -1582,6 +1102,7 @@ packages: resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} dependencies: undici-types: 7.16.0 + dev: true /@types/uuid@9.0.8: resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} @@ -1775,11 +1296,6 @@ packages: - supports-color dev: false - /boolean@3.2.0: - resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - dev: false - /braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -1886,6 +1402,7 @@ packages: /chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} + dev: true /cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} @@ -2041,24 +1558,6 @@ packages: clone: 1.0.4 dev: false - /define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - dev: false - - /define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - dev: false - /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -2069,10 +1568,6 @@ packages: engines: {node: '>=8'} dev: false - /detect-node@2.1.0: - resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} - dev: false - /diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2144,10 +1639,6 @@ packages: es-errors: 1.3.0 dev: false - /es6-error@4.1.1: - resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} - dev: false - /esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -2194,11 +1685,6 @@ packages: dev: false optional: true - /escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - dev: false - /estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} dependencies: @@ -2359,10 +1845,6 @@ packages: resolution: {integrity: sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA==} dev: false - /flatbuffers@25.9.23: - resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==} - dev: false - /forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -2452,26 +1934,6 @@ packages: path-scurry: 2.0.1 dev: true - /global-agent@3.0.0: - resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} - engines: {node: '>=10.0'} - dependencies: - boolean: 3.2.0 - es6-error: 4.1.1 - matcher: 3.0.0 - roarr: 2.15.4 - semver: 7.7.3 - serialize-error: 7.0.1 - dev: false - - /globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 - dev: false - /gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -2481,21 +1943,11 @@ packages: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} dev: true - /guid-typescript@1.0.9: - resolution: {integrity: sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==} - dev: false - /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} dev: false - /has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - dependencies: - es-define-property: 1.0.1 - dev: false - /has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -2668,10 +2120,6 @@ packages: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} dev: false - /json-stringify-safe@5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - dev: false - /lmdb@3.4.4: resolution: {integrity: sha512-+Y2DqovevLkb6DrSQ6SXTYLEd6kvlRbhsxzgJrk7BUfOVA/mt21ak6pFDZDKxiAczHMWxrb02kXBTSTIA0O94A==} hasBin: true @@ -2718,10 +2166,6 @@ packages: is-unicode-supported: 0.1.0 dev: false - /long@5.3.2: - resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - dev: false - /loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} dependencies: @@ -2762,13 +2206,6 @@ packages: - supports-color dev: true - /matcher@3.0.0: - resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} - engines: {node: '>=10'} - dependencies: - escape-string-regexp: 4.0.0 - dev: false - /math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2897,12 +2334,14 @@ packages: /minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + dev: true /minizlib@3.1.0: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} dependencies: minipass: 7.1.2 + dev: true /mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -3021,11 +2460,6 @@ packages: engines: {node: '>= 0.4'} dev: false - /object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - dev: false - /on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -3053,35 +2487,6 @@ packages: mimic-fn: 4.0.0 dev: true - /onnxruntime-common@1.21.0: - resolution: {integrity: sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==} - dev: false - - /onnxruntime-common@1.22.0-dev.20250409-89f8206ba4: - resolution: {integrity: sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==} - dev: false - - /onnxruntime-node@1.21.0: - resolution: {integrity: sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==} - os: [win32, darwin, linux] - requiresBuild: true - dependencies: - global-agent: 3.0.0 - onnxruntime-common: 1.21.0 - tar: 7.5.2 - dev: false - - /onnxruntime-web@1.22.0-dev.20250409-89f8206ba4: - resolution: {integrity: sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==} - dependencies: - flatbuffers: 25.9.23 - guid-typescript: 1.0.9 - long: 5.3.2 - onnxruntime-common: 1.22.0-dev.20250409-89f8206ba4 - platform: 1.3.6 - protobufjs: 7.5.4 - dev: false - /ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} @@ -3178,13 +2583,6 @@ packages: engines: {node: '>=12'} dev: true - /piscina@5.1.4: - resolution: {integrity: sha512-7uU4ZnKeQq22t9AsmHGD2w4OYQGonwFnTypDypaWi7Qr2EvQIFVtG8J5D/3bE7W123Wdc9+v4CZDu5hJXVCtBg==} - engines: {node: '>=20.x'} - optionalDependencies: - '@napi-rs/nice': 1.1.1 - dev: false - /pkce-challenge@5.0.1: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} @@ -3200,7 +2598,9 @@ packages: /platform@1.3.6: resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} + requiresBuild: true dev: false + optional: true /postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} @@ -3233,25 +2633,6 @@ packages: retry: 0.12.0 dev: true - /protobufjs@7.5.4: - resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} - engines: {node: '>=12.0.0'} - requiresBuild: true - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.4 - '@protobufjs/eventemitter': 1.1.0 - '@protobufjs/fetch': 1.1.0 - '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.0 - '@protobufjs/path': 1.1.2 - '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.0 - '@types/node': 24.10.1 - long: 5.3.2 - dev: false - /proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -3336,18 +2717,6 @@ packages: engines: {iojs: '>=1.0.0', node: '>=0.10.0'} dev: false - /roarr@2.15.4: - resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} - engines: {node: '>=8.0'} - dependencies: - boolean: 3.2.0 - detect-node: 2.1.0 - globalthis: 1.0.4 - json-stringify-safe: 5.0.1 - semver-compare: 1.0.0 - sprintf-js: 1.1.3 - dev: false - /rollup@4.53.3: resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -3413,14 +2782,11 @@ packages: dev: false optional: true - /semver-compare@1.0.0: - resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} - dev: false - /semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true + dev: true /send@1.2.0: resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} @@ -3441,13 +2807,6 @@ packages: - supports-color dev: false - /serialize-error@7.0.1: - resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} - engines: {node: '>=10'} - dependencies: - type-fest: 0.13.1 - dev: false - /serve-static@2.2.0: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} @@ -3464,41 +2823,6 @@ packages: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} dev: false - /sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - requiresBuild: true - dependencies: - '@img/colour': 1.0.0 - detect-libc: 2.1.2 - semver: 7.7.3 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 - dev: false - /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3609,10 +2933,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /sprintf-js@1.1.3: - resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - dev: false - /ssri@13.0.0: resolution: {integrity: sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==} engines: {node: ^20.17.0 || >=22.9.0} @@ -3690,6 +3010,7 @@ packages: minipass: 7.1.2 minizlib: 3.1.0 yallist: 5.0.0 + dev: true /thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} @@ -3784,11 +3105,6 @@ packages: engines: {node: '>=4'} dev: true - /type-fest@0.13.1: - resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} - engines: {node: '>=10'} - dev: false - /type-is@2.0.1: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} @@ -3831,6 +3147,7 @@ packages: /undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + dev: true /unique-filename@5.0.0: resolution: {integrity: sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==} @@ -4071,6 +3388,7 @@ packages: /yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} + dev: true /yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} @@ -4110,3 +3428,8 @@ packages: /zod@4.1.13: resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} + + file:../osgrep_v2: + resolution: {directory: ../osgrep_v2, type: directory} + name: osgrep-core + dev: false diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 58874f7d..6595f6b3 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -2,7 +2,8 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { Command } from "commander"; -import { MODEL_IDS, PATHS } from "../config"; +import { PATHS } from "../config"; +import { initNative } from "../lib/native"; import { gracefulExit } from "../lib/utils/exit"; import { findProjectRoot } from "../lib/utils/project-root"; @@ -12,9 +13,7 @@ export const doctor = new Command("doctor") console.log("🏥 osgrep Doctor\n"); const root = PATHS.globalRoot; - const models = PATHS.models; const grammars = PATHS.grammars; - const modelIds = [MODEL_IDS.embed, MODEL_IDS.colbert]; const checkDir = (name: string, p: string) => { const exists = fs.existsSync(p); @@ -23,24 +22,15 @@ export const doctor = new Command("doctor") }; checkDir("Root", root); - checkDir("Models", models); checkDir("Grammars", grammars); - const modelStatuses = modelIds.map((id) => { - const modelPath = path.join(models, ...id.split("/")); - return { id, path: modelPath, exists: fs.existsSync(modelPath) }; - }); - - modelStatuses.forEach(({ id, path: p, exists }) => { - const symbol = exists ? "✅" : "❌"; - console.log(`${symbol} Model: ${id} (${p})`); - }); - - const missingModels = modelStatuses.filter(({ exists }) => !exists); - if (missingModels.length > 0) { - console.log( - "❌ Some models are missing; osgrep will try bundled copies first, then download.", - ); + try { + await initNative(); + console.log("✅ Native models initialized"); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + console.log(`❌ Native init failed: ${msg}`); + console.log(" Try: osgrep setup"); } console.log(`\nLocal Project: ${process.cwd()}`); diff --git a/src/commands/search.ts b/src/commands/search.ts index 6765db03..09d93ffd 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -1,4 +1,3 @@ -import * as fs from "node:fs"; import * as path from "node:path"; import type { Command } from "commander"; import { Command as CommanderCommand } from "commander"; @@ -10,8 +9,6 @@ import { import { initialSync } from "../lib/index/syncer"; import { Searcher } from "../lib/search/searcher"; import { ensureSetup } from "../lib/setup/setup-helpers"; -import { Skeletonizer } from "../lib/skeleton"; -import { getStoredSkeleton } from "../lib/skeleton/retriever"; import type { ChunkType, FileMetadata, @@ -262,95 +259,6 @@ function formatCompactTable( return formatCompactPretty(hits, projectRoot, query, termWidth, true); } -// Reuse Skeletonizer instance -let globalSkeletonizer: Skeletonizer | null = null; - -async function outputSkeletons( - results: any[], - projectRoot: string, - limit: number, - db?: VectorDB | null, -): Promise<void> { - const seenPaths = new Set<string>(); - const filesToProcess: string[] = []; - - for (const result of results) { - const p = (result.metadata as any)?.path; - if (typeof p === "string" && !seenPaths.has(p)) { - seenPaths.add(p); - filesToProcess.push(p); - if (filesToProcess.length >= limit) break; - } - } - - if (filesToProcess.length === 0) { - console.log("No skeleton matches found."); - return; - } - - // Reuse or init skeletonizer for fallbacks - if (!globalSkeletonizer) { - globalSkeletonizer = new Skeletonizer(); - // Lazy init only if we actually fallback - } - - const skeletonOpts = { includeSummary: true }; - const skeletonResults: Array<{ - file: string; - skeleton: string; - tokens: number; - error?: string; - }> = []; - - for (const relPath of filesToProcess) { - // 1. Try DB cache - if (db) { - const cached = await getStoredSkeleton(db, relPath); - if (cached) { - skeletonResults.push({ - file: relPath, - skeleton: cached, - tokens: Math.ceil(cached.length / 4), // Rough estimate - }); - continue; - } - } - - // 2. Fallback to fresh generation - await globalSkeletonizer.init(); - const absPath = path.resolve(projectRoot, relPath); - if (!fs.existsSync(absPath)) { - skeletonResults.push({ - file: relPath, - skeleton: `// File not found: ${relPath}`, - tokens: 0, - error: "File not found", - }); - continue; - } - - const content = fs.readFileSync(absPath, "utf-8"); - const res = await globalSkeletonizer.skeletonizeFile( - relPath, - content, - skeletonOpts, - ); - skeletonResults.push({ - file: relPath, - skeleton: res.skeleton, - tokens: res.tokenEstimate, - error: res.error, - }); - } - - // Since search doesn't support --json explicitly yet, we just print text. - // But if we ever add it, we have the structure. - for (const res of skeletonResults) { - console.log(res.skeleton); - console.log(""); // Separator - } -} - export const search: Command = new CommanderCommand("search") .description("File pattern searcher") .option( @@ -383,11 +291,6 @@ export const search: Command = new CommanderCommand("search") "Show what would be indexed without actually indexing", false, ) - .option( - "--skeleton", - "Show code skeleton for matching files instead of snippets", - false, - ) .argument("<pattern>", "The pattern to search for") .argument("[path]", "The path to search in") .allowUnknownOption(true) @@ -403,7 +306,6 @@ export const search: Command = new CommanderCommand("search") plain: boolean; sync: boolean; dryRun: boolean; - skeleton: boolean; } = cmd.optsWithGlobals(); if (exec_path?.startsWith("--")) { @@ -444,22 +346,6 @@ export const search: Command = new CommanderCommand("search") (r) => typeof r.score !== "number" || r.score >= minScore, ); - if (options.skeleton) { - await outputSkeletons( - filteredData, - projectRootForServer, - parseInt(options.m, 10), - // Server doesn't easily expose DB instance here in HTTP client mode, - // but we are in client. Wait, this text implies "Server Search" block. - // Client talks to server. The server returns JSON. - // We don't have DB access here. - // So we pass null, and it will fallback to generating local skeleton (if file exists locally). - // This is acceptable for Phase 3. - null, - ); - return; - } - const compactHits = options.compact ? toCompactHits(filteredData) : []; @@ -612,16 +498,6 @@ export const search: Command = new CommanderCommand("search") (r) => typeof r.score !== "number" || r.score >= minScore, ); - if (options.skeleton) { - await outputSkeletons( - filteredData, - projectRoot, - parseInt(options.m, 10), - vectorDb, - ); - return; - } - const compactHits = options.compact ? toCompactHits(filteredData) : []; const compactText = options.compact && compactHits.length diff --git a/src/commands/serve.ts b/src/commands/serve.ts index f24eb166..412976a2 100644 --- a/src/commands/serve.ts +++ b/src/commands/serve.ts @@ -175,7 +175,6 @@ export const serve = new Command("serve") { rerank: true }, undefined, searchPath, - undefined, // intent ac.signal, ); diff --git a/src/commands/setup.ts b/src/commands/setup.ts index eebb528f..13119e8a 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -1,8 +1,8 @@ import * as fs from "node:fs"; -import * as path from "node:path"; import { Command } from "commander"; -import { MODEL_IDS, PATHS } from "../config"; +import { PATHS } from "../config"; import { ensureGrammars } from "../lib/index/grammar-loader"; +import { initNative } from "../lib/native"; import { ensureSetup } from "../lib/setup/setup-helpers"; import { gracefulExit } from "../lib/utils/exit"; @@ -21,8 +21,6 @@ export const setup = new Command("setup") // Show final status console.log("\nSetup Complete!\n"); - const modelIds = [MODEL_IDS.embed, MODEL_IDS.colbert]; - const checkDir = (name: string, p: string) => { const exists = fs.existsSync(p); const symbol = exists ? "✓" : "✗"; @@ -30,54 +28,16 @@ export const setup = new Command("setup") }; checkDir("Global Root", PATHS.globalRoot); - checkDir("Models", PATHS.models); checkDir("Grammars", PATHS.grammars); // Download Grammars console.log("\nChecking Tree-sitter Grammars..."); await ensureGrammars(); - const modelStatuses = modelIds.map((id) => { - const modelPath = path.join(PATHS.models, ...id.split("/")); - return { id, path: modelPath, exists: fs.existsSync(modelPath) }; - }); - - modelStatuses.forEach(({ id, exists }) => { - const symbol = exists ? "✓" : "✗"; - console.log(`${symbol} Model: ${id}`); - }); - - // Check for skiplist.json and try to download if missing - const colbertPath = path.join( - PATHS.models, - ...MODEL_IDS.colbert.split("/"), - ); - const skiplistPath = path.join(colbertPath, "skiplist.json"); - if (fs.existsSync(skiplistPath)) { - console.log(`✓ Skiplist found: ${skiplistPath}`); - } else { - console.log(`⚠ Skiplist missing, attempting to download...`); - try { - const url = `https://huggingface.co/${MODEL_IDS.colbert}/resolve/main/skiplist.json`; - const response = await fetch(url); - if (response.ok) { - const buffer = await response.arrayBuffer(); - fs.writeFileSync(skiplistPath, Buffer.from(buffer)); - console.log(`✓ Skiplist downloaded successfully`); - } else { - console.log( - `⚠ Skiplist download failed (HTTP ${response.status}), will use fallback`, - ); - console.log(` Expected at: ${skiplistPath}`); - } - } catch (error) { - console.log(`⚠ Skiplist download failed, will use fallback`); - console.log( - ` Error: ${error instanceof Error ? error.message : String(error)}`, - ); - console.log(` Expected at: ${skiplistPath}`); - } - } + // Pre-warm native models (downloads via HuggingFace Hub cache on first run) + console.log("\nInitializing native models..."); + await initNative(); + console.log("✓ Native models ready"); console.log(`\nosgrep is ready! You can now run:`); console.log(` osgrep index # Index your repository`); diff --git a/src/commands/skeleton.ts b/src/commands/skeleton.ts deleted file mode 100644 index b24e9bbb..00000000 --- a/src/commands/skeleton.ts +++ /dev/null @@ -1,321 +0,0 @@ -/** - * osgrep skeleton - Show code skeleton (signatures without implementation) - * - * Usage: - * osgrep skeleton <file> # Skeleton of a file - * osgrep skeleton <symbol> # Find symbol and skeleton its file - * osgrep skeleton "query" # Search and skeleton top results - */ - -import * as fs from "node:fs"; -import * as path from "node:path"; -import { Command } from "commander"; -import { createIndexingSpinner } from "../lib/index/sync-helpers"; -import { initialSync } from "../lib/index/syncer"; -import { Searcher } from "../lib/search/searcher"; -import { ensureSetup } from "../lib/setup/setup-helpers"; -import { getStoredSkeleton } from "../lib/skeleton/retriever"; -import { Skeletonizer } from "../lib/skeleton/skeletonizer"; -import { VectorDB } from "../lib/store/vector-db"; -import { gracefulExit } from "../lib/utils/exit"; -import { ensureProjectPaths, findProjectRoot } from "../lib/utils/project-root"; - -interface SkeletonOptions { - limit: string; - json: boolean; - noSummary: boolean; - sync: boolean; -} - -/** - * Check if target looks like a file path. - */ -function isFilePath(target: string): boolean { - // Has path separator or file extension - return ( - target.includes("/") || target.includes("\\") || /\.\w{1,10}$/.test(target) - ); -} - -/** - * Check if target looks like a symbol name (PascalCase or camelCase identifier). - */ -function isSymbolLike(target: string): boolean { - // PascalCase class name or camelCase function name - // Must be a single word without spaces - return /^[A-Za-z_][A-Za-z0-9_]*$/.test(target) && !target.includes(" "); -} - -/** - * Find a file by symbol name in the index. - */ -async function findFileBySymbol( - symbol: string, - db: VectorDB, -): Promise<string | null> { - try { - const table = await db.ensureTable(); - - // Search for files that define this symbol - const results = await table.search(symbol).limit(10).toArray(); - - // Find a result where this symbol is defined - for (const result of results) { - const defined = result.defined_symbols as string[] | undefined; - if (defined?.includes(symbol)) { - return result.path as string; - } - } - - // Fallback: just return the first match's file - if (results.length > 0) { - return results[0].path as string; - } - - return null; - } catch { - return null; - } -} - -export const skeleton = new Command("skeleton") - .description("Show code skeleton (signatures without implementation)") - .argument("<target>", "File path, symbol name, or search query") - .option("-l, --limit <n>", "Max files for query mode", "3") - .option("--json", "Output as JSON", false) - .option("--no-summary", "Omit call/complexity summary in bodies", false) - .option("-s, --sync", "Sync index before searching", false) - .action(async (target: string, options: SkeletonOptions, _cmd) => { - let vectorDb: VectorDB | null = null; - - try { - // Initialize - await ensureSetup(); - const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd(); - const paths = ensureProjectPaths(projectRoot); - vectorDb = new VectorDB(paths.lancedbDir); - - // Sync if requested - if (options.sync) { - const { spinner, onProgress } = createIndexingSpinner( - projectRoot, - "Syncing...", - { verbose: false }, - ); - await initialSync({ projectRoot, onProgress }); - spinner.succeed("Sync complete"); - } - - // Initialize skeletonizer - const skeletonizer = new Skeletonizer(); - await skeletonizer.init(); - - const skeletonOpts = { - includeSummary: !options.noSummary, - }; - - // Determine mode based on target - if (isFilePath(target)) { - // === FILE MODE === - const filePath = path.resolve(target); - - if (!fs.existsSync(filePath)) { - console.error(`File not found: ${filePath}`); - process.exitCode = 1; - return; - } - - if (vectorDb) { - const relativeToProject = path.relative(projectRoot, filePath); - const cached = await getStoredSkeleton(vectorDb, relativeToProject); - if (cached) { - outputResult( - { - success: true, - skeleton: cached, - tokenEstimate: Math.ceil(cached.length / 4), - }, - options, - ); - return; - } - } - - const content = fs.readFileSync(filePath, "utf-8"); - const result = await skeletonizer.skeletonizeFile( - filePath, - content, - skeletonOpts, - ); - - outputResult(result, options); - } else if (isSymbolLike(target) && !target.includes(" ")) { - // === SYMBOL MODE === - const filePath = await findFileBySymbol(target, vectorDb); - - if (!filePath) { - console.error(`Symbol not found in index: ${target}`); - console.error( - "Try running 'osgrep index' first or use a search query.", - ); - process.exitCode = 1; - return; - } - - const absolutePath = path.resolve(projectRoot, filePath); - if (!fs.existsSync(absolutePath)) { - console.error(`File not found: ${absolutePath}`); - process.exitCode = 1; - return; - } - - const cached = await getStoredSkeleton(vectorDb!, filePath); - if (cached) { - outputResult( - { - success: true, - skeleton: cached, - tokenEstimate: Math.ceil(cached.length / 4), - }, - options, - ); - return; - } - - const content = fs.readFileSync(absolutePath, "utf-8"); - const result = await skeletonizer.skeletonizeFile( - filePath, - content, - skeletonOpts, - ); - - outputResult(result, options); - } else { - // === QUERY MODE === - const searcher = new Searcher(vectorDb); - const limit = Math.min(Number.parseInt(options.limit, 10) || 3, 10); - - const searchResults = await searcher.search(target, limit); - - if (!searchResults.data || searchResults.data.length === 0) { - console.error(`No results found for: ${target}`); - process.exitCode = 1; - return; - } - - // Get unique file paths from results - const seenPaths = new Set<string>(); - const filePaths: string[] = []; - - for (const result of searchResults.data) { - const resultPath = (result.metadata as { path?: string })?.path; - if (resultPath && !seenPaths.has(resultPath)) { - seenPaths.add(resultPath); - filePaths.push(resultPath); - if (filePaths.length >= limit) break; - } - } - - // Skeletonize each file - const results: Array<{ - file: string; - skeleton: string; - tokens: number; - error?: string; - }> = []; - - for (const filePath of filePaths) { - const absolutePath = path.resolve(projectRoot, filePath); - - if (!fs.existsSync(absolutePath)) { - results.push({ - file: filePath, - skeleton: `// File not found: ${filePath}`, - tokens: 0, - error: "File not found", - }); - continue; - } - - // Try cache first - const cached = await getStoredSkeleton(vectorDb!, filePath); - if (cached) { - results.push({ - file: filePath, - skeleton: cached, - tokens: Math.ceil(cached.length / 4), - }); - continue; - } - - const content = fs.readFileSync(absolutePath, "utf-8"); - const result = await skeletonizer.skeletonizeFile( - filePath, - content, - skeletonOpts, - ); - - results.push({ - file: filePath, - skeleton: result.skeleton, - tokens: result.tokenEstimate, - error: result.error, - }); - } - - // Output results - if (options.json) { - console.log(JSON.stringify(results, null, 2)); - } else { - for (const result of results) { - console.log(result.skeleton); - console.log(""); // Blank line between files - } - } - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.error("Error:", message); - process.exitCode = 1; - } finally { - if (vectorDb) { - try { - await vectorDb.close(); - } catch { - // Ignore close errors - } - } - const code = typeof process.exitCode === "number" ? process.exitCode : 0; - await gracefulExit(code); - } - }); - -/** - * Output a skeleton result. - */ -function outputResult( - result: { - success: boolean; - skeleton: string; - tokenEstimate: number; - error?: string; - }, - options: SkeletonOptions, -): void { - if (options.json) { - console.log( - JSON.stringify( - { - success: result.success, - skeleton: result.skeleton, - tokens: result.tokenEstimate, - error: result.error, - }, - null, - 2, - ), - ); - } else { - console.log(result.skeleton); - } -} diff --git a/src/commands/trace.ts b/src/commands/trace.ts deleted file mode 100644 index 7a52032d..00000000 --- a/src/commands/trace.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Command } from "commander"; -import { GraphBuilder } from "../lib/graph/graph-builder"; -import { formatTrace } from "../lib/output/formatter"; -import { VectorDB } from "../lib/store/vector-db"; -import { gracefulExit } from "../lib/utils/exit"; -import { ensureProjectPaths, findProjectRoot } from "../lib/utils/project-root"; - -export const trace = new Command("trace") - .description("Trace the call graph for a symbol") - .argument("<symbol>", "The symbol to trace") - .action(async (symbol) => { - const root = process.cwd(); - let vectorDb: VectorDB | null = null; - - try { - const projectRoot = findProjectRoot(root) ?? root; - const paths = ensureProjectPaths(projectRoot); - - vectorDb = new VectorDB(paths.lancedbDir); - - const graphBuilder = new GraphBuilder(vectorDb); - const graph = await graphBuilder.buildGraph(symbol); - console.log(formatTrace(graph)); - } catch (error) { - const message = error instanceof Error ? error.message : "Unknown error"; - console.error("Trace failed:", message); - process.exitCode = 1; - } finally { - if (vectorDb) { - try { - await vectorDb.close(); - } catch (err) { - console.error("Failed to close VectorDB:", err); - } - } - await gracefulExit(); - } - }); diff --git a/src/commands/verify.ts b/src/commands/verify.ts deleted file mode 100644 index 9484fa70..00000000 --- a/src/commands/verify.ts +++ /dev/null @@ -1,169 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import { AutoTokenizer } from "@huggingface/transformers"; -import * as ort from "onnxruntime-node"; - -// CONFIGURATION -const MODEL_DIR = path.resolve("./osgrep-models/colbert"); // Adjust if your path differs -const MODEL_PATH = path.join(MODEL_DIR, "model.onnx"); -const SKIPLIST_PATH = path.join(MODEL_DIR, "skiplist.json"); - -async function main() { - console.log("🔍 Starting ColBERT Integrity Check...\n"); - - // --- CHECK 1: FILES EXIST --- - if (!fs.existsSync(MODEL_PATH)) - throw new Error(`Missing model at ${MODEL_PATH}`); - if (!fs.existsSync(SKIPLIST_PATH)) - throw new Error(`Missing skiplist at ${SKIPLIST_PATH}`); - console.log("✅ Files found."); - - // --- CHECK 2: TOKENIZER & MARKERS --- - console.log("⏳ Loading Tokenizer..."); - const tokenizer = await AutoTokenizer.from_pretrained(MODEL_DIR); - - const queryText = "function test(a, b)"; - // We manually add the [Q] marker to simulate what the worker does - // Note: We use the ID we know works from your export: 50368 - // But let's see if the tokenizer resolves "[Q] " correctly. - - const encoded = await tokenizer(queryText, { add_special_tokens: false }); - const inputIds = encoded.input_ids; // BigInt64Array in newer transformers versions - - // Convert to standard array for inspection - const ids = Array.from(inputIds).map(Number); - - // Mixedbread expects: [CLS] [Q] ...tokens... [SEP] - // Let's verify we can construct that. - const Q_ID = 50368; - const CLS_ID = tokenizer.model.tokens_to_ids.get("[CLS]") ?? 50281; // Fallback to standard if null - - console.log(`\n--- Tokenizer Check ---`); - console.log(`Query: "${queryText}"`); - console.log(`Raw IDs:`, ids); - - // Check if tokenizer recognizes the special tokens by text - const qCheck = tokenizer.model.tokens_to_ids.get("[Q] "); - const dCheck = tokenizer.model.tokens_to_ids.get("[D] "); - - if (qCheck === 50368 && dCheck === 50369) { - console.log(`✅ Tokenizer Map Correct: [Q] -> ${qCheck}, [D] -> ${dCheck}`); - } else { - console.error( - `❌ Tokenizer Map Mismatch! Found [Q]->${qCheck}, [D]->${dCheck}`, - ); - console.error(` Expected 50368 and 50369.`); - } - - // --- CHECK 3: SKIPLIST --- - const skiplist = new Set(JSON.parse(fs.readFileSync(SKIPLIST_PATH, "utf-8"))); - console.log(`\n--- Skiplist Check ---`); - console.log(`Skiplist size: ${skiplist.size}`); - - // Check common punctuation - const commaId = tokenizer.model.tokens_to_ids.get(","); - const dotId = tokenizer.model.tokens_to_ids.get("."); - - if (skiplist.has(commaId) && skiplist.has(dotId)) { - console.log( - `✅ Skiplist contains punctuation ('.'=${dotId}, ','=${commaId})`, - ); - } else { - console.error(`❌ Skiplist missing basic punctuation!`); - } - - // --- CHECK 4: ONNX INFERENCE --- - console.log(`\n--- ONNX Inference Check ---`); - const session = await ort.InferenceSession.create(MODEL_PATH); - console.log(`Session loaded. Input names: ${session.inputNames}`); - - // Construct a dummy batch: [CLS] [Q] test [SEP] - const batchIds = [ - BigInt(CLS_ID), - BigInt(Q_ID), - BigInt(1234), - BigInt(tokenizer.sep_token_id ?? 50282), - ]; - const tensorIds = new ort.Tensor( - "int64", - new BigInt64Array(batchIds), - [1, 4], - ); - const tensorMask = new ort.Tensor( - "int64", - new BigInt64Array([BigInt(1), BigInt(1), BigInt(1), BigInt(1)]), - [1, 4], - ); - - const start = performance.now(); - const feeds = { input_ids: tensorIds, attention_mask: tensorMask }; - const results = await session.run(feeds); - const end = performance.now(); - - const outputName = session.outputNames[0]; - const embeddings = results[outputName]; - - // Dims should be [1, 4, 48] - const dims = embeddings.dims; - console.log(`Output Dimensions: [${dims.join(", ")}]`); - console.log(`Inference Time (cold): ${(end - start).toFixed(2)}ms`); - - if (dims[2] !== 48) { - console.error(`❌ CRITICAL: Expected dimension 48, got ${dims[2]}`); - process.exit(1); - } else { - console.log(`✅ Correct dimension (48d) detected.`); - } - - // --- CHECK 5: MAXSIM PERFORMANCE SIMULATION --- - console.log(`\n--- MaxSim Logic Benchmark ---`); - - // Create dummy vectors for a fake document (1000 tokens) - const docLen = 1000; - const docIds = new Array(docLen) - .fill(0) - .map(() => Math.floor(Math.random() * 50000)); - - // Inject some punctuation into the dummy document to simulate real text - // Let's say 15% of the doc is punctuation - let punctuationCount = 0; - for (let i = 0; i < docLen; i++) { - if (Math.random() < 0.15) { - docIds[i] = commaId ?? 0; // Force a comma - punctuationCount++; - } - } - - const qLen = 32; - - // Naive Dot Product count - const naiveOps = qLen * docLen; - - // Skiplist Dot Product count - let optimizedOps = 0; - for (let i = 0; i < qLen; i++) { - for (let j = 0; j < docLen; j++) { - if (!skiplist.has(docIds[j])) { - optimizedOps++; - } - } - } - - console.log(`Document Length: ${docLen} tokens`); - console.log( - `Punctuation/Skip tokens: ${punctuationCount} (~${((punctuationCount / docLen) * 100).toFixed(1)}%)`, - ); - console.log(`Naive Operations: ${naiveOps}`); - console.log(`Skiplist Operations: ${optimizedOps}`); - console.log(`Savings: ${naiveOps - optimizedOps} operations avoided`); - console.log( - `⚡ Speedup: ${(naiveOps / optimizedOps).toFixed(2)}x (theoretical)`, - ); - - console.log("\n✅ VERIFICATION COMPLETE. MODEL IS GOOD TO GO."); -} - -main().catch((err) => { - console.error("\n❌ TEST FAILED:", err); - process.exit(1); -}); diff --git a/src/config.ts b/src/config.ts index 65518218..96404bcd 100644 --- a/src/config.ts +++ b/src/config.ts @@ -25,27 +25,6 @@ export const CONFIG = { QUERY_PREFIX: "", }; -export const WORKER_TIMEOUT_MS = Number.parseInt( - process.env.OSGREP_WORKER_TIMEOUT_MS || "60000", - 10, -); - -export const WORKER_BOOT_TIMEOUT_MS = Number.parseInt( - process.env.OSGREP_WORKER_BOOT_TIMEOUT_MS || "300000", - 10, -); - -export const MAX_WORKER_MEMORY_MB = Number.parseInt( - process.env.OSGREP_MAX_WORKER_MEMORY_MB || - String( - Math.max( - 2048, - Math.floor((os.totalmem() / 1024 / 1024) * 0.5), // 50% of system RAM - ), - ), - 10, -); - const HOME = os.homedir(); const GLOBAL_ROOT = path.join(HOME, ".osgrep"); diff --git a/src/index.ts b/src/index.ts index 8387883c..ca94ba29 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,9 +13,7 @@ import { installOpencode, uninstallOpencode } from "./commands/opencode"; import { search } from "./commands/search"; import { serve } from "./commands/serve"; import { setup } from "./commands/setup"; -import { skeleton } from "./commands/skeleton"; import { symbols } from "./commands/symbols"; -import { trace } from "./commands/trace"; program .version( @@ -48,9 +46,7 @@ if (isIndexCommand && fs.existsSync(legacyDataPath)) { program.addCommand(search, { isDefault: true }); program.addCommand(index); program.addCommand(list); -program.addCommand(skeleton); program.addCommand(symbols); -program.addCommand(trace); program.addCommand(setup); program.addCommand(serve); program.addCommand(mcp); diff --git a/src/lib/graph/graph-builder.ts b/src/lib/graph/graph-builder.ts deleted file mode 100644 index a715feb2..00000000 --- a/src/lib/graph/graph-builder.ts +++ /dev/null @@ -1,129 +0,0 @@ -import type { VectorRecord } from "../store/types"; -import type { VectorDB } from "../store/vector-db"; -import { escapeSqlString } from "../utils/filter-builder"; - -export interface GraphNode { - symbol: string; - file: string; - line: number; - role: string; - calls: string[]; - calledBy: string[]; - complexity?: number; -} - -export class GraphBuilder { - constructor(private db: VectorDB) {} - - /** - * Find all chunks that call the given symbol. - */ - async getCallers(symbol: string): Promise<GraphNode[]> { - const table = await this.db.ensureTable(); - const escaped = escapeSqlString(symbol); - - // Find chunks where referenced_symbols contains the symbol - const rows = await table - .query() - .where(`array_contains(referenced_symbols, '${escaped}')`) - .limit(100) - .toArray(); - - return rows.map((row) => - this.mapRowToNode(row as unknown as VectorRecord, symbol, "caller"), - ); - } - - /** - * Find what the given symbol calls. - * First finds the definition of the symbol, then returns its referenced_symbols. - */ - async getCallees(symbol: string): Promise<string[]> { - const table = await this.db.ensureTable(); - const escaped = escapeSqlString(symbol); - - // Find the definition of the symbol - const rows = await table - .query() - .where(`array_contains(defined_symbols, '${escaped}')`) - .limit(1) - .toArray(); - - if (rows.length === 0) return []; - - const record = rows[0] as unknown as VectorRecord; - return record.referenced_symbols || []; - } - - /** - * Build a 1-hop graph around a symbol. - */ - async buildGraph(symbol: string): Promise<{ - center: GraphNode | null; - callers: GraphNode[]; - callees: string[]; - }> { - const table = await this.db.ensureTable(); - const escaped = escapeSqlString(symbol); - - // 1. Get Center (Definition) - const centerRows = await table - .query() - .where(`array_contains(defined_symbols, '${escaped}')`) - .limit(1) - .toArray(); - - const center = - centerRows.length > 0 - ? this.mapRowToNode( - centerRows[0] as unknown as VectorRecord, - symbol, - "center", - ) - : null; - - // 2. Get Callers - const callers = await this.getCallers(symbol); - - // 3. Get Callees (from center) - const callees = center ? center.calls : []; - - return { center, callers, callees }; - } - - private mapRowToNode( - row: VectorRecord, - targetSymbol: string, - type: "center" | "caller", - ): GraphNode { - // Helper to convert Arrow Vector to array if needed - const toArray = (val: any): string[] => { - if (val && typeof val.toArray === "function") { - return val.toArray(); - } - return Array.isArray(val) ? val : []; - }; - - const definedSymbols = toArray(row.defined_symbols); - const referencedSymbols = toArray(row.referenced_symbols); - - // If it's a caller, the symbol of interest is the one DOING the calling. - // We try to find the defined symbol in this chunk that is responsible for the call. - // If multiple are defined, we pick the first one or the parent_symbol. - - let symbol = definedSymbols[0] || row.parent_symbol || "unknown"; - if (type === "center") { - symbol = targetSymbol; - } - - return { - symbol, - file: row.path, - line: row.start_line, - role: row.role || "IMPLEMENTATION", - calls: referencedSymbols, - calledBy: [], // To be filled if we do reverse lookup - complexity: row.complexity, - }; - } -} diff --git a/src/lib/index/syncer.ts b/src/lib/index/syncer.ts index dc540874..0726d6f2 100644 --- a/src/lib/index/syncer.ts +++ b/src/lib/index/syncer.ts @@ -7,8 +7,7 @@ import { VectorDB } from "../store/vector-db"; import { isIndexableFile } from "../utils/file-utils"; import { acquireWriterLockWithRetry, type LockHandle } from "../utils/lock"; import { ensureProjectPaths } from "../utils/project-root"; -import { getWorkerPool } from "../workers/pool"; -import type { ProcessFileResult } from "../workers/worker"; +import { processFile, type ProcessFileResult } from "../workers/orchestrator"; import type { InitialSyncProgress, InitialSyncResult } from "./sync-helpers"; import { walk } from "./walker"; @@ -119,7 +118,6 @@ export async function initialSync( let total = 0; onProgress?.({ processed: 0, indexed: 0, total, filePath: "Scanning..." }); - const pool = getWorkerPool(); const cachedPaths = dryRun || treatAsEmptyCache ? new Set<string>() @@ -187,28 +185,15 @@ export async function initialSync( await flushLock; }; - const isTimeoutError = (err: unknown) => - err instanceof Error && err.message?.toLowerCase().includes("timed out"); - const processFileWithRetry = async ( relPath: string, absPath: string, ): Promise<ProcessFileResult> => { - let retries = 0; - while (true) { - try { - return await pool.processFile({ - path: relPath, - absolutePath: absPath, - }); - } catch (err) { - if (isTimeoutError(err) && retries === 0) { - retries += 1; - continue; - } - throw err; - } - } + // No retries needed - native embedding is stable + return await processFile({ + path: relPath, + absolutePath: absPath, + }); }; const schedule = async (task: () => Promise<void>) => { diff --git a/src/lib/native/index.ts b/src/lib/native/index.ts new file mode 100644 index 00000000..e007f230 --- /dev/null +++ b/src/lib/native/index.ts @@ -0,0 +1,238 @@ +/** + * Native bindings to osgrep-core (Rust) + * + * This module provides the bridge to the native Rust code for: + * - Dense embeddings (384-dim, granite-30m) + * - ColBERT embeddings (48-dim per token) + * - ColBERT reranking + * + * All ML inference happens in Rust via ONNX Runtime for maximum speed. + */ + +import { CONFIG, MODEL_IDS } from "../../config"; + +// Try to load native binding +let native: typeof import("osgrep-core") | null = null; +let initPromise: Promise<void> | null = null; +let initialized = false; + +async function loadNative() { + if (native) return native; + + try { + native = await import("osgrep-core"); + } catch (e) { + throw new Error( + `Failed to load osgrep-core native binding. Run 'npm run build:release' in osgrep_v2/: ${e}` + ); + } + + return native; +} + +/** + * Initialize native models. Call once at startup. + */ +export async function initNative(): Promise<void> { + if (initialized) return; + if (initPromise) return initPromise; + + initPromise = (async () => { + const n = await loadNative(); + + if (!n.isInitialized()) { + n.initModels(MODEL_IDS.embed, MODEL_IDS.colbert); + } + + initialized = true; + })(); + + await initPromise; + initPromise = null; +} + +/** + * Check if native models are initialized + */ +export function isNativeInitialized(): boolean { + return initialized && native?.isInitialized() === true; +} + +// ============================================================================= +// Dense Embeddings +// ============================================================================= + +export interface DenseEmbedding { + vector: Float32Array; +} + +/** + * Embed texts to dense vectors (384-dim, L2-normalized) + */ +export async function embedDense(texts: string[]): Promise<Float32Array[]> { + await initNative(); + const n = await loadNative(); + + const result = n.embedDense(texts); + const dim = CONFIG.VECTOR_DIM; + + // Split flat array into per-text vectors + const vectors: Float32Array[] = []; + for (let i = 0; i < result.count; i++) { + const start = i * dim; + const vec = new Float32Array(dim); + for (let j = 0; j < dim; j++) { + vec[j] = result.embeddings[start + j]; + } + vectors.push(vec); + } + + return vectors; +} + +// ============================================================================= +// ColBERT Embeddings (for indexing) +// ============================================================================= + +export interface ColbertPacked { + /** Quantized embeddings as Int8Array */ + embeddings: Int8Array; + /** Number of tokens per document */ + lengths: Uint32Array; + /** Byte offsets for each document */ + offsets: Uint32Array; +} + +/** + * Embed texts to ColBERT format (48-dim per token, INT8 quantized) + * Use at INDEX TIME to pre-compute embeddings + */ +export async function embedColbert(texts: string[]): Promise<ColbertPacked> { + await initNative(); + const n = await loadNative(); + + const result = n.embedColbertPacked(texts); + + return { + embeddings: new Int8Array(result.embeddings), + lengths: new Uint32Array(result.lengths), + offsets: new Uint32Array(result.offsets), + }; +} + +// ============================================================================= +// Combined Embedding (for indexing) +// ============================================================================= + +export interface HybridEmbedding { + dense: Float32Array; + colbert: Int8Array; + colbertLength: number; + colbertOffset: number; +} + +/** + * Embed texts for indexing (both dense and ColBERT in one call) + * Returns per-text embeddings ready for storage + */ +export async function embedBatch(texts: string[]): Promise<HybridEmbedding[]> { + await initNative(); + const n = await loadNative(); + + const result = n.embedBatch(texts); + const dim = CONFIG.VECTOR_DIM; + const colbertDim = CONFIG.COLBERT_DIM; + + const embeddings: HybridEmbedding[] = []; + + for (let i = 0; i < texts.length; i++) { + // Extract dense vector + const denseStart = i * dim; + const dense = new Float32Array(dim); + for (let j = 0; j < dim; j++) { + dense[j] = result.dense[denseStart + j]; + } + + // Extract ColBERT embedding for this doc + const colbertOffset = result.colbertOffsets[i]; + const colbertLength = result.colbertLengths[i]; + const colbertSize = colbertLength * colbertDim; + const colbert = new Int8Array(colbertSize); + for (let j = 0; j < colbertSize; j++) { + colbert[j] = result.colbertEmbeddings[colbertOffset + j]; + } + + embeddings.push({ + dense, + colbert, + colbertLength, + colbertOffset: 0, // Will be set when storing + }); + } + + return embeddings; +} + +// ============================================================================= +// ColBERT Query Encoding +// ============================================================================= + +/** + * Encode query for ColBERT reranking + * Returns query embedding matrix as Float32Array + */ +export async function encodeQueryColbert(query: string): Promise<Float32Array> { + await initNative(); + const n = await loadNative(); + + const result = n.encodeQueryColbert(query); + return new Float32Array(result); +} + +// ============================================================================= +// ColBERT Reranking +// ============================================================================= + +export interface RerankInput { + /** Query ColBERT embedding from encodeQueryColbert */ + queryEmbedding: Float32Array; + /** Packed ColBERT doc embeddings (INT8) */ + docEmbeddings: Int8Array; + /** Token counts per doc */ + docLengths: number[]; + /** Byte offsets per doc */ + docOffsets: number[]; + /** Which doc indices to rerank */ + candidateIndices: number[]; + /** How many to return */ + topK: number; +} + +export interface RerankResult { + /** Original indices of top-k docs */ + indices: number[]; + /** MaxSim scores */ + scores: number[]; +} + +/** + * Rerank documents using pre-indexed ColBERT embeddings + */ +export async function rerankColbert(input: RerankInput): Promise<RerankResult> { + await initNative(); + const n = await loadNative(); + + const result = n.rerankColbert( + Array.from(input.queryEmbedding), + Array.from(input.docEmbeddings), + input.docLengths, + input.docOffsets, + input.candidateIndices, + input.topK + ); + + return { + indices: Array.from(result.indices), + scores: Array.from(result.scores), + }; +} diff --git a/src/lib/output/formatter.ts b/src/lib/output/formatter.ts index 105e2b75..09f78750 100644 --- a/src/lib/output/formatter.ts +++ b/src/lib/output/formatter.ts @@ -116,51 +116,3 @@ export function formatResults( if (results.length === 0) return "No results found."; return results.map((r) => formatResult(r, root, options)).join("\n\n"); } - -import type { GraphNode } from "../graph/graph-builder"; - -export function formatTrace(graph: { - center: GraphNode | null; - callers: GraphNode[]; - callees: string[]; -}): string { - if (!graph.center) { - return style.dim("Symbol not found."); - } - - const lines: string[] = []; - - // 1. Callers (Upstream) - if (graph.callers.length > 0) { - lines.push(style.bold("Callers (Who calls this?):")); - graph.callers.forEach((caller) => { - lines.push( - ` ${style.blue("↑")} ${style.green(caller.symbol)} ${style.dim(`(${caller.file}:${caller.line})`)}`, - ); - }); - lines.push(""); - } else { - lines.push(style.dim("No known callers.")); - lines.push(""); - } - - // 2. Center (The Symbol) - lines.push(style.bold("▶ " + graph.center.symbol)); - lines.push( - ` ${style.dim(`Defined in ${graph.center.file}:${graph.center.line}`)}`, - ); - lines.push(` ${style.dim(`Role: ${graph.center.role}`)}`); - lines.push(""); - - // 3. Callees (Downstream) - if (graph.callees.length > 0) { - lines.push(style.bold("Callees (What does this call?):")); - graph.callees.forEach((callee) => { - lines.push(` ${style.cyan("↓")} ${callee}`); - }); - } else { - lines.push(style.dim("No known callees.")); - } - - return lines.join("\n"); -} diff --git a/src/lib/output/json-formatter.ts b/src/lib/output/json-formatter.ts index f9fccdc4..8a5e2dec 100644 --- a/src/lib/output/json-formatter.ts +++ b/src/lib/output/json-formatter.ts @@ -1,4 +1,3 @@ -import type { GraphNode } from "../graph/graph-builder"; import type { ChunkType } from "../store/types"; export interface JsonOutput { @@ -6,11 +5,6 @@ export interface JsonOutput { hits?: unknown[]; tsv?: string; format?: string; - graph?: { - center: GraphNode | null; - callers: GraphNode[]; - callees: string[]; - }; metadata?: { count: number; query?: string; diff --git a/src/lib/search/intent.ts b/src/lib/search/intent.ts deleted file mode 100644 index f12edce6..00000000 --- a/src/lib/search/intent.ts +++ /dev/null @@ -1,34 +0,0 @@ -export interface SearchIntent { - type: "DEFINITION" | "FLOW" | "USAGE" | "ARCHITECTURE" | "GENERAL"; - filters?: { - definitionsOnly?: boolean; - usagesOnly?: boolean; - }; - mode?: "orchestration_first" | "show_examples" | "group_by_role"; -} - -export function detectIntent(query: string): SearchIntent { - const normalized = query.toLowerCase(); - - // Definition queries - if (/where is|what is|define/.test(normalized)) { - return { type: "DEFINITION", filters: { definitionsOnly: true } }; - } - - // Implementation queries - if (/how does|how is|implementation/.test(normalized)) { - return { type: "FLOW", mode: "orchestration_first" }; - } - - // Usage queries - if (/example|how to use|usage/.test(normalized)) { - return { type: "USAGE", mode: "show_examples" }; - } - - // Architecture queries - if (/architecture|system|overview/.test(normalized)) { - return { type: "ARCHITECTURE", mode: "group_by_role" }; - } - - return { type: "GENERAL" }; -} diff --git a/src/lib/search/searcher.ts b/src/lib/search/searcher.ts index 040a4297..1008219e 100644 --- a/src/lib/search/searcher.ts +++ b/src/lib/search/searcher.ts @@ -8,12 +8,17 @@ import type { } from "../store/types"; import type { VectorDB } from "../store/vector-db"; import { escapeSqlString, normalizePath } from "../utils/filter-builder"; -import { getWorkerPool } from "../workers/pool"; -import { detectIntent, type SearchIntent } from "./intent"; +import { encodeQuery, rerank } from "../workers/orchestrator"; export class Searcher { constructor(private db: VectorDB) {} + private static readonly PRE_RERANK_K_MULT = 5; + private static readonly PRE_RERANK_K_MIN = 500; + private static readonly RERANK_CANDIDATES_K = 80; + private static readonly FUSED_WEIGHT = 0.5; + private static readonly MAX_PER_FILE = 3; + private mapRecordToChunk( record: Partial<VectorRecord>, score: number, @@ -135,72 +140,12 @@ export class Searcher { private applyStructureBoost( record: Partial<VectorRecord>, score: number, - intent?: SearchIntent, ): number { let adjusted = score; - // Item 6: Anchors are recall helpers, not rank contenders + // Anchor penalty (anchors are recall helpers, not results) if (record.is_anchor) { - // Minimal penalty to break ties - const anchorPenalty = - Number.parseFloat(process.env.OSGREP_ANCHOR_PENALTY ?? "") || 0.99; - adjusted *= anchorPenalty; - } else { - // Only boost non-anchors - const chunkType = record.chunk_type || ""; - const boosted = [ - "function", - "class", - "method", - "interface", - "type_alias", - ]; - if (boosted.includes(chunkType)) { - let boostFactor = 1.0; - - // Base boost - boostFactor *= 1.1; - - // --- Role Boost --- - if (record.role === "ORCHESTRATION") { - boostFactor *= 1.5; - } else if (record.role === "DEFINITION") { - boostFactor *= 1.2; - } else if (record.role === "IMPLEMENTATION") { - boostFactor *= 1.1; - } - - // --- Complexity/Orchestration Boost (User Requested) --- - const refs = record.referenced_symbols?.length || 0; - - if (refs > 5) { - // Small boost for non-trivial functions - boostFactor *= 1.1; - } - if (refs > 15) { - // Massive boost for Orchestrators - boostFactor *= 1.25; - } - - // Intent-based boosts - if (intent) { - if (intent.type === "DEFINITION" && record.role === "DEFINITION") { - boostFactor *= 1.2; - } - if (intent.type === "FLOW" && record.role === "ORCHESTRATION") { - boostFactor *= 1.4; - } - if (intent.type === "USAGE" && record.role === "IMPLEMENTATION") { - boostFactor *= 1.2; - } - } - - adjusted *= boostFactor; - } - } - - if (record.role === "DOCS") { - adjusted *= 0.6; + adjusted *= 0.99; } const pathStr = (record.path || "").toLowerCase(); @@ -211,25 +156,15 @@ export class Searcher { /\.(test|spec)\.[cm]?[jt]sx?$/i.test(pathStr); if (isTestPath) { - const testPenalty = - Number.parseFloat(process.env.OSGREP_TEST_PENALTY ?? "") || 0.5; - adjusted *= testPenalty; + adjusted *= 0.5; } if ( pathStr.endsWith(".md") || - pathStr.endsWith(".mdx") || - pathStr.endsWith(".txt") || pathStr.endsWith(".json") || pathStr.endsWith(".lock") || pathStr.includes("/docs/") ) { - const docPenalty = - Number.parseFloat(process.env.OSGREP_DOC_PENALTY ?? "") || 0.6; - adjusted *= docPenalty; // Downweight docs/data - } - // Import-only penalty - if ((record.content || "").length < 50 && !record.is_exported) { - adjusted *= 0.9; + adjusted *= 0.6; } return adjusted; @@ -283,17 +218,13 @@ export class Searcher { async search( query: string, top_k?: number, - _search_options?: { rerank?: boolean }, - _filters?: SearchFilter, + options?: { rerank?: boolean }, + filters?: SearchFilter, pathPrefix?: string, - intent?: SearchIntent, signal?: AbortSignal, ): Promise<SearchResponse> { const finalLimit = top_k ?? 10; - const doRerank = _search_options?.rerank ?? true; - const searchIntent = intent || detectIntent(query); - - const pool = getWorkerPool(); + const doRerank = options?.rerank ?? true; if (signal?.aborted) { const err = new Error("Aborted"); @@ -305,8 +236,7 @@ export class Searcher { dense: queryVector, colbert: queryMatrixRaw, colbertDim, - pooled_colbert_48d: queryPooled, - } = await pool.encodeQuery(query, signal); + } = await encodeQuery(query); if (signal?.aborted) { const err = new Error("Aborted"); @@ -328,43 +258,28 @@ export class Searcher { } // Handle --def (definition) filter - const defFilter = _filters?.def; + const defFilter = filters?.def; if (typeof defFilter === "string" && defFilter) { whereClauseParts.push( `array_contains(defined_symbols, '${escapeSqlString(defFilter)}')`, ); - } else if ( - searchIntent.type === "DEFINITION" && - searchIntent.filters?.definitionsOnly - ) { - // If intent is DEFINITION but no specific symbol provided, filter by role - whereClauseParts.push( - `(role = 'DEFINITION' OR array_length(defined_symbols) > 0)`, - ); } // Handle --ref (reference) filter - const refFilter = _filters?.ref; + const refFilter = filters?.ref; if (typeof refFilter === "string" && refFilter) { whereClauseParts.push( `array_contains(referenced_symbols, '${escapeSqlString(refFilter)}')`, ); - } else if ( - searchIntent.type === "USAGE" && - searchIntent.filters?.usagesOnly - ) { - // If intent is USAGE, we might want to filter out definitions? - // For now, let's just rely on boosting. } const whereClause = whereClauseParts.length > 0 ? whereClauseParts.join(" AND ") : undefined; - const envPreK = Number.parseInt(process.env.OSGREP_PRE_K ?? "", 10); - const PRE_RERANK_K = - Number.isFinite(envPreK) && envPreK > 0 - ? envPreK - : Math.max(finalLimit * 5, 500); + const PRE_RERANK_K = Math.max( + finalLimit * Searcher.PRE_RERANK_K_MULT, + Searcher.PRE_RERANK_K_MIN, + ); let table: Table; try { table = await this.db.ensureTable(); @@ -430,61 +345,14 @@ export class Searcher { .map(([key]) => docMap.get(key)) .filter(Boolean) as VectorRecord[]; - // Item 8: Widen PRE_RERANK_K - // Retrieve a wide set for Stage 1 filtering - const envStage1 = Number.parseInt(process.env.OSGREP_STAGE1_K ?? "", 10); - const STAGE1_K = - Number.isFinite(envStage1) && envStage1 > 0 ? envStage1 : 200; - const topCandidates = fused.slice(0, STAGE1_K); - - // Item 9: Two-stage rerank - // Stage 1: Cheap pooled cosine filter - let stage2Candidates = topCandidates; - const envStage2K = Number.parseInt(process.env.OSGREP_STAGE2_K ?? "", 10); - const STAGE2_K = - Number.isFinite(envStage2K) && envStage2K > 0 ? envStage2K : 40; - - const envRerankTop = Number.parseInt( - process.env.OSGREP_RERANK_TOP ?? "", - 10, - ); - const RERANK_TOP = - Number.isFinite(envRerankTop) && envRerankTop > 0 ? envRerankTop : 20; - const envBlend = Number.parseFloat(process.env.OSGREP_RERANK_BLEND ?? ""); - const FUSED_WEIGHT = - Number.isFinite(envBlend) && envBlend >= 0 ? envBlend : 0.5; - - if (queryPooled && topCandidates.length > STAGE2_K) { - const cosineScores = topCandidates.map((doc) => { - if (!doc.pooled_colbert_48d) return -1; - // Manual cosine sim since we don't have helper here easily - // Assuming vectors are normalized (which they should be from orchestrator) - let dot = 0; - const docVec = doc.pooled_colbert_48d; - for (let i = 0; i < queryPooled.length; i++) { - dot += queryPooled[i] * (docVec[i] || 0); - } - return dot; - }); - - // Sort by cosine score and keep top N - const withScore = topCandidates.map((doc, i) => ({ - doc, - score: cosineScores[i], - })); - withScore.sort((a, b) => b.score - a.score); - stage2Candidates = withScore.slice(0, STAGE2_K).map((x) => x.doc); - } - - if (stage2Candidates.length === 0) { + if (fused.length === 0) { return { data: [] }; } - const rerankCandidates = stage2Candidates.slice(0, RERANK_TOP); + const rerankCandidates = fused.slice(0, Searcher.RERANK_CANDIDATES_K); const scores = doRerank - ? await pool.rerank( - { + ? await rerank({ query: queryMatrixRaw, docs: rerankCandidates.map((doc) => ({ colbert: (doc.colbert as Buffer | Int8Array | number[]) ?? [], @@ -495,9 +363,7 @@ export class Searcher { : undefined, })), colbertDim, - }, - signal, - ) + }) : rerankCandidates.map((doc, idx) => { // If rerank is disabled, fall back to fusion ordering with structural boost const key = doc.id || `${doc.path}:${doc.chunk_index}`; @@ -515,8 +381,8 @@ export class Searcher { const base = scores?.[idx] ?? 0; const key = doc.id || `${doc.path}:${doc.chunk_index}`; const fusedScore = candidateScores.get(key) ?? 0; - const blended = base + FUSED_WEIGHT * fusedScore; - const boosted = this.applyStructureBoost(doc, blended, searchIntent); + const blended = base + Searcher.FUSED_WEIGHT * fusedScore; + const boosted = this.applyStructureBoost(doc, blended); return { record: doc, score: boosted }; }); @@ -529,17 +395,11 @@ export class Searcher { // Item 10: Per-file diversification const seenFiles = new Map<string, number>(); const diversified: ScoredItem[] = []; - const envMaxPerFile = Number.parseInt( - process.env.OSGREP_MAX_PER_FILE ?? "", - 10, - ); - const MAX_PER_FILE = - Number.isFinite(envMaxPerFile) && envMaxPerFile > 0 ? envMaxPerFile : 3; for (const item of uniqueScored) { const path = item.record.path || ""; const count = seenFiles.get(path) || 0; - if (count < MAX_PER_FILE) { + if (count < Searcher.MAX_PER_FILE) { diversified.push(item); seenFiles.set(path, count + 1); } diff --git a/src/lib/setup/model-loader.ts b/src/lib/setup/model-loader.ts deleted file mode 100644 index 2d014fa9..00000000 --- a/src/lib/setup/model-loader.ts +++ /dev/null @@ -1,68 +0,0 @@ -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import { Worker } from "node:worker_threads"; -import { MODEL_IDS } from "../../config"; - -const HOMEDIR = os.homedir(); -const CACHE_DIR = path.join(HOMEDIR, ".osgrep", "models"); -const LOG_MODELS = - process.env.OSGREP_DEBUG_MODELS === "1" || - process.env.OSGREP_DEBUG_MODELS === "true"; - -/** - * Triggers the download of models by spawning a worker thread. - * This prevents the main thread from loading onnxruntime, avoiding exit crashes. - */ -export async function downloadModels(): Promise<void> { - return new Promise((resolve, reject) => { - const tsWorkerPath = path.join(__dirname, "../workers/download-worker.ts"); - const jsWorkerPath = path.join(__dirname, "../workers/download-worker.js"); - const hasTsWorker = fs.existsSync(tsWorkerPath); - const hasJsWorker = fs.existsSync(jsWorkerPath); - const runningTs = path.extname(__filename) === ".ts"; - const isDev = (runningTs && hasTsWorker) || (hasTsWorker && !hasJsWorker); - - const workerPath = isDev ? tsWorkerPath : jsWorkerPath; - const execArgv = isDev ? ["-r", "ts-node/register"] : []; - - const worker = new Worker(workerPath, { execArgv }); - - worker.on("message", (msg) => { - if (msg.type === "progress") { - // Ignore progress messages for now, or log if debug enabled - return; - } - - if (msg.status === "success") { - if (LOG_MODELS) console.log("Worker: Models ready."); - resolve(); - } else if (msg.status === "error") { - reject(new Error(msg.error || "Unknown worker error")); - } - // Ignore other messages - }); - - worker.on("error", (err) => { - reject(err); - }); - - worker.on("exit", (code) => { - if (code !== 0) { - reject(new Error(`Download worker exited with code ${code}`)); - } - }); - }); -} - -/** - * Simple check to see if the cache folder exists for our models. - * This is a loose check for the UI/Doctor command. - */ -export function areModelsDownloaded(): boolean { - // Check if the model directories exist in the cache - const embedPath = path.join(CACHE_DIR, ...MODEL_IDS.embed.split("/")); - const colbertPath = path.join(CACHE_DIR, ...MODEL_IDS.colbert.split("/")); - - return fs.existsSync(embedPath) && fs.existsSync(colbertPath); -} diff --git a/src/lib/setup/setup-helpers.ts b/src/lib/setup/setup-helpers.ts index bbf7ddec..b344d75b 100644 --- a/src/lib/setup/setup-helpers.ts +++ b/src/lib/setup/setup-helpers.ts @@ -1,7 +1,6 @@ import * as fs from "node:fs"; import ora from "ora"; import { PATHS } from "../../config"; -import { areModelsDownloaded, downloadModels } from "./model-loader"; export interface SetupPaths { root: string; @@ -11,7 +10,6 @@ export interface SetupPaths { export interface SetupStatus extends SetupPaths { createdDirs: boolean; - downloadedModels: boolean; } function getPaths(): SetupPaths { @@ -56,22 +54,5 @@ export async function ensureSetup({ throw error; } - const modelsPresent = areModelsDownloaded(); - let downloadedModels = false; - - if (!modelsPresent) { - const modelSpinner = !silent - ? ora("Downloading models (first run)...").start() - : null; - try { - await downloadModels(); - downloadedModels = true; - modelSpinner?.succeed("Models downloaded and ready"); - } catch (error) { - modelSpinner?.fail("Failed to download models"); - throw error; - } - } - - return { ...paths, createdDirs, downloadedModels }; + return { ...paths, createdDirs }; } diff --git a/src/lib/skeleton/body-fields.ts b/src/lib/skeleton/body-fields.ts deleted file mode 100644 index 5c32b33d..00000000 --- a/src/lib/skeleton/body-fields.ts +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Body field mappings for TreeSitter AST nodes. - * - * Maps language -> node type -> body field name - * - string: The field name containing the body to elide - * - null: Node type has no body to elide (e.g., type aliases, interfaces) - * - undefined: Node type not recognized for this language - * - * IMPORTANT: Classes are "containers" - we don't elide their bodies, - * we recurse into them to skeletonize individual methods. - */ - -export const BODY_FIELDS: Record<string, Record<string, string | null>> = { - typescript: { - function_declaration: "body", - method_definition: "body", - arrow_function: "body", - generator_function_declaration: "body", - // Classes are containers - don't elide, recurse into them - class_declaration: null, - interface_declaration: null, // Interfaces have no body to elide - keep as-is - type_alias_declaration: null, // Type aliases are already compact - enum_declaration: null, // Enums are already compact - }, - - tsx: { - // Same as typescript - function_declaration: "body", - method_definition: "body", - arrow_function: "body", - generator_function_declaration: "body", - class_declaration: null, // Container - interface_declaration: null, - type_alias_declaration: null, - enum_declaration: null, - }, - - javascript: { - // Same as typescript (uses tsx grammar) - function_declaration: "body", - method_definition: "body", - arrow_function: "body", - generator_function_declaration: "body", - class_declaration: null, // Container - }, - - python: { - function_definition: "body", - class_definition: null, // Container - recurse into methods - }, - - go: { - function_declaration: "body", - method_declaration: "body", - type_declaration: null, // Type declarations are compact - }, - - rust: { - function_item: "body", - impl_item: null, // Container - recurse into methods - trait_item: null, // Trait definitions show method signatures - keep as-is - struct_item: null, // Struct definitions are compact - enum_item: null, // Enum definitions are compact - mod_item: "body", - }, - - java: { - method_declaration: "body", - constructor_declaration: "body", - class_declaration: null, // Container - interface_declaration: null, - enum_declaration: null, - }, - - c_sharp: { - method_declaration: "body", - constructor_declaration: "body", - class_declaration: null, // Container - interface_declaration: null, - struct_declaration: null, // Container - namespace_declaration: null, - }, - - cpp: { - function_definition: "body", - class_specifier: null, // Container - struct_specifier: null, // Container - namespace_definition: null, // Container - enum_specifier: null, - }, - - c: { - function_definition: "body", - struct_specifier: null, - enum_specifier: null, - }, - - ruby: { - method: "body", - class: null, // Container - module: null, // Container - singleton_method: "body", - }, - - php: { - function_definition: "body", - method_declaration: "body", - class_declaration: null, // Container - interface_declaration: null, - trait_declaration: null, // Container - }, -}; - -/** - * Container types - these hold methods/functions but shouldn't be elided themselves. - * We recurse into them to skeletonize their contents. - */ -export const CONTAINER_TYPES: Record<string, string[]> = { - typescript: ["class_declaration", "class_body"], - tsx: ["class_declaration", "class_body"], - javascript: ["class_declaration", "class_body"], - python: ["class_definition"], - go: [], // Go doesn't have classes - rust: ["impl_item"], - java: ["class_declaration", "class_body"], - c_sharp: ["class_declaration", "struct_declaration", "class_body"], - cpp: ["class_specifier", "struct_specifier"], - c: [], - ruby: ["class", "module"], - php: ["class_declaration", "trait_declaration"], -}; - -/** - * Check if a node type is a container (holds methods). - */ -export function isContainerType(langId: string, nodeType: string): boolean { - return CONTAINER_TYPES[langId]?.includes(nodeType) ?? false; -} - -/** - * Get the body field name for a given language and node type. - * - * @returns string - Field name to access body - * @returns null - Node has no body to elide (keep as-is) - * @returns undefined - Node type not recognized - */ -export function getBodyField( - langId: string, - nodeType: string, -): string | null | undefined { - return BODY_FIELDS[langId]?.[nodeType]; -} - -/** - * Check if a node type has a body that can be elided. - */ -export function hasBodyField(langId: string, nodeType: string): boolean { - const field = getBodyField(langId, nodeType); - return typeof field === "string"; -} - -/** - * Check if a node type should be kept as-is (no elision). - * These are typically type definitions, interfaces, etc. - */ -export function shouldPreserveWhole(langId: string, nodeType: string): boolean { - const field = getBodyField(langId, nodeType); - return field === null; -} diff --git a/src/lib/skeleton/index.ts b/src/lib/skeleton/index.ts deleted file mode 100644 index 8039d518..00000000 --- a/src/lib/skeleton/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Skeleton module - Code compression for AI agents. - * - * Reduces token usage by 80-95% while preserving: - * - Function/method signatures - * - Class declarations - * - Type definitions - * - Summary of what functions do - */ - -export { - BODY_FIELDS, - getBodyField, - hasBodyField, - shouldPreserveWhole, -} from "./body-fields"; -export type { SkeletonOptions, SkeletonResult } from "./skeletonizer"; -export { Skeletonizer, skeletonizeFile } from "./skeletonizer"; -export type { ChunkMetadata, SummaryOptions } from "./summary-formatter"; -export { - formatSkeletonHeader, - formatSummary, - getCommentStyle, -} from "./summary-formatter"; diff --git a/src/lib/skeleton/retriever.ts b/src/lib/skeleton/retriever.ts deleted file mode 100644 index bc9b7651..00000000 --- a/src/lib/skeleton/retriever.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { VectorDB } from "../store/vector-db"; - -export async function getStoredSkeleton( - db: VectorDB, - filePath: string, -): Promise<string | null> { - try { - const table = await db.ensureTable(); - // LanceDB query - const results = await table - .query() - .where(`path = '${filePath.replace(/'/g, "''")}' AND is_anchor = true`) - .limit(1) - .toArray(); - - if (results.length > 0) { - const skel = results[0].file_skeleton; - if (typeof skel === "string" && skel.length > 0) { - return skel; - } - } - return null; - } catch (e) { - // If table doesn't exist or query fails, return null - return null; - } -} diff --git a/src/lib/skeleton/skeletonizer.ts b/src/lib/skeleton/skeletonizer.ts deleted file mode 100644 index d53b8831..00000000 --- a/src/lib/skeleton/skeletonizer.ts +++ /dev/null @@ -1,562 +0,0 @@ -/** - * Skeletonizer - Compress code by replacing function bodies with summaries. - * - * Reduces token usage by 80-95% while preserving: - * - Function/method signatures - * - Class/interface declarations (structure preserved, methods skeletonized) - * - Type definitions - * - Decorators and annotations - * - Inline summaries of what functions do (calls, complexity, role) - */ - -import * as fs from "node:fs"; -import * as path from "node:path"; -import { getLanguageByExtension } from "../core/languages"; -import { GRAMMARS_DIR } from "../index/grammar-loader"; -import { getBodyField } from "./body-fields"; -import { - type ChunkMetadata, - formatSkeletonHeader, - formatSummary, - getCommentStyle, -} from "./summary-formatter"; - -// Import web-tree-sitter (CommonJS) -const TreeSitter = require("web-tree-sitter"); -const Parser = TreeSitter.Parser; -const Language = TreeSitter.Language; - -// TreeSitter types (matching chunker.ts) -interface TreeSitterNode { - type: string; - text: string; - startPosition: { row: number; column: number }; - endPosition: { row: number; column: number }; - startIndex: number; - endIndex: number; - namedChildren?: TreeSitterNode[]; - children?: TreeSitterNode[]; - parent?: TreeSitterNode; - childForFieldName?: (field: string) => TreeSitterNode | null; - previousSibling?: TreeSitterNode | null; -} - -interface TreeSitterParser { - setLanguage(language: TreeSitterLanguage): void; - parse(content: string): { rootNode: TreeSitterNode }; -} - -type TreeSitterLanguage = Record<string, never>; - -// Result types -export interface SkeletonResult { - success: boolean; - skeleton: string; - tokenEstimate: number; - symbolCount: number; - language: string; - error?: string; -} - -export interface SkeletonOptions { - /** Preserve decorators/annotations in output. Default: true */ - preserveDecorators?: boolean; - /** Include summary comment in elided bodies. Default: true */ - includeSummary?: boolean; - /** Max function calls to show in summary. Default: 4 */ - maxCallsInSummary?: number; -} - -/** Represents a region to elide (replace with summary) */ -interface ElisionRegion { - bodyStart: number; - bodyEnd: number; - summary: string; - langId: string; -} - -const DEFAULT_OPTIONS: Required<SkeletonOptions> = { - preserveDecorators: true, - includeSummary: true, - maxCallsInSummary: 4, -}; - -// WASM locator (same as chunker.ts) -function resolveTreeSitterWasmLocator(): string { - try { - return require.resolve("web-tree-sitter/tree-sitter.wasm"); - } catch { - try { - const pkgDir = path.dirname( - require.resolve("web-tree-sitter/package.json"), - ); - const candidate = path.join(pkgDir, "tree-sitter.wasm"); - if (fs.existsSync(candidate)) return candidate; - } catch { - // fall through - } - return path.join( - __dirname, - "..", - "..", - "..", - "node_modules", - "web-tree-sitter", - "tree-sitter.wasm", - ); - } -} - -/** - * Main Skeletonizer class. - */ -export class Skeletonizer { - private parser: TreeSitterParser | null = null; - private languages: Map<string, TreeSitterLanguage | null> = new Map(); - private initialized = false; - - async init(): Promise<void> { - if (this.initialized) return; - - try { - const wasmLocator = resolveTreeSitterWasmLocator(); - await Parser.init({ locator: wasmLocator }); - this.parser = new Parser() as TreeSitterParser; - } catch (_err) { - console.warn("⚠️ TreeSitter unavailable for skeletonization"); - this.parser = null; - } - - if (!fs.existsSync(GRAMMARS_DIR)) { - fs.mkdirSync(GRAMMARS_DIR, { recursive: true }); - } - - this.initialized = true; - } - - /** - * Check if a file can be skeletonized. - */ - isSupported(filePath: string): { - supported: boolean; - language?: string; - reason?: string; - } { - const ext = path.extname(filePath).toLowerCase(); - const langDef = getLanguageByExtension(ext); - - if (!langDef) { - return { - supported: false, - reason: `Unknown file extension: ${ext}`, - }; - } - - if (!langDef.grammar) { - return { - supported: false, - language: langDef.id, - reason: `No TreeSitter grammar for ${langDef.id}`, - }; - } - - return { - supported: true, - language: langDef.id, - }; - } - - /** - * Skeletonize a file. - */ - async skeletonizeFile( - filePath: string, - content: string, - options?: SkeletonOptions, - ): Promise<SkeletonResult> { - if (!this.initialized) await this.init(); - - const opts = { ...DEFAULT_OPTIONS, ...options }; - const support = this.isSupported(filePath); - - // Handle unsupported languages - if (!support.supported) { - return this.createFallbackResult( - filePath, - content, - support.reason || "Unsupported language", - ); - } - - const ext = path.extname(filePath).toLowerCase(); - const langDef = getLanguageByExtension(ext); - if (!langDef?.grammar) { - return this.createFallbackResult( - filePath, - content, - "No grammar available", - ); - } - - // Load language - const language = await this.getLanguage(langDef.grammar.name); - if (!language || !this.parser) { - return this.createFallbackResult( - filePath, - content, - "Failed to load grammar", - ); - } - - try { - // Parse the file - this.parser.setLanguage(language); - const tree = this.parser.parse(content); - const root = tree.rootNode; - - // Find all regions to elide (function/method bodies) - const elisions: ElisionRegion[] = []; - this.findElisionRegions(root, langDef.id, content, elisions, opts); - - if (elisions.length === 0) { - // No functions found - return a compact version - return this.createFallbackResult( - filePath, - content, - "No functions/methods found", - ); - } - - // Sort by position (ascending) for correct reconstruction - elisions.sort((a, b) => a.bodyStart - b.bodyStart); - - // Build skeleton by replacing bodies with summaries - const skeleton = this.buildSkeleton(content, elisions, langDef.id); - const tokenEstimate = Math.ceil(skeleton.length / 4); - - return { - success: true, - skeleton: `${formatSkeletonHeader(filePath, tokenEstimate, langDef.id)}\n${skeleton}`, - tokenEstimate, - symbolCount: elisions.length, - language: langDef.id, - }; - } catch (err) { - return this.createFallbackResult( - filePath, - content, - `Parse error: ${err instanceof Error ? err.message : String(err)}`, - ); - } - } - - /** - * Find all regions that should be elided (function/method bodies). - * Recurses into containers (classes) to find methods. - */ - private findElisionRegions( - node: TreeSitterNode, - langId: string, - content: string, - elisions: ElisionRegion[], - opts: Required<SkeletonOptions>, - ): void { - // Check if this node has a body to elide - const bodyFieldName = getBodyField(langId, node.type); - - if (typeof bodyFieldName === "string") { - // This is a function/method - extract its body region - const bodyNode = node.childForFieldName?.(bodyFieldName); - if (bodyNode) { - const summary = this.createSummary(bodyNode, langId, opts); - elisions.push({ - bodyStart: bodyNode.startIndex, - bodyEnd: bodyNode.endIndex, - summary, - langId, - }); - // Don't recurse into the body - we're eliding it - return; - } - } - - // For containers (classes) or other nodes, recurse into children - const children = node.namedChildren || []; - for (const child of children) { - this.findElisionRegions(child, langId, content, elisions, opts); - } - } - - /** - * Create a summary comment for a function body. - */ - private createSummary( - bodyNode: TreeSitterNode, - langId: string, - opts: Required<SkeletonOptions>, - ): string { - if (!opts.includeSummary) { - return this.createElidedBody(langId, "// ..."); - } - - // Extract metadata from the body - const referencedSymbols = this.extractReferencedSymbols(bodyNode); - const complexity = this.calculateComplexity(bodyNode); - const role = this.classifyRole(complexity, referencedSymbols.length); - - const metadata: ChunkMetadata = { - referencedSymbols, - complexity, - role, - }; - - const commentStyle = getCommentStyle(langId); - const summaryLine = formatSummary(metadata, { - maxCalls: opts.maxCallsInSummary, - commentStyle, - }); - - return this.createElidedBody(langId, summaryLine); - } - - /** - * Create an elided body with summary for a specific language. - */ - private createElidedBody(langId: string, summary: string): string { - switch (langId) { - case "python": - // Python uses indented ... (Ellipsis) - valid syntax - return `\n ${summary}\n ...`; - case "ruby": - // Ruby body doesn't include closing 'end' in AST - it's preserved after - return `\n ${summary}`; - default: - // C-style languages use { ... } - return `{\n ${summary}\n }`; - } - } - - /** - * Build the skeleton by replacing function bodies with summaries. - */ - private buildSkeleton( - content: string, - elisions: ElisionRegion[], - _langId: string, - ): string { - const parts: string[] = []; - let cursor = 0; - - for (const elision of elisions) { - // Add content before this body - if (elision.bodyStart > cursor) { - parts.push(content.slice(cursor, elision.bodyStart)); - } - - // Add the elided body with summary - parts.push(elision.summary); - - cursor = elision.bodyEnd; - } - - // Add remaining content after last elision - if (cursor < content.length) { - parts.push(content.slice(cursor)); - } - - return this.cleanupSkeleton(parts.join("")); - } - - /** - * Clean up the skeleton output. - */ - private cleanupSkeleton(skeleton: string): string { - return ( - skeleton - // Remove excessive blank lines - .replace(/\n{3,}/g, "\n\n") - // Trim trailing whitespace on lines - .split("\n") - .map((line) => line.trimEnd()) - .join("\n") - .trim() - ); - } - - /** - * Create a fallback result when skeletonization isn't possible. - * Returns first 30 lines as preview. - */ - private createFallbackResult( - filePath: string, - content: string, - reason: string, - ): SkeletonResult { - const lines = content.split("\n"); - const previewLines = lines.slice(0, 30); - const truncated = lines.length > 30; - - const preview = [ - `// Skeleton unavailable: ${reason}`, - `// File: ${filePath}`, - `// Showing first ${Math.min(30, lines.length)} lines${truncated ? " (truncated)" : ""}`, - "", - ...previewLines, - ...(truncated ? ["", `// ... ${lines.length - 30} more lines`] : []), - ].join("\n"); - - return { - success: false, - skeleton: preview, - tokenEstimate: Math.ceil(preview.length / 4), - symbolCount: 0, - language: reason.includes("Unknown file extension") - ? path.extname(filePath) - : "unknown", - error: reason, - }; - } - - /** - * Load a TreeSitter language grammar. - */ - private async getLanguage(lang: string): Promise<TreeSitterLanguage | null> { - const cached = this.languages.get(lang); - if (cached !== undefined) return cached; - - const wasmPath = path.join(GRAMMARS_DIR, `tree-sitter-${lang}.wasm`); - if (!fs.existsSync(wasmPath)) { - this.languages.set(lang, null); - return null; - } - - try { - const language = Language - ? ((await Language.load(wasmPath)) as TreeSitterLanguage | null) - : null; - this.languages.set(lang, language); - return language; - } catch { - this.languages.set(lang, null); - return null; - } - } - - /** - * Extract referenced symbols (function calls) from a node. - */ - private extractReferencedSymbols(node: TreeSitterNode): string[] { - const refs: string[] = []; - const seen = new Set<string>(); - - const extract = (n: TreeSitterNode) => { - if (n.type === "call_expression" || n.type === "call") { - const func = n.childForFieldName?.("function"); - if (func) { - let funcName = func.text; - - // Handle member access (obj.method) - extract just method - if (func.type === "member_expression") { - const prop = func.childForFieldName?.("property"); - if (prop) funcName = prop.text; - } else if (func.type === "attribute") { - const attr = func.childForFieldName?.("attribute"); - if (attr) funcName = attr.text; - } - - // Dedupe and filter noise - if (funcName && !seen.has(funcName) && funcName.length < 30) { - seen.add(funcName); - refs.push(funcName); - } - } - } else if ( - n.type === "method_invocation" || // Java - n.type === "invocation_expression" // C# - ) { - // Java/C# method calls - const nameNode = - n.childForFieldName?.("name") || n.childForFieldName?.("function"); - if (nameNode) { - refs.push(nameNode.text); - seen.add(nameNode.text); - } - } else if ( - n.type === "method_call" || // Ruby - n.type === "command" || // Ruby - n.type === "command_call" // Ruby - ) { - const nameNode = - n.childForFieldName?.("method") || n.childForFieldName?.("name"); - if (nameNode) { - refs.push(nameNode.text); - seen.add(nameNode.text); - } - } - - for (const child of n.namedChildren || []) { - extract(child); - } - }; - - extract(node); - return refs; - } - - /** - * Calculate cyclomatic complexity of a node. - */ - private calculateComplexity(node: TreeSitterNode): number { - let complexity = 1; - const complexTypes = [ - "if_statement", - "for_statement", - "while_statement", - "switch_statement", - "catch_clause", - "conditional_expression", - ]; - - const count = (n: TreeSitterNode) => { - if (complexTypes.includes(n.type)) { - complexity++; - } - if (n.type === "binary_expression") { - const op = n.childForFieldName?.("operator"); - if (["&&", "||", "??"].includes(op?.text || "")) { - complexity++; - } - } - for (const child of n.namedChildren || []) { - count(child); - } - }; - - count(node); - return complexity; - } - - /** - * Classify the role of a function based on its characteristics. - */ - private classifyRole(complexity: number, refCount: number): string { - // High complexity + many calls = orchestration - if (complexity > 5 && refCount > 5) { - return "ORCHESTRATION"; - } - return "IMPLEMENTATION"; - } -} - -/** - * Convenience function to skeletonize a file. - */ -export async function skeletonizeFile( - filePath: string, - content: string, - options?: SkeletonOptions, -): Promise<SkeletonResult> { - const skeletonizer = new Skeletonizer(); - await skeletonizer.init(); - return skeletonizer.skeletonizeFile(filePath, content, options); -} diff --git a/src/lib/skeleton/summary-formatter.ts b/src/lib/skeleton/summary-formatter.ts deleted file mode 100644 index 575dc53b..00000000 --- a/src/lib/skeleton/summary-formatter.ts +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Format inline summary comments for skeleton bodies. - * - * Output format: "// → call1, call2, call3 | C:8 | ORCH" - * - Shows referenced symbols (what the function calls) - * - Shows complexity score - * - Shows ORCH role only (the interesting one) - */ - -export interface ChunkMetadata { - referencedSymbols?: string[]; - complexity?: number; - role?: string; -} - -export interface SummaryOptions { - /** Maximum number of calls to show before truncating */ - maxCalls?: number; - /** Whether to show complexity */ - showComplexity?: boolean; - /** Whether to show role (only ORCH is shown) */ - showRole?: boolean; - /** Comment style for the language */ - commentStyle?: "slash" | "hash" | "dash"; -} - -const DEFAULT_OPTIONS: Required<SummaryOptions> = { - maxCalls: 4, - showComplexity: true, - showRole: true, - commentStyle: "slash", -}; - -/** - * Format the inline summary comment for a function body. - * - * @example - * // Input: { referencedSymbols: ['findByEmail', 'compare', 'sign'], complexity: 8, role: 'ORCHESTRATION' } - * // Output: "// → findByEmail, compare, sign | C:8 | ORCH" - */ -export function formatSummary( - metadata: ChunkMetadata, - options: SummaryOptions = {}, -): string { - const opts = { ...DEFAULT_OPTIONS, ...options }; - const parts: string[] = []; - - // Calls (referenced symbols) - if (metadata.referencedSymbols?.length) { - const calls = metadata.referencedSymbols.slice(0, opts.maxCalls); - if (metadata.referencedSymbols.length > opts.maxCalls) { - calls.push("..."); - } - parts.push(`→ ${calls.join(", ")}`); - } - - // Complexity (only if > 1, trivial functions don't need it) - if (opts.showComplexity && metadata.complexity && metadata.complexity > 1) { - parts.push(`C:${metadata.complexity}`); - } - - // Role (only show ORCH - it's the architecturally interesting one) - if (opts.showRole && metadata.role === "ORCHESTRATION") { - parts.push("ORCH"); - } - - // Build the comment - const commentPrefix = getCommentPrefix(opts.commentStyle); - - if (parts.length === 0) { - return `${commentPrefix} ...`; - } - - return `${commentPrefix} ${parts.join(" | ")}`; -} - -/** - * Get the single-line comment prefix for a language style. - */ -function getCommentPrefix(style: "slash" | "hash" | "dash"): string { - switch (style) { - case "hash": - return "#"; - case "dash": - return "--"; - case "slash": - default: - return "//"; - } -} - -/** - * Get the appropriate comment style for a language. - */ -export function getCommentStyle(langId: string): "slash" | "hash" | "dash" { - switch (langId) { - case "python": - case "ruby": - case "bash": - return "hash"; - case "sql": - case "lua": - return "dash"; - default: - return "slash"; - } -} - -/** - * Format the skeleton file header comment. - */ -export function formatSkeletonHeader( - filePath: string, - tokenEstimate: number, - langId?: string, -): string { - const prefix = langId - ? getCommentPrefix(getCommentStyle(langId)) - : "//"; - return `${prefix} ${filePath} (skeleton, ~${tokenEstimate} tokens)`; -} diff --git a/src/lib/utils/exit.ts b/src/lib/utils/exit.ts index b9eac674..f498ae62 100644 --- a/src/lib/utils/exit.ts +++ b/src/lib/utils/exit.ts @@ -1,4 +1,3 @@ -import { destroyWorkerPool, isWorkerPoolInitialized } from "../workers/pool"; import { runCleanup } from "./cleanup"; export async function gracefulExit(code?: number): Promise<void> { @@ -9,14 +8,6 @@ export async function gracefulExit(code?: number): Promise<void> { ? process.exitCode : 0; - try { - if (isWorkerPoolInitialized()) { - await destroyWorkerPool(); - } - } catch (err) { - console.error("[exit] Failed to destroy worker pool:", err); - } - await runCleanup(); // Avoid exiting the process during test runs so Vitest can report results. diff --git a/src/lib/workers/colbert-math.ts b/src/lib/workers/colbert-math.ts deleted file mode 100644 index ea5b04d9..00000000 --- a/src/lib/workers/colbert-math.ts +++ /dev/null @@ -1,92 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import { inner } from "simsimd"; -import { MODEL_IDS, PATHS } from "../../config"; - -let SKIP_IDS: Set<number> | null = null; - -function loadSkipIds(): Set<number> { - if (SKIP_IDS) return SKIP_IDS; - - // Check local models first (same logic as orchestrator) - const PROJECT_ROOT = process.env.OSGREP_PROJECT_ROOT - ? path.resolve(process.env.OSGREP_PROJECT_ROOT) - : process.cwd(); - const localModels = path.join(PROJECT_ROOT, "models"); - const localColbert = path.join(localModels, ...MODEL_IDS.colbert.split("/")); - const localSkipPath = path.join(localColbert, "skiplist.json"); - - // Try local first, then global - const globalBasePath = path.join( - PATHS.models, - ...MODEL_IDS.colbert.split("/"), - ); - const globalSkipPath = path.join(globalBasePath, "skiplist.json"); - - const skipPath = fs.existsSync(localSkipPath) - ? localSkipPath - : globalSkipPath; - - if (fs.existsSync(skipPath)) { - try { - const parsed = JSON.parse(fs.readFileSync(skipPath, "utf8")) as number[]; - SKIP_IDS = new Set<number>(parsed.map((n) => Number(n))); - return SKIP_IDS; - } catch (_e) { - // fall through to empty set - } - } - SKIP_IDS = new Set<number>(); - return SKIP_IDS; -} - -export function maxSim( - queryEmbeddings: number[][] | Float32Array[], - docEmbeddings: number[][] | Float32Array[], - docTokenIds?: number[], -): number { - if (queryEmbeddings.length === 0 || docEmbeddings.length === 0) { - return 0; - } - - const qVecs = queryEmbeddings.map((v) => - v instanceof Float32Array ? v : new Float32Array(v), - ); - const dVecs = docEmbeddings.map((v) => - v instanceof Float32Array ? v : new Float32Array(v), - ); - const dTokenIds = - docTokenIds && docTokenIds.length === dVecs.length ? docTokenIds : null; - const skipIds = loadSkipIds(); - - let totalScore = 0; - for (const qVec of qVecs) { - let maxDotProduct = -Infinity; - for (let idx = 0; idx < dVecs.length; idx++) { - const tokenId = dTokenIds ? dTokenIds[idx] : null; - if (tokenId !== null && skipIds.has(Number(tokenId))) continue; - const dVec = dVecs[idx]; - const dim = Math.min(qVec.length, dVec.length); - const dot = inner(qVec.subarray(0, dim), dVec.subarray(0, dim)); - if (dot > maxDotProduct) maxDotProduct = dot; - } - if (maxDotProduct === -Infinity) maxDotProduct = 0; - totalScore += maxDotProduct; - } - - return totalScore; -} - -export function cosineSim( - a: number[] | Float32Array, - b: number[] | Float32Array, -): number { - const aVec = a instanceof Float32Array ? a : new Float32Array(a); - const bVec = b instanceof Float32Array ? b : new Float32Array(b); - - const dim = Math.min(aVec.length, bVec.length); - if (aVec.length !== bVec.length) { - return inner(aVec.subarray(0, dim), bVec.subarray(0, dim)); - } - return inner(aVec, bVec); -} diff --git a/src/lib/workers/colbert-tokenizer.ts b/src/lib/workers/colbert-tokenizer.ts deleted file mode 100644 index 0571a1e5..00000000 --- a/src/lib/workers/colbert-tokenizer.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { - AutoTokenizer, - type PreTrainedTokenizer, -} from "@huggingface/transformers"; - -const QUERY_MARKER_TOKEN = "[Q] "; -const DOC_MARKER_TOKEN = "[D] "; -const MASK_TOKEN = "[MASK]"; -const QUERY_MAXLEN = 32; // Standard ColBERT query length -const DOC_MAXLEN = 512; // Standard ColBERT document length - -export class ColBERTTokenizer { - private tokenizer: PreTrainedTokenizer | null = null; - private specialTokenIds: { - cls: number; - sep: number; - pad: number; - mask: number; - queryMarker: number; - docMarker: number; - } | null = null; - - async init(modelPath: string) { - this.tokenizer = await AutoTokenizer.from_pretrained(modelPath); - - // Get special token IDs with fallbacks - // We use the IDs we discovered in validation: [Q]=50368, [D]=50369 - // But we still try to look them up dynamically first. - - const tokenizer = this.tokenizer; - const get = (token: string) => tokenizer?.model.tokens_to_ids.get(token); - - const specialTokens = tokenizer as Partial<{ - cls_token: string; - sep_token: string; - pad_token: string; - }>; - const clsId = get(specialTokens.cls_token ?? "[CLS]") ?? 50281; - const sepId = get(specialTokens.sep_token ?? "[SEP]") ?? 50282; - const padId = get(specialTokens.pad_token ?? "[PAD]") ?? 50283; - const maskId = get(MASK_TOKEN) ?? 50284; - const queryMarkerId = get(QUERY_MARKER_TOKEN) ?? 50368; - const docMarkerId = get(DOC_MARKER_TOKEN) ?? 50369; - - this.specialTokenIds = { - cls: clsId, - sep: sepId, - pad: padId, - mask: maskId, - queryMarker: queryMarkerId, - docMarker: docMarkerId, - }; - } - - async encodeQuery( - text: string, - ): Promise<{ input_ids: bigint[]; attention_mask: bigint[] }> { - if (!this.tokenizer || !this.specialTokenIds) { - throw new Error("Tokenizer not initialized. Call init() first."); - } - - // Tokenize without special tokens - const encoded = await this.tokenizer(text, { - add_special_tokens: false, - truncation: true, - max_length: QUERY_MAXLEN - 2, // Reserve space for [CLS] and [Q] - }); - - const { input_ids } = encoded; - - // Build sequence: [CLS] [Q] token1 token2 ... [SEP] [MASK] [MASK] ... - const finalIds: number[] = [ - this.specialTokenIds.cls, - this.specialTokenIds.queryMarker, - ...Array.from(input_ids.data as BigInt64Array).map(Number), - this.specialTokenIds.sep, - ]; - - // Query Expansion: pad with [MASK] tokens up to QUERY_MAXLEN - while (finalIds.length < QUERY_MAXLEN) { - finalIds.push(this.specialTokenIds.mask); - } - - // Truncate if somehow longer (safety check) - if (finalIds.length > QUERY_MAXLEN) { - finalIds.length = QUERY_MAXLEN; - } - - // Create attention mask (1 for all tokens, since MASK is also attended to) - const attentionMask = new Array(finalIds.length).fill(1); - - return { - input_ids: finalIds.map((id) => BigInt(id)), - attention_mask: attentionMask.map((v) => BigInt(v)), - }; - } - - async encodeDoc( - text: string, - ): Promise<{ input_ids: bigint[]; attention_mask: bigint[] }> { - if (!this.tokenizer || !this.specialTokenIds) { - throw new Error("Tokenizer not initialized. Call init() first."); - } - - // Tokenize without special tokens - const encoded = await this.tokenizer(text, { - add_special_tokens: false, - truncation: true, - max_length: DOC_MAXLEN - 3, // Reserve space for [CLS], [D], and [SEP] - }); - - const { input_ids } = encoded; - - // Build sequence: [CLS] [D] token1 token2 ... [SEP] - const finalIds: number[] = [ - this.specialTokenIds.cls, - this.specialTokenIds.docMarker, - ...Array.from(input_ids.data as BigInt64Array).map(Number), - this.specialTokenIds.sep, - ]; - - // Create attention mask - const attentionMask = new Array(finalIds.length).fill(1); - - return { - input_ids: finalIds.map((id) => BigInt(id)), - attention_mask: attentionMask.map((v) => BigInt(v)), - }; - } -} diff --git a/src/lib/workers/download-worker.ts b/src/lib/workers/download-worker.ts deleted file mode 100644 index 1bfd9040..00000000 --- a/src/lib/workers/download-worker.ts +++ /dev/null @@ -1,150 +0,0 @@ -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import { parentPort } from "node:worker_threads"; -import { env, pipeline } from "@huggingface/transformers"; -import { MODEL_IDS } from "../../config"; - -// Configuration -const HOMEDIR = os.homedir(); -const CACHE_DIR = path.join(HOMEDIR, ".osgrep", "models"); -env.cacheDir = CACHE_DIR; -env.allowLocalModels = true; -env.allowRemoteModels = true; - -// Suppress noisy warnings from transformers.js/onnxruntime -const originalWarn = console.warn; -console.warn = (...args) => { - if ( - args[0] && - typeof args[0] === "string" && - args[0].includes("Unable to determine content-length") - ) { - return; - } - originalWarn(...args); -}; - -type QuantizationDType = - | "auto" - | "fp32" - | "fp16" - | "q8" - | "int8" - | "uint8" - | "q4" - | "bnb4" - | "q4f16"; - -type PipelineDType = QuantizationDType | Record<string, QuantizationDType>; - -// Helper to download with timeout -async function downloadModelWithTimeout(modelId: string, dtype: PipelineDType) { - const TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes - - try { - const downloadPromise = pipeline("feature-extraction", modelId, { - dtype, - progress_callback: (progress: unknown) => { - if (parentPort) parentPort.postMessage({ type: "progress", progress }); - }, - }); - - const timeoutPromise = new Promise<never>((_, reject) => { - setTimeout( - () => reject(new Error(`Download timed out after ${TIMEOUT_MS} ms`)), - TIMEOUT_MS, - ); - }); - - return Promise.race([downloadPromise, timeoutPromise]); - } catch (err) { - console.error(`Worker: pipeline creation failed for ${modelId}: `, err); - throw err; - } -} - -// Helper to manually download extra files like skiplist.json -async function downloadExtraFile(modelId: string, filename: string) { - const url = `https://huggingface.co/${modelId}/resolve/main/${filename}`; - // Construct path: ~/.osgrep/models/ryandono/osgrep-colbert-q8/skiplist.json - const destDir = path.join(CACHE_DIR, ...modelId.split("/")); - const destPath = path.join(destDir, filename); - - if (!fs.existsSync(destDir)) { - fs.mkdirSync(destDir, { recursive: true }); - } - - // If file exists and is non-zero, skip (or implement hash check if you want SOTA robustness) - if (fs.existsSync(destPath) && fs.statSync(destPath).size > 0) { - return; - } - - if (parentPort) { - parentPort.postMessage({ - type: "progress", - progress: { status: "downloading", file: filename }, - }); - } - - try { - const res = await fetch(url); - if (!res.ok) { - throw new Error(`HTTP ${res.status}: ${res.statusText}`); - } - const buffer = await res.arrayBuffer(); - fs.writeFileSync(destPath, Buffer.from(buffer)); - if (parentPort) { - parentPort.postMessage({ - type: "progress", - progress: { status: "downloaded", file: filename }, - }); - } - } catch (e) { - const errorMsg = e instanceof Error ? e.message : String(e); - console.warn(`⚠️ Failed to download ${filename} from ${url}:`, errorMsg); - // Don't crash, just warn. The math worker has a fallback (empty set). - // But report the failure so setup can retry - if (parentPort) { - parentPort.postMessage({ - type: "warning", - file: filename, - error: errorMsg, - }); - } - } -} - -async function download() { - try { - // 1. Download Dense Model - const embedPipeline = await downloadModelWithTimeout(MODEL_IDS.embed, "q4"); - await embedPipeline.dispose(); - - // 2. Download ColBERT Model - const colbertPipeline = await downloadModelWithTimeout( - MODEL_IDS.colbert, - "int8", - ); - await colbertPipeline.dispose(); - - // 3. Download the custom Skiplist - await downloadExtraFile(MODEL_IDS.colbert, "skiplist.json"); - - if (parentPort) { - parentPort.postMessage({ status: "success" }); - } else { - process.exit(0); - } - } catch (error) { - console.error("Worker failed to download models:", error); - if (parentPort) { - const errorMsg = error instanceof Error ? error.message : String(error); - parentPort.postMessage({ status: "error", error: errorMsg }); - } else { - process.exit(1); - } - } -} - -download(); diff --git a/src/lib/workers/embeddings/colbert.ts b/src/lib/workers/embeddings/colbert.ts deleted file mode 100644 index 0982db18..00000000 --- a/src/lib/workers/embeddings/colbert.ts +++ /dev/null @@ -1,199 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import * as ort from "onnxruntime-node"; -import { MODEL_IDS, PATHS } from "../../../config"; -import { ColBERTTokenizer } from "../colbert-tokenizer"; - -const CACHE_DIR = PATHS.models; -const ONNX_THREADS = 1; -const LOG_MODELS = - process.env.OSGREP_DEBUG_MODELS === "1" || - process.env.OSGREP_DEBUG_MODELS === "true"; -const log = (...args: unknown[]) => { - if (LOG_MODELS) console.log(...args); -}; - -export type HybridResult = { - dense: Float32Array; - colbert: Int8Array; - scale: number; - pooled_colbert_48d?: Float32Array; - token_ids?: number[]; -}; - -export class ColbertModel { - private session: ort.InferenceSession | null = null; - public tokenizer: ColBERTTokenizer | null = null; - - async load() { - if (this.session && this.tokenizer) return; - - this.tokenizer = new ColBERTTokenizer(); - - const basePath = path.join(CACHE_DIR, MODEL_IDS.colbert); - const onnxDir = path.join(basePath, "onnx"); - - const modelPath = path.join(onnxDir, "model_int8.onnx"); - if (!fs.existsSync(modelPath)) { - throw new Error(`ColBERT ONNX model not found at ${modelPath}`); - } - - await this.tokenizer.init(basePath); - - const sessionOptions: ort.InferenceSession.SessionOptions = { - executionProviders: ["cpu"], - intraOpNumThreads: ONNX_THREADS, - interOpNumThreads: 1, - graphOptimizationLevel: "all", - }; - - log(`Worker: Loading ColBERT ONNX session from ${modelPath}`); - this.session = await ort.InferenceSession.create(modelPath, sessionOptions); - - if (!this.session) { - throw new Error(`ColBERT ONNX load failed; tried ${modelPath}`); - } - } - - isReady(): boolean { - return !!(this.session && this.tokenizer); - } - - async runBatch( - texts: string[], - denseVectors: Float32Array[], - vectorDimensions: number, - ): Promise<HybridResult[]> { - if (!this.session || !this.tokenizer) return []; - const tokenizer = this.tokenizer; - const session = this.session; - - const encodedBatch = await Promise.all( - texts.map((t) => tokenizer.encodeDoc(t)), - ); - - const maxLen = Math.max(...encodedBatch.map((e) => e.input_ids.length)); - const batchInputIds = new BigInt64Array(texts.length * maxLen); - const batchAttentionMask = new BigInt64Array(texts.length * maxLen); - const padId = BigInt(50283); - - for (let i = 0; i < encodedBatch.length; i++) { - const encoded = encodedBatch[i]; - const offset = i * maxLen; - for (let j = 0; j < maxLen; j++) { - if (j < encoded.input_ids.length) { - batchInputIds[offset + j] = encoded.input_ids[j]; - batchAttentionMask[offset + j] = encoded.attention_mask[j]; - } else { - batchInputIds[offset + j] = padId; - batchAttentionMask[offset + j] = BigInt(0); - } - } - } - - const feeds = { - input_ids: new ort.Tensor("int64", batchInputIds, [texts.length, maxLen]), - attention_mask: new ort.Tensor("int64", batchAttentionMask, [ - texts.length, - maxLen, - ]), - }; - - const sessionOut = await session.run(feeds); - const outputName = session.outputNames[0]; - const output = sessionOut[outputName]; - if (!output) { - throw new Error("ColBERT session output missing embeddings tensor"); - } - - const data = output.data as Float32Array; - const [batch, seq, dim] = output.dims as number[]; - const results: HybridResult[] = []; - - for (let b = 0; b < batch; b++) { - const batchOffset = b * seq * dim; - const originalLen = encodedBatch[b].input_ids.length; - const normalized = new Float32Array(originalLen * dim); - let maxVal = 0; - - for (let s = 0; s < originalLen; s++) { - const offset = batchOffset + s * dim; - let sumSq = 0; - for (let d = 0; d < dim; d++) { - const val = data[offset + d]; - sumSq += val * val; - } - const norm = Math.sqrt(sumSq) || 1; - - for (let d = 0; d < dim; d++) { - const val = data[offset + d] / norm; - const idx = s * dim + d; - normalized[idx] = val; - if (Math.abs(val) > maxVal) maxVal = Math.abs(val); - } - } - - if (maxVal === 0) maxVal = 1; - - const int8Array = new Int8Array(normalized.length); - for (let i = 0; i < normalized.length; i++) { - int8Array[i] = Math.max( - -127, - Math.min(127, Math.round((normalized[i] / maxVal) * 127)), - ); - } - - const pooled = new Float32Array(dim); - const tokenCount = Math.max(1, originalLen); - for (let s = 0; s < originalLen; s++) { - const tokenOffset = s * dim; - for (let d = 0; d < dim; d++) { - pooled[d] += normalized[tokenOffset + d]; - } - } - let pooledNorm = 0; - for (let d = 0; d < dim; d++) { - pooled[d] /= tokenCount; - pooledNorm += pooled[d] * pooled[d]; - } - pooledNorm = Math.sqrt(pooledNorm) || 1; - for (let d = 0; d < dim; d++) { - pooled[d] /= pooledNorm; - } - - results.push({ - dense: denseVectors[b] ?? new Float32Array(vectorDimensions).fill(0), - colbert: int8Array, - scale: maxVal, - pooled_colbert_48d: pooled, - token_ids: Array.from(encodedBatch[b].input_ids, (v) => Number(v)), - }); - } - - return results; - } - - async encodeQuery(text: string): Promise<{ - input_ids: BigInt64Array; - attention_mask: BigInt64Array; - }> { - if (!this.tokenizer) throw new Error("ColBERT tokenizer not initialized"); - const encoded = await this.tokenizer.encodeQuery(text); - return { - input_ids: new BigInt64Array(encoded.input_ids), - attention_mask: new BigInt64Array(encoded.attention_mask), - }; - } - - async runSession( - feeds: Record<string, ort.Tensor>, - ): Promise<ort.InferenceSession.OnnxValueMapType> { - if (!this.session) throw new Error("ColBERT session not initialized"); - return this.session.run(feeds); - } - - getOutputName(): string { - if (!this.session) throw new Error("ColBERT session not initialized"); - return this.session.outputNames[0]; - } -} diff --git a/src/lib/workers/embeddings/granite.ts b/src/lib/workers/embeddings/granite.ts deleted file mode 100644 index b468061f..00000000 --- a/src/lib/workers/embeddings/granite.ts +++ /dev/null @@ -1,175 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import { - AutoTokenizer, - type PreTrainedTokenizer, -} from "@huggingface/transformers"; -import * as ort from "onnxruntime-node"; -import { CONFIG, MODEL_IDS, PATHS } from "../../../config"; - -const CACHE_DIR = PATHS.models; -const ONNX_THREADS = 1; -const LOG_MODELS = - process.env.OSGREP_DEBUG_MODELS === "1" || - process.env.OSGREP_DEBUG_MODELS === "true"; -const log = (...args: unknown[]) => { - if (LOG_MODELS) console.log(...args); -}; - -export class GraniteModel { - private session: ort.InferenceSession | null = null; - private tokenizer: PreTrainedTokenizer | null = null; - private readonly vectorDimensions = CONFIG.VECTOR_DIM; - - private resolvePaths(): { modelPath: string; tokenizerPath: string } { - const basePath = path.join(CACHE_DIR, MODEL_IDS.embed); - const onnxDir = path.join(basePath, "onnx"); - const candidates = ["model_q4.onnx", "model.onnx"]; - - for (const candidate of candidates) { - const candidatePath = path.join(onnxDir, candidate); - if (fs.existsSync(candidatePath)) { - return { modelPath: candidatePath, tokenizerPath: basePath }; - } - } - - throw new Error( - `Granite ONNX model not found. Looked for ${candidates.join( - ", ", - )} in ${onnxDir}`, - ); - } - - async load() { - if (this.session && this.tokenizer) return; - - const { modelPath, tokenizerPath } = this.resolvePaths(); - log(`Worker: Loading Granite ONNX session from ${modelPath}`); - - this.tokenizer = await AutoTokenizer.from_pretrained(tokenizerPath); - - const sessionOptions: ort.InferenceSession.SessionOptions = { - executionProviders: ["cpu"], - intraOpNumThreads: ONNX_THREADS, - interOpNumThreads: 1, - graphOptimizationLevel: "all", - }; - this.session = await ort.InferenceSession.create(modelPath, sessionOptions); - } - - isReady(): boolean { - return !!(this.session && this.tokenizer); - } - - private meanPool( - hidden: Float32Array, - attention: BigInt64Array, - batch: number, - seq: number, - hiddenDim: number, - targetDim: number, - ): Float32Array[] { - const vectors: Float32Array[] = []; - const seqFromMask = attention.length / Math.max(1, batch); - const usableSeq = Math.min(seq, seqFromMask); - const dim = Math.min(hiddenDim, targetDim); - - for (let b = 0; b < batch; b++) { - const sum = new Float32Array(dim); - let count = 0; - const attOffset = b * seqFromMask; - const hiddenOffset = b * seq * hiddenDim; - - for (let s = 0; s < usableSeq; s++) { - if (attention[attOffset + s] > 0) { - count++; - const tokenOffset = hiddenOffset + s * hiddenDim; - for (let d = 0; d < dim; d++) { - sum[d] += hidden[tokenOffset + d]; - } - } - } - - if (count === 0) count = 1; - let norm = 0; - for (let d = 0; d < dim; d++) { - sum[d] /= count; - norm += sum[d] * sum[d]; - } - norm = Math.sqrt(norm) || 1; - for (let d = 0; d < dim; d++) { - sum[d] /= norm; - } - - if (dim < targetDim) { - const padded = new Float32Array(targetDim); - padded.set(sum); - vectors.push(padded); - } else { - vectors.push(sum); - } - } - - return vectors; - } - - async runBatch(texts: string[]): Promise<Float32Array[]> { - if (!this.session || !this.tokenizer) return []; - - const encoded = await this.tokenizer(texts, { - padding: true, - truncation: true, - max_length: 256, - }); - - type EncodedTensor = { data: BigInt64Array; dims?: number[] }; - const inputTensor = encoded.input_ids as unknown as EncodedTensor; - const attentionTensor = encoded.attention_mask as unknown as EncodedTensor; - const inputIds = inputTensor.data; - const attentionMask = attentionTensor.data; - const seqLen = - inputTensor.dims?.[1] ?? - Math.max(1, Math.floor(inputIds.length / texts.length)); - - const tokenTypeIdsRaw = ( - encoded as Partial<{ token_type_ids: EncodedTensor }> - ).token_type_ids; - const tokenTypeIds = - tokenTypeIdsRaw && - tokenTypeIdsRaw.data.length === inputIds.length && - tokenTypeIdsRaw.data.length === attentionMask.length - ? tokenTypeIdsRaw.data - : new BigInt64Array(inputIds.length).fill(BigInt(0)); - - const feeds = { - input_ids: new ort.Tensor("int64", inputIds, [texts.length, seqLen]), - attention_mask: new ort.Tensor("int64", attentionMask, [ - texts.length, - seqLen, - ]), - token_type_ids: new ort.Tensor("int64", tokenTypeIds, [ - texts.length, - seqLen, - ]), - }; - - const sessionOut = await this.session.run(feeds); - const hidden = - sessionOut.last_hidden_state ?? sessionOut[this.session.outputNames[0]]; - - if (!hidden) { - throw new Error("Granite ONNX output missing last_hidden_state"); - } - - const hiddenData = hidden.data as Float32Array; - const [batch, seq, dim] = hidden.dims as number[]; - return this.meanPool( - hiddenData, - attentionMask, - batch, - seq, - dim, - this.vectorDimensions, - ); - } -} diff --git a/src/lib/workers/orchestrator.ts b/src/lib/workers/orchestrator.ts index 63a366dd..3dc9d95f 100644 --- a/src/lib/workers/orchestrator.ts +++ b/src/lib/workers/orchestrator.ts @@ -1,16 +1,24 @@ -import * as fs from "node:fs"; +/** + * Orchestrator: Coordinates file processing and embedding + * + * This module handles: + * - File reading and validation + * - Chunking via tree-sitter + * - Embedding via native Rust code + * + * No worker pool needed - Rust ONNX Runtime is fast and stable. + */ + import * as path from "node:path"; -import { env } from "@huggingface/transformers"; -import * as ort from "onnxruntime-node"; import { v4 as uuidv4 } from "uuid"; -import { CONFIG, PATHS } from "../../config"; +import { CONFIG } from "../../config"; import { buildAnchorChunk, type ChunkWithContext, formatChunkText, TreeSitterChunker, } from "../index/chunker"; -import { Skeletonizer } from "../skeleton"; +import { embedBatch, initNative, encodeQueryColbert } from "../native"; import type { PreparedChunk, VectorRecord } from "../store/types"; import { computeBufferHash, @@ -18,9 +26,10 @@ import { isIndexableFile, readFileSnapshot, } from "../utils/file-utils"; -import { maxSim } from "./colbert-math"; -import { ColbertModel, type HybridResult } from "./embeddings/colbert"; -import { GraniteModel } from "./embeddings/granite"; + +// ============================================================================= +// Types +// ============================================================================= export type ProcessFileInput = { path: string; @@ -41,48 +50,23 @@ export type RerankDoc = { token_ids?: number[]; }; -const CACHE_DIR = PATHS.models; -const LOG_MODELS = - process.env.OSGREP_DEBUG_MODELS === "1" || - process.env.OSGREP_DEBUG_MODELS === "true"; -const log = (...args: unknown[]) => { - if (LOG_MODELS) console.log(...args); -}; - -env.cacheDir = CACHE_DIR; -env.allowLocalModels = true; -env.allowRemoteModels = true; +// ============================================================================= +// Orchestrator +// ============================================================================= const PROJECT_ROOT = process.env.OSGREP_PROJECT_ROOT ? path.resolve(process.env.OSGREP_PROJECT_ROOT) : process.cwd(); -const LOCAL_MODELS = path.join(PROJECT_ROOT, "models"); -if (fs.existsSync(LOCAL_MODELS)) { - env.localModelPath = LOCAL_MODELS; - log(`Worker: Using local models from ${LOCAL_MODELS}`); -} export class WorkerOrchestrator { - private granite = new GraniteModel(); - private colbert = new ColbertModel(); private chunker = new TreeSitterChunker(); - private skeletonizer = new Skeletonizer(); private initPromise: Promise<void> | null = null; - private readonly vectorDimensions = CONFIG.VECTOR_DIM; private async ensureReady() { - if (this.granite.isReady() && this.colbert.isReady()) { - return; - } if (this.initPromise) return this.initPromise; this.initPromise = (async () => { - await Promise.all([ - this.chunker.init(), - this.skeletonizer.init(), - this.granite.load(), - this.colbert.load(), - ]); + await Promise.all([this.chunker.init(), initNative()]); })().finally(() => { this.initPromise = null; }); @@ -90,46 +74,14 @@ export class WorkerOrchestrator { return this.initPromise; } - private async computeHybrid( - texts: string[], - onProgress?: () => void, - ): Promise<HybridResult[]> { - if (!texts.length) return []; - await this.ensureReady(); - - const results: HybridResult[] = []; - const envBatch = Number.parseInt( - process.env.OSGREP_WORKER_BATCH_SIZE ?? "", - 10, - ); - const BATCH_SIZE = - Number.isFinite(envBatch) && envBatch > 0 - ? Math.max(4, Math.min(16, envBatch)) - : 16; - for (let i = 0; i < texts.length; i += BATCH_SIZE) { - if (i > 0) onProgress?.(); - const batchTexts = texts.slice(i, i + BATCH_SIZE); - const denseBatch = await this.granite.runBatch(batchTexts); - const colbertBatch = await this.colbert.runBatch( - batchTexts, - denseBatch, - this.vectorDimensions, - ); - results.push(...colbertBatch); - } - onProgress?.(); - - return results; - } - private async chunkFile( pathname: string, - content: string, + content: string ): Promise<ChunkWithContext[]> { await this.ensureReady(); const { chunks: parsedChunks, metadata } = await this.chunker.chunk( pathname, - content, + content ); const anchorChunk = buildAnchorChunk(pathname, content, metadata); @@ -159,12 +111,11 @@ export class WorkerOrchestrator { } private toPreparedChunks( - path: string, + filePath: string, hash: string, - chunks: ChunkWithContext[], - skeleton?: string, + chunks: ChunkWithContext[] ): PreparedChunk[] { - const texts = chunks.map((chunk) => formatChunkText(chunk, path)); + const texts = chunks.map((chunk) => formatChunkText(chunk, filePath)); const prepared: PreparedChunk[] = []; for (let i = 0; i < texts.length; i++) { @@ -175,10 +126,10 @@ export class WorkerOrchestrator { prepared.push({ id: uuidv4(), - path, + path: filePath, hash, - content: content, // Now minimal - display_text: displayText, // Now rich + content, + display_text: displayText, context_prev: typeof prev === "string" ? prev : undefined, context_next: typeof next === "string" ? next : undefined, start_line: chunk.startLine, @@ -192,7 +143,6 @@ export class WorkerOrchestrator { referenced_symbols: chunk.referencedSymbols, role: chunk.role, parent_symbol: chunk.parentSymbol, - file_skeleton: chunk.isAnchor ? skeleton : undefined, }); } @@ -201,7 +151,7 @@ export class WorkerOrchestrator { async processFile( input: ProcessFileInput, - onProgress?: () => void, + onProgress?: () => void ): Promise<ProcessFileResult> { const absolutePath = path.isAbsolute(input.path) ? input.path @@ -225,49 +175,28 @@ export class WorkerOrchestrator { onProgress?.(); const content = buffer.toString("utf-8"); - const chunksPromise = this.chunkFile(input.path, content); - - // Generate skeleton in parallel - const skeletonPromise = this.skeletonizer.skeletonizeFile( - input.path, - content, - { - includeSummary: true, - }, - ); - - const [chunks, skeletonResult] = await Promise.all([ - chunksPromise, - skeletonPromise, - ]); + const chunks = await this.chunkFile(input.path, content); onProgress?.(); if (!chunks.length) return { vectors: [], hash, mtimeMs, size }; - const preparedChunks = this.toPreparedChunks( - input.path, - hash, - chunks, - skeletonResult.success ? skeletonResult.skeleton : undefined, - ); - const hybrids = await this.computeHybrid( - preparedChunks.map((chunk) => chunk.content), - onProgress, + const preparedChunks = this.toPreparedChunks(input.path, hash, chunks); + + // Embed all chunks via native Rust + const hybrids = await embedBatch( + preparedChunks.map((chunk) => chunk.content) ); + onProgress?.(); - const vectors = preparedChunks.map((chunk, idx) => { - const hybrid = hybrids[idx] ?? { - dense: new Float32Array(), - colbert: new Int8Array(), - scale: 1, - }; + const vectors: VectorRecord[] = preparedChunks.map((chunk, idx) => { + const hybrid = hybrids[idx]; return { ...chunk, vector: hybrid.dense, colbert: Buffer.from(hybrid.colbert), - colbert_scale: hybrid.scale, - pooled_colbert_48d: hybrid.pooled_colbert_48d, - doc_token_ids: hybrid.token_ids, + colbert_scale: 1, // Native returns pre-scaled INT8 + pooled_colbert_48d: undefined, // Can compute if needed + doc_token_ids: undefined, }; }); @@ -283,51 +212,21 @@ export class WorkerOrchestrator { }> { await this.ensureReady(); - const [denseVector] = await this.granite.runBatch([text]); - - const encoded = await this.colbert.encodeQuery(text); - - const feeds = { - input_ids: new ort.Tensor("int64", encoded.input_ids, [ - 1, - encoded.input_ids.length, - ]), - attention_mask: new ort.Tensor("int64", encoded.attention_mask, [ - 1, - encoded.attention_mask.length, - ]), - }; - - const sessionOut = await this.colbert.runSession(feeds); - const outputName = this.colbert.getOutputName(); - const output = sessionOut[outputName]; - if (!output) { - throw new Error("ColBERT session output missing embeddings tensor"); - } + // Get dense embedding + const hybrids = await embedBatch([text]); + const denseVector = hybrids[0].dense; - const data = output.data as Float32Array; - const [, seq, dim] = output.dims as number[]; + // Get ColBERT query embedding + const colbertFlat = await encodeQueryColbert(text); + const dim = CONFIG.COLBERT_DIM; + const seqLen = colbertFlat.length / dim; + // Reshape to matrix const matrix: number[][] = []; - - for (let s = 0; s < seq; s++) { - let sumSq = 0; - const offset = s * dim; - for (let d = 0; d < dim; d++) { - const val = data[offset + d]; - sumSq += val * val; - } - const norm = Math.sqrt(sumSq); - + for (let s = 0; s < seqLen; s++) { const row: number[] = []; - if (norm > 1e-9) { - for (let d = 0; d < dim; d++) { - row.push(data[offset + d] / norm); - } - } else { - for (let d = 0; d < dim; d++) { - row.push(data[offset + d]); - } + for (let d = 0; d < dim; d++) { + row.push(colbertFlat[s * dim + d]); } matrix.push(row); } @@ -339,7 +238,7 @@ export class WorkerOrchestrator { pooled[d] += row[d]; } } - // Normalize pooled + // Normalize let sumSq = 0; for (let d = 0; d < dim; d++) { pooled[d] /= matrix.length || 1; @@ -353,7 +252,7 @@ export class WorkerOrchestrator { } return { - dense: Array.from(denseVector ?? []), + dense: Array.from(denseVector), colbert: matrix, colbertDim: dim, pooled_colbert_48d: Array.from(pooled), @@ -366,8 +265,10 @@ export class WorkerOrchestrator { colbertDim: number; }): Promise<number[]> { await this.ensureReady(); + + // MaxSim scoring in TypeScript (simple, matches Rust behavior) const queryMatrix = input.query.map((row) => - row instanceof Float32Array ? row : new Float32Array(row), + row instanceof Float32Array ? row : new Float32Array(row) ); return input.docs.map((doc) => { @@ -378,15 +279,6 @@ export class WorkerOrchestrator { colbert = col; } else if (Buffer.isBuffer(col)) { colbert = new Int8Array(col.buffer, col.byteOffset, col.byteLength); - } else if ( - col && - typeof col === "object" && - "type" in col && - (col as any).type === "Buffer" && - Array.isArray((col as any).data) - ) { - // IPC serialization fallback (still copies, but unavoidable without SharedArrayBuffer) - colbert = new Int8Array((col as any).data); } else if (Array.isArray(col)) { colbert = new Int8Array(col); } else { @@ -394,20 +286,60 @@ export class WorkerOrchestrator { } const seqLen = Math.floor(colbert.length / input.colbertDim); - const docMatrix: Float32Array[] = []; - for (let i = 0; i < seqLen; i++) { - const start = i * input.colbertDim; - const row = new Float32Array(input.colbertDim); - for (let d = 0; d < input.colbertDim; d++) { - row[d] = (colbert[start + d] * doc.scale) / 127.0; + + // MaxSim: for each query token, find max similarity with doc tokens, sum + let totalScore = 0; + for (let q = 0; q < queryMatrix.length; q++) { + const qRow = queryMatrix[q]; + let maxDot = -Infinity; + + for (let d = 0; d < seqLen; d++) { + let dot = 0; + for (let k = 0; k < input.colbertDim; k++) { + // Dequantize INT8 back to float + const docVal = (colbert[d * input.colbertDim + k] * doc.scale) / 127; + dot += qRow[k] * docVal; + } + if (dot > maxDot) maxDot = dot; + } + + if (maxDot > -Infinity) { + totalScore += maxDot; } - docMatrix.push(row); } - const tokenIds = - Array.isArray(doc.token_ids) && doc.token_ids.length === seqLen - ? doc.token_ids - : undefined; - return maxSim(queryMatrix, docMatrix, tokenIds); + + return totalScore; }); } } + +// ============================================================================= +// Singleton for direct use (no worker pool needed) +// ============================================================================= + +let orchestrator: WorkerOrchestrator | null = null; + +export function getOrchestrator(): WorkerOrchestrator { + if (!orchestrator) { + orchestrator = new WorkerOrchestrator(); + } + return orchestrator; +} + +export async function processFile( + input: ProcessFileInput +): Promise<ProcessFileResult> { + return getOrchestrator().processFile(input); +} + +export async function encodeQuery(text: string) { + return getOrchestrator().encodeQuery(text); +} + +export async function rerank(input: { + query: number[][]; + docs: RerankDoc[]; + colbertDim: number; +}) { + return getOrchestrator().rerank(input); +} diff --git a/src/lib/workers/pool.ts b/src/lib/workers/pool.ts deleted file mode 100644 index 0216464b..00000000 --- a/src/lib/workers/pool.ts +++ /dev/null @@ -1,450 +0,0 @@ -/** - * Architecture Note: We use a custom Child Process pool instead of Worker Threads - * to ensure the ONNX Runtime segfaults do not crash the main process. - */ -import * as childProcess from "node:child_process"; -import * as fs from "node:fs"; -import * as path from "node:path"; -import { CONFIG, WORKER_TIMEOUT_MS } from "../../config"; -import type { ProcessFileInput, ProcessFileResult, RerankDoc } from "./worker"; - -type TaskMethod = "processFile" | "encodeQuery" | "rerank"; - -type EncodeQueryResult = Awaited< - ReturnType<typeof import("./worker")["encodeQuery"]> ->; -type RerankResult = Awaited<ReturnType<typeof import("./worker")["rerank"]>>; - -type TaskPayloads = { - processFile: ProcessFileInput; - encodeQuery: { text: string }; - rerank: { query: number[][]; docs: RerankDoc[]; colbertDim: number }; -}; - -type TaskResults = { - processFile: ProcessFileResult; - encodeQuery: EncodeQueryResult; - rerank: RerankResult; -}; - -type WorkerMessage = - | { id: number; result: TaskResults[TaskMethod] } - | { id: number; error: string } - | { id: number; heartbeat: true }; - -function reviveBufferLike(input: unknown): Buffer | Int8Array | unknown { - if ( - input && - typeof input === "object" && - "type" in (input as Record<string, unknown>) && - (input as Record<string, unknown>).type === "Buffer" && - Array.isArray((input as Record<string, unknown>).data) - ) { - return Buffer.from((input as Record<string, unknown>).data as number[]); - } - return input; -} - -function reviveProcessFileResult( - result: TaskResults["processFile"], -): TaskResults["processFile"] { - if (!result || !Array.isArray(result.vectors)) return result; - const vectors = result.vectors.map((v) => { - const revived = reviveBufferLike(v.colbert); - return revived && (Buffer.isBuffer(revived) || revived instanceof Int8Array) - ? { ...v, colbert: revived } - : v; - }); - return { ...result, vectors }; -} - -type PendingTask<M extends TaskMethod = TaskMethod> = { - id: number; - method: M; - payload: TaskPayloads[M]; - resolve: (value: TaskResults[M]) => void; - reject: (reason?: unknown) => void; - worker?: ProcessWorker; - timeout?: NodeJS.Timeout; -}; - -const TASK_TIMEOUT_MS = (() => { - const fromEnv = Number.parseInt( - process.env.OSGREP_WORKER_TASK_TIMEOUT_MS ?? "", - 10, - ); - if (Number.isFinite(fromEnv) && fromEnv > 0) return fromEnv; - return 120_000; -})(); - -const FORCE_KILL_GRACE_MS = 200; - -class ProcessWorker { - child: childProcess.ChildProcess; - busy = false; - pendingTaskId: number | null = null; - - constructor( - public modulePath: string, - public execArgv: string[], - ) { - this.child = childProcess.fork(modulePath, { - execArgv, - env: { ...process.env }, - }); - } -} - -function resolveProcessWorker(): { filename: string; execArgv: string[] } { - const jsWorker = path.join(__dirname, "process-child.js"); - const tsWorker = path.join(__dirname, "process-child.ts"); - - if (fs.existsSync(jsWorker)) { - return { filename: jsWorker, execArgv: [] }; - } - - if (fs.existsSync(tsWorker)) { - return { filename: tsWorker, execArgv: ["-r", "ts-node/register"] }; - } - - throw new Error("Process worker file not found"); -} - -export class WorkerPool { - private workers: ProcessWorker[] = []; - private taskQueue: number[] = []; - private tasks = new Map<number, PendingTask<TaskMethod>>(); - private nextId = 1; - private destroyed = false; - private destroyPromise: Promise<void> | null = null; - private readonly modulePath: string; - private readonly execArgv: string[]; - - constructor() { - const resolved = resolveProcessWorker(); - this.modulePath = resolved.filename; - this.execArgv = resolved.execArgv; - - const workerCount = Math.max(1, CONFIG.WORKER_THREADS); - for (let i = 0; i < workerCount; i++) { - this.spawnWorker(); - } - } - - private clearTaskTimeout<M extends TaskMethod>(task: PendingTask<M>) { - if (task.timeout) { - clearTimeout(task.timeout); - task.timeout = undefined; - } - } - - private removeFromQueue(taskId: number) { - const idx = this.taskQueue.indexOf(taskId); - if (idx !== -1) this.taskQueue.splice(idx, 1); - } - - private completeTask<M extends TaskMethod>( - task: PendingTask<M>, - worker: ProcessWorker | null, - ) { - this.clearTaskTimeout(task); - this.tasks.delete(task.id); - this.removeFromQueue(task.id); - - if (worker) { - worker.busy = false; - worker.pendingTaskId = null; - } - } - - private handleWorkerExit( - worker: ProcessWorker, - code: number | null, - signal: NodeJS.Signals | null, - ) { - worker.busy = false; - const failedTasks = Array.from(this.tasks.values()).filter( - (t) => t.worker === worker, - ); - for (const task of failedTasks) { - this.clearTaskTimeout(task); - task.reject( - new Error( - `Worker exited unexpectedly${code ? ` (code ${code})` : ""}${ - signal ? ` signal ${signal}` : "" - }`, - ), - ); - this.completeTask(task, null); - } - - this.workers = this.workers.filter((w) => w !== worker); - if (!this.destroyed) { - this.spawnWorker(); - this.dispatch(); - } - } - - private spawnWorker() { - const worker = new ProcessWorker(this.modulePath, this.execArgv); - - const onMessage = (msg: WorkerMessage) => { - const task = this.tasks.get(msg.id); - if (!task) return; - - if ("heartbeat" in msg) { - // Reset timeout - this.clearTaskTimeout(task); - if (task.worker) { - task.timeout = setTimeout( - () => this.handleTaskTimeout(task, task.worker!), - TASK_TIMEOUT_MS, - ); - } - return; - } - - if ("error" in msg) { - task.reject(new Error(msg.error)); - } else { - let result = msg.result as TaskResults[TaskMethod]; - if (task.method === "processFile") { - result = reviveProcessFileResult( - result as TaskResults["processFile"], - ) as TaskResults[TaskMethod]; - } - task.resolve(result); - } - - this.completeTask(task, worker); - this.dispatch(); - }; - - const onExit = (code: number | null, signal: NodeJS.Signals | null) => - this.handleWorkerExit(worker, code, signal); - - worker.child.on("message", onMessage); - worker.child.on("exit", onExit); - this.workers.push(worker); - } - - private enqueue<M extends TaskMethod>( - method: M, - payload: TaskPayloads[M], - signal?: AbortSignal, - ): Promise<TaskResults[M]> { - if (this.destroyed) { - return Promise.reject(new Error("Worker pool destroyed")); - } - if (signal?.aborted) { - const err = new Error("Aborted"); - err.name = "AbortError"; - return Promise.reject(err); - } - - const id = this.nextId++; - return new Promise((resolve, reject) => { - let settled = false; - const safeResolve = (val: TaskResults[M]) => { - if (!settled) { - settled = true; - resolve(val); - } - }; - const safeReject = (reason?: unknown) => { - if (!settled) { - settled = true; - reject(reason); - } - }; - - const task: PendingTask<M> = { - id, - method, - payload, - resolve: safeResolve, - reject: safeReject, - }; - - if (signal) { - signal.addEventListener( - "abort", - () => { - // If task is still in queue, remove it - const idx = this.taskQueue.indexOf(id); - if (idx !== -1) { - this.taskQueue.splice(idx, 1); - this.tasks.delete(id); - const err = new Error("Aborted"); - err.name = "AbortError"; - safeReject(err); - } - // If task is already running (assigned to worker), we can't easily kill it without - // killing the worker. For now, we just let it finish but reject the promise early so - // the caller doesn't wait. The worker will eventually finish and we'll ignore the result. - else if (this.tasks.has(id)) { - // Task is running. Reject caller immediately. - const err = new Error("Aborted"); - err.name = "AbortError"; - safeReject(err); - // We intentionally do NOT delete the task map entry here, - // because we need handleWorkerMessage to cleanly cleanup the worker state - // when it eventually finishes. - } - }, - { once: true }, - ); - } - - this.tasks.set(id, task as unknown as PendingTask<TaskMethod>); - this.taskQueue.push(id); - this.dispatch(); - }); - } - - private handleTaskTimeout<M extends TaskMethod>( - task: PendingTask<M>, - worker: ProcessWorker, - ) { - if (this.destroyed || !this.tasks.has(task.id)) return; - - this.clearTaskTimeout(task); - if (task.method !== "processFile") { - console.warn( - `[worker-pool] ${task.method} timed out after ${TASK_TIMEOUT_MS}ms; restarting worker.`, - ); - } - this.completeTask(task, null); - task.reject( - new Error( - `Worker task ${task.method} timed out after ${TASK_TIMEOUT_MS}ms`, - ), - ); - - worker.child.removeAllListeners("message"); - worker.child.removeAllListeners("exit"); - try { - worker.child.kill("SIGKILL"); - } catch {} - - this.workers = this.workers.filter((w) => w !== worker); - if (!this.destroyed) { - this.spawnWorker(); - } - this.dispatch(); - } - - private dispatch() { - if (this.destroyed) return; - const idle = this.workers.find((w) => !w.busy); - const nextTaskId = this.taskQueue.find((id) => { - const t = this.tasks.get(id); - return t && !t.worker; - }); - - if (!idle || nextTaskId === undefined) return; - const task = this.tasks.get(nextTaskId); - if (!task) { - this.removeFromQueue(nextTaskId); - this.dispatch(); - return; - } - - idle.busy = true; - idle.pendingTaskId = task.id; - task.worker = idle; - - task.timeout = setTimeout( - () => this.handleTaskTimeout(task, idle), - TASK_TIMEOUT_MS, - ); - - try { - idle.child.send({ - id: task.id, - method: task.method, - payload: task.payload, - }); - } catch (err) { - this.clearTaskTimeout(task); - this.completeTask(task, idle); - task.reject(err); - return; - } - - this.dispatch(); - } - - processFile(input: ProcessFileInput) { - // ProcessFile doesn't currently use cancellation, but we could add it later - return this.enqueue("processFile", input); - } - - encodeQuery(text: string, signal?: AbortSignal) { - return this.enqueue("encodeQuery", { text }, signal); - } - - rerank(input: TaskPayloads["rerank"], signal?: AbortSignal) { - return this.enqueue("rerank", input, signal); - } - - async destroy(): Promise<void> { - if (this.destroyPromise) return this.destroyPromise; - if (this.destroyed) return; - - this.destroyed = true; - - for (const task of this.tasks.values()) { - this.clearTaskTimeout(task); - task.reject(new Error("Worker pool destroyed")); - } - this.tasks.clear(); - this.taskQueue = []; - - const killPromises = this.workers.map( - (w) => - new Promise<void>((resolve) => { - w.child.removeAllListeners("message"); - w.child.removeAllListeners("exit"); - w.child.once("exit", () => resolve()); - w.child.kill("SIGTERM"); - const force = setTimeout(() => { - try { - w.child.kill("SIGKILL"); - } catch {} - }, FORCE_KILL_GRACE_MS); - setTimeout(() => { - clearTimeout(force); - resolve(); - }, WORKER_TIMEOUT_MS); - }), - ); - - this.destroyPromise = Promise.allSettled(killPromises).then(() => { - this.workers = []; - this.destroyPromise = null; - }); - - await this.destroyPromise; - } -} - -let singleton: WorkerPool | null = null; - -export function getWorkerPool(): WorkerPool { - if (!singleton) { - singleton = new WorkerPool(); - } - return singleton; -} - -export async function destroyWorkerPool(): Promise<void> { - if (!singleton) return; - const pool = singleton; - singleton = null; - await pool.destroy(); -} - -export function isWorkerPoolInitialized(): boolean { - return singleton !== null; -} diff --git a/src/lib/workers/process-child.ts b/src/lib/workers/process-child.ts deleted file mode 100644 index 25e5ccd1..00000000 --- a/src/lib/workers/process-child.ts +++ /dev/null @@ -1,70 +0,0 @@ -import process from "node:process"; -import processFile, { - encodeQuery, - type ProcessFileInput, - type ProcessFileResult, - type RerankDoc, - rerank, -} from "./worker"; - -type IncomingMessage = - | { id: number; method: "processFile"; payload: ProcessFileInput } - | { id: number; method: "encodeQuery"; payload: { text: string } } - | { - id: number; - method: "rerank"; - payload: { query: number[][]; docs: RerankDoc[]; colbertDim: number }; - }; - -type OutgoingMessage = - | { id: number; result: ProcessFileResult } - | { id: number; result: Awaited<ReturnType<typeof encodeQuery>> } - | { id: number; result: Awaited<ReturnType<typeof rerank>> } - | { id: number; error: string } - | { id: number; heartbeat: true }; - -const send = (msg: OutgoingMessage) => { - if (process.send) { - process.send(msg); - } -}; - -process.on("message", async (msg: IncomingMessage) => { - const { id, method, payload } = msg; - try { - if (method === "processFile") { - const onProgress = () => { - send({ id, heartbeat: true }); - }; - const result = await processFile(payload, onProgress); - send({ id, result }); - return; - } - if (method === "encodeQuery") { - const result = await encodeQuery(payload); - send({ id, result }); - return; - } - if (method === "rerank") { - const result = await rerank(payload); - send({ id, result }); - return; - } - send({ id, error: `Unknown method: ${method}` }); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - send({ id, error: message }); - } -}); - -process.on("uncaughtException", (err) => { - console.error("[process-worker] uncaughtException", err); - process.exitCode = 1; - process.exit(); -}); - -process.on("unhandledRejection", (reason) => { - console.error("[process-worker] unhandledRejection", reason); - process.exitCode = 1; - process.exit(); -}); diff --git a/src/lib/workers/worker.ts b/src/lib/workers/worker.ts deleted file mode 100644 index 65f284de..00000000 --- a/src/lib/workers/worker.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { - type ProcessFileInput, - type ProcessFileResult, - type RerankDoc, - WorkerOrchestrator, -} from "./orchestrator"; - -export type { ProcessFileInput, ProcessFileResult, RerankDoc }; - -const orchestrator = new WorkerOrchestrator(); - -export default async function processFile( - input: ProcessFileInput, - onProgress?: () => void, -): Promise<ProcessFileResult> { - return orchestrator.processFile(input, onProgress); -} - -export async function encodeQuery(input: { text: string }) { - return orchestrator.encodeQuery(input.text); -} - -export async function rerank(input: { - query: number[][]; - docs: RerankDoc[]; - colbertDim: number; -}) { - return orchestrator.rerank(input); -} From 30ffe4dd50382bbe7db5eef91caae1ceada02a53 Mon Sep 17 00:00:00 2001 From: Ryan D'Onofrio <97113725+Ryandonofrio3@users.noreply.github.com> Date: Sat, 13 Dec 2025 18:05:17 -0800 Subject: [PATCH 02/19] second commit --- src/config.ts | 1 - src/lib/search/searcher.ts | 5 -- src/lib/setup/setup-helpers.ts | 4 +- src/lib/store/types.ts | 3 - src/lib/store/vector-db.ts | 24 -------- src/lib/workers/orchestrator.ts | 105 +++++++++++++++----------------- {src => tools}/eval.ts | 16 ++--- 7 files changed, 59 insertions(+), 99 deletions(-) rename {src => tools}/eval.ts (98%) diff --git a/src/config.ts b/src/config.ts index 96404bcd..511cb697 100644 --- a/src/config.ts +++ b/src/config.ts @@ -30,7 +30,6 @@ const GLOBAL_ROOT = path.join(HOME, ".osgrep"); export const PATHS = { globalRoot: GLOBAL_ROOT, - models: path.join(GLOBAL_ROOT, "models"), grammars: path.join(GLOBAL_ROOT, "grammars"), }; diff --git a/src/lib/search/searcher.ts b/src/lib/search/searcher.ts index 1008219e..b664fe75 100644 --- a/src/lib/search/searcher.ts +++ b/src/lib/search/searcher.ts @@ -356,11 +356,6 @@ export class Searcher { query: queryMatrixRaw, docs: rerankCandidates.map((doc) => ({ colbert: (doc.colbert as Buffer | Int8Array | number[]) ?? [], - scale: - typeof doc.colbert_scale === "number" ? doc.colbert_scale : 1, - token_ids: Array.isArray((doc as any).doc_token_ids) - ? ((doc as any).doc_token_ids as number[]) - : undefined, })), colbertDim, }) diff --git a/src/lib/setup/setup-helpers.ts b/src/lib/setup/setup-helpers.ts index b344d75b..c5e26e8e 100644 --- a/src/lib/setup/setup-helpers.ts +++ b/src/lib/setup/setup-helpers.ts @@ -4,7 +4,6 @@ import { PATHS } from "../../config"; export interface SetupPaths { root: string; - models: string; grammars: string; } @@ -15,7 +14,6 @@ export interface SetupStatus extends SetupPaths { function getPaths(): SetupPaths { return { root: PATHS.globalRoot, - models: PATHS.models, grammars: PATHS.grammars, }; } @@ -30,7 +28,7 @@ export async function ensureSetup({ silent?: boolean; } = {}): Promise<SetupStatus> { const paths = getPaths(); - const dirs = [paths.root, paths.models, paths.grammars]; + const dirs = [paths.root, paths.grammars]; const needsDirs = dirs.some((dir) => !fs.existsSync(dir)); let createdDirs = false; diff --git a/src/lib/store/types.ts b/src/lib/store/types.ts index a22b807c..a7847eeb 100644 --- a/src/lib/store/types.ts +++ b/src/lib/store/types.ts @@ -31,9 +31,6 @@ export type PreparedChunk = { export type VectorRecord = PreparedChunk & { vector: Float32Array | number[]; colbert: Int8Array | Buffer | number[]; - colbert_scale: number; - pooled_colbert_48d?: Float32Array | number[]; - doc_token_ids?: number[] | Int32Array; } & Record<string, unknown>; export interface FileMetadata extends MetadataRecord { diff --git a/src/lib/store/vector-db.ts b/src/lib/store/vector-db.ts index 0c940ef9..ea10506c 100644 --- a/src/lib/store/vector-db.ts +++ b/src/lib/store/vector-db.ts @@ -6,7 +6,6 @@ import { Field, FixedSizeList, Float32, - Float64, Int32, List, Schema, @@ -56,9 +55,6 @@ export class VectorDB { is_exported: false, vector: Array(CONFIG.VECTOR_DIM).fill(0), colbert: Buffer.alloc(0), - colbert_scale: 1, - pooled_colbert_48d: Array(CONFIG.COLBERT_DIM).fill(0), - doc_token_ids: [], defined_symbols: [], referenced_symbols: [], imports: [], @@ -108,20 +104,6 @@ export class VectorDB { new Field("complexity", new Float32(), true), new Field("is_exported", new Bool(), true), new Field("colbert", new Binary(), true), - new Field("colbert_scale", new Float64(), true), - new Field( - "pooled_colbert_48d", - new FixedSizeList( - CONFIG.COLBERT_DIM, - new Field("item", new Float32(), false), - ), - true, - ), - new Field( - "doc_token_ids", - new List(new Field("item", new Int32(), true)), - true, - ), new Field( "defined_symbols", new List(new Field("item", new Utf8(), true)), @@ -232,12 +214,6 @@ export class VectorDB { is_exported: rec.is_exported ?? false, vector: vec, colbert: toBuffer(rec.colbert), - colbert_scale: - typeof rec.colbert_scale === "number" ? rec.colbert_scale : 1, - pooled_colbert_48d: rec.pooled_colbert_48d - ? Array.from(rec.pooled_colbert_48d) - : undefined, - doc_token_ids: rec.doc_token_ids ? Array.from(rec.doc_token_ids) : null, defined_symbols: rec.defined_symbols ?? [], referenced_symbols: rec.referenced_symbols ?? [], imports: rec.imports ?? [], diff --git a/src/lib/workers/orchestrator.ts b/src/lib/workers/orchestrator.ts index 3dc9d95f..51657489 100644 --- a/src/lib/workers/orchestrator.ts +++ b/src/lib/workers/orchestrator.ts @@ -18,7 +18,7 @@ import { formatChunkText, TreeSitterChunker, } from "../index/chunker"; -import { embedBatch, initNative, encodeQueryColbert } from "../native"; +import { embedBatch, initNative, encodeQueryColbert, rerankColbert } from "../native"; import type { PreparedChunk, VectorRecord } from "../store/types"; import { computeBufferHash, @@ -46,8 +46,6 @@ export type ProcessFileResult = { export type RerankDoc = { colbert: Buffer | Int8Array | number[]; - scale: number; - token_ids?: number[]; }; // ============================================================================= @@ -194,9 +192,6 @@ export class WorkerOrchestrator { ...chunk, vector: hybrid.dense, colbert: Buffer.from(hybrid.colbert), - colbert_scale: 1, // Native returns pre-scaled INT8 - pooled_colbert_48d: undefined, // Can compute if needed - doc_token_ids: undefined, }; }); @@ -208,7 +203,6 @@ export class WorkerOrchestrator { dense: number[]; colbert: number[][]; colbertDim: number; - pooled_colbert_48d?: number[]; }> { await this.ensureReady(); @@ -231,31 +225,10 @@ export class WorkerOrchestrator { matrix.push(row); } - // Compute pooled embedding (mean of tokens) - const pooled = new Float32Array(dim); - for (const row of matrix) { - for (let d = 0; d < dim; d++) { - pooled[d] += row[d]; - } - } - // Normalize - let sumSq = 0; - for (let d = 0; d < dim; d++) { - pooled[d] /= matrix.length || 1; - sumSq += pooled[d] * pooled[d]; - } - const norm = Math.sqrt(sumSq); - if (norm > 1e-9) { - for (let d = 0; d < dim; d++) { - pooled[d] /= norm; - } - } - return { dense: Array.from(denseVector), colbert: matrix, colbertDim: dim, - pooled_colbert_48d: Array.from(pooled), }; } @@ -266,15 +239,27 @@ export class WorkerOrchestrator { }): Promise<number[]> { await this.ensureReady(); - // MaxSim scoring in TypeScript (simple, matches Rust behavior) - const queryMatrix = input.query.map((row) => - row instanceof Float32Array ? row : new Float32Array(row) - ); + // Flatten query matrix to match native `rerankColbert` signature + const queryEmbedding: number[] = []; + for (const row of input.query) { + for (let i = 0; i < row.length; i++) { + queryEmbedding.push(row[i] ?? 0); + } + } - return input.docs.map((doc) => { + const docLengths: number[] = []; + const docOffsets: number[] = []; + const candidateIndices: number[] = []; + + // Pack all doc embeddings into a single buffer; offsets are element offsets + const packedChunks: Int8Array[] = []; + let totalElements = 0; + + for (let i = 0; i < input.docs.length; i++) { + const doc = input.docs[i]; const col = doc.colbert; - let colbert: Int8Array; + let colbert: Int8Array; if (col instanceof Int8Array) { colbert = col; } else if (Buffer.isBuffer(col)) { @@ -286,30 +271,40 @@ export class WorkerOrchestrator { } const seqLen = Math.floor(colbert.length / input.colbertDim); + const used = colbert.subarray(0, seqLen * input.colbertDim); - // MaxSim: for each query token, find max similarity with doc tokens, sum - let totalScore = 0; - for (let q = 0; q < queryMatrix.length; q++) { - const qRow = queryMatrix[q]; - let maxDot = -Infinity; - - for (let d = 0; d < seqLen; d++) { - let dot = 0; - for (let k = 0; k < input.colbertDim; k++) { - // Dequantize INT8 back to float - const docVal = (colbert[d * input.colbertDim + k] * doc.scale) / 127; - dot += qRow[k] * docVal; - } - if (dot > maxDot) maxDot = dot; - } - - if (maxDot > -Infinity) { - totalScore += maxDot; - } - } + docOffsets.push(totalElements); + docLengths.push(seqLen); + candidateIndices.push(i); + packedChunks.push(used); + totalElements += used.length; + } - return totalScore; + const packed = new Int8Array(totalElements); + let cursor = 0; + for (const chunk of packedChunks) { + packed.set(chunk, cursor); + cursor += chunk.length; + } + + const result = await rerankColbert({ + queryEmbedding: new Float32Array(queryEmbedding), + docEmbeddings: packed, + docLengths, + docOffsets, + candidateIndices, + topK: input.docs.length, }); + + const scoreByIndex = new Map<number, number>(); + for (let i = 0; i < result.indices.length; i++) { + const idx = result.indices[i] ?? -1; + const score = result.scores[i] ?? 0; + if (typeof idx === "number") scoreByIndex.set(idx, score); + } + + // Return scores aligned to input order (Searcher expects this) + return candidateIndices.map((i) => scoreByIndex.get(i) ?? 0); } } diff --git a/src/eval.ts b/tools/eval.ts similarity index 98% rename from src/eval.ts rename to tools/eval.ts index 82a3d875..3644efdd 100644 --- a/src/eval.ts +++ b/tools/eval.ts @@ -1,11 +1,11 @@ -// Reduce worker pool fan-out during eval to avoid ONNX concurrency issues -process.env.OSGREP_WORKER_COUNT ??= "1"; - -import { Searcher } from "./lib/search/searcher"; -import type { SearchResponse } from "./lib/store/types"; -import { VectorDB } from "./lib/store/vector-db"; -import { gracefulExit } from "./lib/utils/exit"; -import { ensureProjectPaths, findProjectRoot } from "./lib/utils/project-root"; +import { Searcher } from "../src/lib/search/searcher"; +import type { SearchResponse } from "../src/lib/store/types"; +import { VectorDB } from "../src/lib/store/vector-db"; +import { gracefulExit } from "../src/lib/utils/exit"; +import { + ensureProjectPaths, + findProjectRoot, +} from "../src/lib/utils/project-root"; export type EvalCase = { query: string; From 7fabb4f6202281f9de7c945452ec14339a181009 Mon Sep 17 00:00:00 2001 From: Ryan D'Onofrio <97113725+Ryandonofrio3@users.noreply.github.com> Date: Sat, 13 Dec 2025 18:42:05 -0800 Subject: [PATCH 03/19] third commit --- src/commands/search.ts | 11 +++-- src/lib/native/index.ts | 27 ++++++++++- src/lib/search/searcher.ts | 74 ++++++++++++++++++++++++++++- src/lib/store/types.ts | 1 + src/lib/store/vector-db.ts | 7 +++ src/lib/workers/orchestrator.ts | 19 ++++++++ tools/eval.ts | 82 +++++++++++---------------------- 7 files changed, 157 insertions(+), 64 deletions(-) diff --git a/src/commands/search.ts b/src/commands/search.ts index 09d93ffd..d4049aaa 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -262,7 +262,7 @@ function formatCompactTable( export const search: Command = new CommanderCommand("search") .description("File pattern searcher") .option( - "-m <max_count>, --max-count <max_count>", + "-m, --max-count <max_count>", "The maximum number of results to return (total)", "5", ) @@ -297,7 +297,7 @@ export const search: Command = new CommanderCommand("search") .allowExcessArguments(true) .action(async (pattern, exec_path, _options, cmd) => { const options: { - m: string; + maxCount: string; content: boolean; perFile: string; scores: boolean; @@ -313,6 +313,9 @@ export const search: Command = new CommanderCommand("search") } const root = process.cwd(); + const limitRaw = Number.parseInt(options.maxCount, 10); + const limit = + Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : 5; const minScore = Number.isFinite(Number.parseFloat(options.minScore)) ? Number.parseFloat(options.minScore) : 0; @@ -331,7 +334,7 @@ export const search: Command = new CommanderCommand("search") headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query: pattern, - limit: parseInt(options.m, 10), + limit, path: exec_path ? path.relative(projectRootForServer, path.resolve(exec_path)) : undefined, @@ -488,7 +491,7 @@ export const search: Command = new CommanderCommand("search") const searchResult = await searcher.search( pattern, - parseInt(options.m, 10), + limit, { rerank: true }, undefined, exec_path ? path.relative(projectRoot, path.resolve(exec_path)) : "", diff --git a/src/lib/native/index.ts b/src/lib/native/index.ts index e007f230..48718923 100644 --- a/src/lib/native/index.ts +++ b/src/lib/native/index.ts @@ -127,6 +127,7 @@ export async function embedColbert(texts: string[]): Promise<ColbertPacked> { export interface HybridEmbedding { dense: Float32Array; colbert: Int8Array; + token_ids: Uint32Array; colbertLength: number; colbertOffset: number; } @@ -144,6 +145,8 @@ export async function embedBatch(texts: string[]): Promise<HybridEmbedding[]> { const colbertDim = CONFIG.COLBERT_DIM; const embeddings: HybridEmbedding[] = []; + const tokenIds = new Uint32Array(result.colbertTokenIds); + let tokenCursor = 0; for (let i = 0; i < texts.length; i++) { // Extract dense vector @@ -162,9 +165,14 @@ export async function embedBatch(texts: string[]): Promise<HybridEmbedding[]> { colbert[j] = result.colbertEmbeddings[colbertOffset + j]; } + // Extract token IDs for this doc (length = colbertLength) + const tokenSlice = tokenIds.subarray(tokenCursor, tokenCursor + colbertLength); + tokenCursor += colbertLength; + embeddings.push({ dense, colbert, + token_ids: new Uint32Array(tokenSlice), colbertLength, colbertOffset: 0, // Will be set when storing }); @@ -198,6 +206,8 @@ export interface RerankInput { queryEmbedding: Float32Array; /** Packed ColBERT doc embeddings (INT8) */ docEmbeddings: Int8Array; + /** Packed ColBERT doc token ids (UINT32) aligned to docEmbeddings */ + docTokenIds: Uint32Array; /** Token counts per doc */ docLengths: number[]; /** Byte offsets per doc */ @@ -222,9 +232,22 @@ export async function rerankColbert(input: RerankInput): Promise<RerankResult> { await initNative(); const n = await loadNative(); + const q = Float64Array.from(input.queryEmbedding as any); + + const docs = + input.docEmbeddings instanceof Int8Array + ? input.docEmbeddings + : new Int8Array(input.docEmbeddings as any); + + const tokenIds = + input.docTokenIds instanceof Uint32Array + ? input.docTokenIds + : Uint32Array.from(input.docTokenIds as any); + const result = n.rerankColbert( - Array.from(input.queryEmbedding), - Array.from(input.docEmbeddings), + q, + docs, + tokenIds, input.docLengths, input.docOffsets, input.candidateIndices, diff --git a/src/lib/search/searcher.ts b/src/lib/search/searcher.ts index b664fe75..349434bb 100644 --- a/src/lib/search/searcher.ts +++ b/src/lib/search/searcher.ts @@ -18,6 +18,61 @@ export class Searcher { private static readonly RERANK_CANDIDATES_K = 80; private static readonly FUSED_WEIGHT = 0.5; private static readonly MAX_PER_FILE = 3; + private static readonly FTS_STOPWORDS = new Set([ + "a", + "an", + "and", + "are", + "as", + "at", + "be", + "before", + "but", + "by", + "do", + "does", + "for", + "from", + "how", + "i", + "if", + "in", + "into", + "is", + "it", + "of", + "on", + "or", + "our", + "that", + "the", + "their", + "then", + "this", + "to", + "we", + "what", + "when", + "where", + "which", + "who", + "why", + "with", + ]); + + private static normalizeFtsQuery(query: string): string { + const tokens = query + .toLowerCase() + .replace(/[^a-z0-9_]+/g, " ") + .split(/\s+/g) + .map((t) => t.trim()) + .filter(Boolean) + .filter((t) => t.length >= 3) + .filter((t) => !Searcher.FTS_STOPWORDS.has(t)); + + // Keep the query short to avoid pathological FTS parsing / scoring. + return tokens.slice(0, 16).join(" "); + } private mapRecordToChunk( record: Partial<VectorRecord>, @@ -158,6 +213,11 @@ export class Searcher { if (isTestPath) { adjusted *= 0.5; } + // Tooling/docs-like paths often contain lots of "how do we..." text and can + // dominate semantic queries (e.g., `tools/eval.ts`). + if (/(^|\/)(tools|scripts|experiments)(\/|$)/i.test(pathStr)) { + adjusted *= 0.35; + } if ( pathStr.endsWith(".md") || pathStr.endsWith(".json") || @@ -223,7 +283,9 @@ export class Searcher { pathPrefix?: string, signal?: AbortSignal, ): Promise<SearchResponse> { - const finalLimit = top_k ?? 10; + const finalLimitRaw = top_k ?? 10; + const finalLimit = + Number.isFinite(finalLimitRaw) && finalLimitRaw > 0 ? finalLimitRaw : 10; const doRerank = options?.rerank ?? true; if (signal?.aborted) { @@ -305,7 +367,9 @@ export class Searcher { let ftsResults: VectorRecord[] = []; try { - let ftsQuery = table.search(query).limit(PRE_RERANK_K); + const ftsText = Searcher.normalizeFtsQuery(query); + if (!ftsText) throw new Error("Empty FTS query after normalization"); + let ftsQuery = table.search(ftsText).limit(PRE_RERANK_K); if (whereClause) { ftsQuery = ftsQuery.where(whereClause); } @@ -356,6 +420,12 @@ export class Searcher { query: queryMatrixRaw, docs: rerankCandidates.map((doc) => ({ colbert: (doc.colbert as Buffer | Int8Array | number[]) ?? [], + token_ids: Array.isArray((doc as any).doc_token_ids) + ? ((doc as any).doc_token_ids as number[]) + : typeof (doc as any).doc_token_ids?.toArray === "function" + ? (((doc as any).doc_token_ids.toArray() as unknown[]) ?? []) + .filter((v) => typeof v === "number") as number[] + : [], })), colbertDim, }) diff --git a/src/lib/store/types.ts b/src/lib/store/types.ts index a7847eeb..76ea4f0a 100644 --- a/src/lib/store/types.ts +++ b/src/lib/store/types.ts @@ -31,6 +31,7 @@ export type PreparedChunk = { export type VectorRecord = PreparedChunk & { vector: Float32Array | number[]; colbert: Int8Array | Buffer | number[]; + doc_token_ids?: number[] | Int32Array; } & Record<string, unknown>; export interface FileMetadata extends MetadataRecord { diff --git a/src/lib/store/vector-db.ts b/src/lib/store/vector-db.ts index ea10506c..094a65a8 100644 --- a/src/lib/store/vector-db.ts +++ b/src/lib/store/vector-db.ts @@ -55,6 +55,7 @@ export class VectorDB { is_exported: false, vector: Array(CONFIG.VECTOR_DIM).fill(0), colbert: Buffer.alloc(0), + doc_token_ids: [], defined_symbols: [], referenced_symbols: [], imports: [], @@ -104,6 +105,11 @@ export class VectorDB { new Field("complexity", new Float32(), true), new Field("is_exported", new Bool(), true), new Field("colbert", new Binary(), true), + new Field( + "doc_token_ids", + new List(new Field("item", new Int32(), true)), + true, + ), new Field( "defined_symbols", new List(new Field("item", new Utf8(), true)), @@ -214,6 +220,7 @@ export class VectorDB { is_exported: rec.is_exported ?? false, vector: vec, colbert: toBuffer(rec.colbert), + doc_token_ids: rec.doc_token_ids ? Array.from(rec.doc_token_ids) : [], defined_symbols: rec.defined_symbols ?? [], referenced_symbols: rec.referenced_symbols ?? [], imports: rec.imports ?? [], diff --git a/src/lib/workers/orchestrator.ts b/src/lib/workers/orchestrator.ts index 51657489..45ecb5cc 100644 --- a/src/lib/workers/orchestrator.ts +++ b/src/lib/workers/orchestrator.ts @@ -46,6 +46,7 @@ export type ProcessFileResult = { export type RerankDoc = { colbert: Buffer | Int8Array | number[]; + token_ids?: number[]; }; // ============================================================================= @@ -192,6 +193,7 @@ export class WorkerOrchestrator { ...chunk, vector: hybrid.dense, colbert: Buffer.from(hybrid.colbert), + doc_token_ids: Array.from(hybrid.token_ids), }; }); @@ -250,10 +252,12 @@ export class WorkerOrchestrator { const docLengths: number[] = []; const docOffsets: number[] = []; const candidateIndices: number[] = []; + const packedTokenChunks: Uint32Array[] = []; // Pack all doc embeddings into a single buffer; offsets are element offsets const packedChunks: Int8Array[] = []; let totalElements = 0; + let totalTokenIds = 0; for (let i = 0; i < input.docs.length; i++) { const doc = input.docs[i]; @@ -273,11 +277,18 @@ export class WorkerOrchestrator { const seqLen = Math.floor(colbert.length / input.colbertDim); const used = colbert.subarray(0, seqLen * input.colbertDim); + const tokenIdsRaw = doc.token_ids ?? []; + const tokenIds = Uint32Array.from( + tokenIdsRaw.slice(0, seqLen).map((v) => (Number.isFinite(v) ? v : 0)), + ); + docOffsets.push(totalElements); docLengths.push(seqLen); candidateIndices.push(i); packedChunks.push(used); + packedTokenChunks.push(tokenIds); totalElements += used.length; + totalTokenIds += tokenIds.length; } const packed = new Int8Array(totalElements); @@ -287,9 +298,17 @@ export class WorkerOrchestrator { cursor += chunk.length; } + const packedTokenIds = new Uint32Array(totalTokenIds); + let tokenCursor = 0; + for (const chunk of packedTokenChunks) { + packedTokenIds.set(chunk, tokenCursor); + tokenCursor += chunk.length; + } + const result = await rerankColbert({ queryEmbedding: new Float32Array(queryEmbedding), docEmbeddings: packed, + docTokenIds: packedTokenIds, docLengths, docOffsets, candidateIndices, diff --git a/tools/eval.ts b/tools/eval.ts index 3644efdd..20e86def 100644 --- a/tools/eval.ts +++ b/tools/eval.ts @@ -14,6 +14,8 @@ export type EvalCase = { note?: string; }; +const DEFAULT_AVOID_PATH = "tools/eval.ts"; + export type EvalResult = { rr: number; found: boolean; @@ -56,52 +58,21 @@ export const cases: EvalCase[] = [ expectedPath: "src/lib/workers/orchestrator.ts", note: "Worker-side rerank that feeds query/docs into maxSim.", }, + // --- Native (N-API) Core --- { - query: "ColBERT maxSim scoring implementation", - expectedPath: "src/lib/workers/colbert-math.ts", - note: "Summed max dot products between query and doc token grids.", - }, - - // --- Worker Pool & Embeddings --- - { - query: "Why are ONNX workers child processes instead of threads?", - expectedPath: "src/lib/workers/pool.ts", - note: "Process pool choice to isolate runtime crashes.", - }, - { - query: "How do we timeout and restart stuck worker tasks?", - expectedPath: "src/lib/workers/pool.ts", - note: "Task timeout handling that kills and respawns workers.", - }, - { - query: "Which script does the worker pool fork at runtime?", - expectedPath: "src/lib/workers/pool.ts", - note: "resolveProcessWorker chooses process-child entrypoint.", - }, - { - query: "How does worker pool shutdown terminate children?", - expectedPath: "src/lib/workers/pool.ts", - note: "destroy() kills processes with SIGTERM/SIGKILL fallback.", - }, - { - query: "Where are Granite embeddings loaded from onnx cache?", - expectedPath: "src/lib/workers/embeddings/granite.ts", - note: "resolvePaths + load selecting ONNX weights and tokenizer.", - }, - { - query: "How do we mean-pool Granite outputs to 384 dimensions?", - expectedPath: "src/lib/workers/embeddings/granite.ts", - note: "meanPool normalizes and pads vectors to CONFIG.VECTOR_DIM.", + query: "Where is the osgrep-core native binding loaded?", + expectedPath: "src/lib/native/index.ts", + note: "loadNative() dynamic import + friendly error message.", }, { - query: "How does ColBERT quantize token grids to int8 with a scale?", - expectedPath: "src/lib/workers/embeddings/colbert.ts", - note: "runBatch builds int8 arrays and records maxVal scale.", + query: "Where do we initialize the dense and ColBERT models?", + expectedPath: "src/lib/native/index.ts", + note: "initNative() calls initModels() once.", }, { - query: "Where do we compute pooled_colbert_48d summaries?", - expectedPath: "src/lib/workers/embeddings/colbert.ts", - note: "Per-chunk pooled embedding stored alongside dense vectors.", + query: "Where do we call native rerankColbert from JS?", + expectedPath: "src/lib/native/index.ts", + note: "rerankColbert() wrapper converts typed arrays and calls native.", }, { query: "How do we normalize ColBERT query embeddings before rerank?", @@ -299,8 +270,8 @@ export const cases: EvalCase[] = [ }, { query: "Where is TASK_TIMEOUT_MS set for worker tasks?", - expectedPath: "src/lib/workers/pool.ts", - note: "OSGREP_WORKER_TASK_TIMEOUT_MS guarded timeout.", + expectedPath: "src/config.ts", + note: "Worker-related timeout env vars live in config.", }, { query: @@ -315,19 +286,19 @@ export const cases: EvalCase[] = [ }, { query: "Where do we load Granite ONNX with CPU execution providers?", - expectedPath: "src/lib/workers/embeddings/granite.ts", - note: "load() builds sessionOptions for cpu backend.", + expectedPath: "src/lib/native/index.ts", + note: "Inference is performed in the native Rust core via ONNX Runtime.", }, { query: "Where do we limit ColBERT ONNX runtime threads to 1?", - expectedPath: "src/lib/workers/embeddings/colbert.ts", - note: "ONNX_THREADS constant and session options.", + expectedPath: "src/lib/native/index.ts", + note: "Threading is configured inside the native Rust core.", }, { query: "How do we normalize ColBERT doc vectors and quantize to int8 scale?", - expectedPath: "src/lib/workers/embeddings/colbert.ts", - note: "runBatch builds normalized grid and scale factor.", + expectedPath: "src/lib/native/index.ts", + note: "Docs are stored as INT8 ColBERT grids produced by native core.", }, { query: "Where do we normalize ColBERT query rows before building matrix?", @@ -559,14 +530,13 @@ export function evaluateCase( return expectedPaths.some((expected) => path.includes(expected)); }); - const avoidRank = response.data.findIndex((chunk) => - chunk.metadata?.path - ?.toLowerCase() - .includes(evalCase.avoidPath?.toLowerCase() || "_____"), - ); + const avoidPath = (evalCase.avoidPath ?? DEFAULT_AVOID_PATH).toLowerCase(); + const avoidRank = response.data.findIndex((chunk) => { + const path = chunk.metadata?.path?.toLowerCase() || ""; + return avoidPath ? path.includes(avoidPath) : false; + }); - const hitAvoid = - evalCase.avoidPath && avoidRank >= 0 && (rank === -1 || avoidRank < rank); + const hitAvoid = avoidRank >= 0 && (rank === -1 || avoidRank < rank); const found = rank >= 0 && !hitAvoid; const rr = found ? 1 / (rank + 1) : 0; const recall = found && rank < 10 ? 1 : 0; From 1e936d28d49b1de75d6fe1fa2b9a39335ae8f85f Mon Sep 17 00:00:00 2001 From: Ryan D'Onofrio <97113725+Ryandonofrio3@users.noreply.github.com> Date: Sat, 13 Dec 2025 19:18:46 -0800 Subject: [PATCH 04/19] rust add --- .gitignore | 11 +- osgrep-core/.gitignore | 5 + osgrep-core/Cargo.lock | 2754 ++++++++++++++++++++++ osgrep-core/Cargo.toml | 32 + osgrep-core/bench/PARITY.md | 45 + osgrep-core/bench/_util.ts | 219 ++ osgrep-core/bench/codeatlas-system.ts | 511 ++++ osgrep-core/bench/colbert-parity-rust.ts | 121 + osgrep-core/bench/colbert-parity.ts | 188 ++ osgrep-core/bench/dense-ort.ts | 69 + osgrep-core/bench/dense.ts | 68 + osgrep-core/bench/rerank-ort.ts | 86 + osgrep-core/bench/rerank-preindexed.ts | 94 + osgrep-core/bench/rerank.ts | 60 + osgrep-core/build.rs | 5 + osgrep-core/bun.lockb | Bin 0 -> 59922 bytes osgrep-core/index.d.ts | 60 + osgrep-core/index.js | 124 + osgrep-core/package.json | 26 + osgrep-core/src/colbert_ort.rs | 592 +++++ osgrep-core/src/dense_ort.rs | 208 ++ osgrep-core/src/lib.rs | 265 +++ osgrep-core/test.mjs | 42 + osgrep-core/tsconfig.json | 27 + package.json | 2 +- pnpm-lock.yaml | 8 +- src/lib/native/index.ts | 2 +- 27 files changed, 5617 insertions(+), 7 deletions(-) create mode 100644 osgrep-core/.gitignore create mode 100644 osgrep-core/Cargo.lock create mode 100644 osgrep-core/Cargo.toml create mode 100644 osgrep-core/bench/PARITY.md create mode 100644 osgrep-core/bench/_util.ts create mode 100644 osgrep-core/bench/codeatlas-system.ts create mode 100644 osgrep-core/bench/colbert-parity-rust.ts create mode 100644 osgrep-core/bench/colbert-parity.ts create mode 100644 osgrep-core/bench/dense-ort.ts create mode 100644 osgrep-core/bench/dense.ts create mode 100644 osgrep-core/bench/rerank-ort.ts create mode 100644 osgrep-core/bench/rerank-preindexed.ts create mode 100644 osgrep-core/bench/rerank.ts create mode 100644 osgrep-core/build.rs create mode 100755 osgrep-core/bun.lockb create mode 100644 osgrep-core/index.d.ts create mode 100644 osgrep-core/index.js create mode 100644 osgrep-core/package.json create mode 100644 osgrep-core/src/colbert_ort.rs create mode 100644 osgrep-core/src/dense_ort.rs create mode 100644 osgrep-core/src/lib.rs create mode 100644 osgrep-core/test.mjs create mode 100644 osgrep-core/tsconfig.json diff --git a/.gitignore b/.gitignore index 9b85d444..5a8f09e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ dist/ node_modules/ +**/node_modules/ *.tsbuildinfo *.tgz ADVANCED.md @@ -25,4 +26,12 @@ benchmarks/*.log .osgrep TODO.md -AGENTS.md \ No newline at end of file +AGENTS.md + +# Native core (Rust / N-API) build outputs +osgrep-core/target/ +**/target/ +osgrep-core/*.node + +# Optional benchmark dependency (was a nested git repo) +osgrep-core/bench/opencode/ diff --git a/osgrep-core/.gitignore b/osgrep-core/.gitignore new file mode 100644 index 00000000..0b03a24c --- /dev/null +++ b/osgrep-core/.gitignore @@ -0,0 +1,5 @@ +target/ +node_modules/ +*.node +.DS_Store +bench/opencode/ diff --git a/osgrep-core/Cargo.lock b/osgrep-core/Cargo.lock new file mode 100644 index 00000000..cda7c17d --- /dev/null +++ b/osgrep-core/Cargo.lock @@ -0,0 +1,2754 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "serde", + "static_assertions", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.59.0", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dary_heap" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06d2e3287df1c007e74221c49ca10a95d557349e54b3a75dc2fb14712c751f04" +dependencies = [ + "serde", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "esaxx-rs" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" +dependencies = [ + "cc", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hf-hub" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629d8f3bbeda9d148036d6b0de0a3ab947abd08ce90626327fc3547a49d59d97" +dependencies = [ + "dirs", + "futures", + "http", + "indicatif", + "libc", + "log", + "native-tls", + "num_cpus", + "rand", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", + "ureq 2.12.1", + "windows-sys 0.60.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec 1.15.1", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec 1.15.1", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec 1.15.1", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", + "web-time", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.10.0", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "macro_rules_attribute" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520" +dependencies = [ + "macro_rules_attribute-proc_macro", + "paste", +] + +[[package]] +name = "macro_rules_attribute-proc_macro" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "monostate" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3341a273f6c9d5bef1908f17b7267bbab0e95c9bf69a0d4dcf8e9e1b2c76ef67" +dependencies = [ + "monostate-impl", + "serde", + "serde_core", +] + +[[package]] +name = "monostate-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi" +version = "2.16.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3" +dependencies = [ + "bitflags 2.10.0", + "ctor", + "napi-derive", + "napi-sys", + "once_cell", +] + +[[package]] +name = "napi-build" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d376940fd5b723c6893cd1ee3f33abbfd86acb1cd1ec079f3ab04a2a3bc4d3b1" + +[[package]] +name = "napi-derive" +version = "2.16.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c" +dependencies = [ + "cfg-if", + "convert_case", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi-derive-backend" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf" +dependencies = [ + "convert_case", + "once_cell", + "proc-macro2", + "quote", + "regex", + "semver", + "syn", +] + +[[package]] +name = "napi-sys" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" +dependencies = [ + "libloading", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndarray" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags 2.10.0", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ort" +version = "2.0.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa7e49bd669d32d7bc2a15ec540a527e7764aec722a45467814005725bcd721" +dependencies = [ + "ndarray", + "ort-sys", + "smallvec 2.0.0-alpha.10", + "tracing", +] + +[[package]] +name = "ort-sys" +version = "2.0.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2aba9f5c7c479925205799216e7e5d07cc1d4fa76ea8058c60a9a30f6a4e890" +dependencies = [ + "flate2", + "pkg-config", + "sha2", + "tar", + "ureq 3.1.4", +] + +[[package]] +name = "osgrep-core" +version = "0.1.0" +dependencies = [ + "anyhow", + "hf-hub", + "napi", + "napi-build", + "napi-derive", + "once_cell", + "ort", + "serde", + "serde_json", + "tokenizers", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-cond" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964d0cf57a3e7a06e8183d14a8b527195c706b7983549cd5462d5aa3747438f" +dependencies = [ + "either", + "itertools", + "rayon", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "reqwest" +version = "0.12.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smallvec" +version = "2.0.0-alpha.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d44cfb396c3caf6fbfd0ab422af02631b69ddd96d2eff0b0f0724f9024051b" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + +[[package]] +name = "spm_precompiled" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326" +dependencies = [ + "base64 0.13.1", + "nom", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokenizers" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a620b996116a59e184c2fa2dfd8251ea34a36d0a514758c6f966386bd2e03476" +dependencies = [ + "ahash", + "aho-corasick", + "compact_str", + "dary_heap", + "derive_builder", + "esaxx-rs", + "getrandom 0.3.4", + "indicatif", + "itertools", + "log", + "macro_rules_attribute", + "monostate", + "onig", + "paste", + "rand", + "rayon", + "rayon-cond", + "regex", + "regex-syntax", + "serde", + "serde_json", + "spm_precompiled", + "thiserror", + "unicode-normalization-alignments", + "unicode-segmentation", + "unicode_categories", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-normalization-alignments" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f613e4fa046e69818dd287fdc4bc78175ff20331479dab6e1b0f98d57062de" +dependencies = [ + "smallvec 1.15.1", +] + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "native-tls", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "socks", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "ureq" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" +dependencies = [ + "base64 0.22.1", + "der", + "log", + "native-tls", + "percent-encoding", + "rustls-pki-types", + "socks", + "ureq-proto", + "utf-8", + "webpki-root-certs", +] + +[[package]] +name = "ureq-proto" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" +dependencies = [ + "base64 0.22.1", + "http", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee3e3b5f5e80bc89f30ce8d0343bf4e5f12341c51f3e26cbeecbc7c85443e85b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.4", +] + +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/osgrep-core/Cargo.toml b/osgrep-core/Cargo.toml new file mode 100644 index 00000000..228e24e7 --- /dev/null +++ b/osgrep-core/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "osgrep-core" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +# NAPI-RS bindings +napi = { version = "2", default-features = false, features = ["napi4"] } +napi-derive = "2" + +# Utilities +once_cell = "1" +anyhow = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# HuggingFace tokenizers + model hub +tokenizers = "0.21" +hf-hub = "0.4" + +# ONNX Runtime (the only ML backend we need) +ort = { version = "2.0.0-rc.10", default-features = true, features = ["download-binaries"] } + +[build-dependencies] +napi-build = "2" + +[profile.release] +lto = true +opt-level = 3 diff --git a/osgrep-core/bench/PARITY.md b/osgrep-core/bench/PARITY.md new file mode 100644 index 00000000..44840ffa --- /dev/null +++ b/osgrep-core/bench/PARITY.md @@ -0,0 +1,45 @@ +# ColBERT Parity (Rust ORT vs Python ONNX) + +This repo has a small “apples-to-apples” parity harness to check whether the +Rust ORT ColBERT reranker matches a reference Python ONNX MaxSim implementation +on identical queries and identical candidate chunks. + +## 1) Export parity input (Node/Rust) + +From the repo root: + +```bash +source ~/.cargo/env +bun run bench/colbert-parity.ts --repo chi --n 30 --lines 20 --dense-topk 100 --topk 20 --out /tmp/colbert-parity-chi.jsonl +``` + +This writes JSONL with: +- the query text +- the dense candidate set (chunk texts + metadata) +- the Rust reranker’s returned indices/scores for the same candidate set + +## 2) Run Python ONNX scorer on the exported JSONL + +You need paths to: +- `model_int8.onnx` (or `model.onnx`) +- `tokenizer.json` +- optional `skiplist.json` + +If you’re using the same HF Hub model as Rust (`ryandono/osgrep-17m-v1-onnx`), +these should exist in your local HuggingFace cache under: + +`~/.cache/huggingface/hub/models--ryandono--osgrep-17m-v1-onnx/snapshots/<SNAPSHOT>/` + +Run: + +```bash +python3 codeAtlas/python/parity_rust_vs_onnx.py \ + --in /tmp/colbert-parity-chi.jsonl \ + --onnx-model ~/.cache/huggingface/hub/models--ryandono--osgrep-17m-v1-onnx/snapshots/<SNAPSHOT>/onnx/model_int8.onnx \ + --tokenizer-json ~/.cache/huggingface/hub/models--ryandono--osgrep-17m-v1-onnx/snapshots/<SNAPSHOT>/tokenizer.json \ + --skiplist-json ~/.cache/huggingface/hub/models--ryandono--osgrep-17m-v1-onnx/snapshots/<SNAPSHOT>/skiplist.json \ + --topk 20 +``` + +Expected: very high Top-1 match and Top-5 overlap if preprocessing is aligned. + diff --git a/osgrep-core/bench/_util.ts b/osgrep-core/bench/_util.ts new file mode 100644 index 00000000..94d5ed43 --- /dev/null +++ b/osgrep-core/bench/_util.ts @@ -0,0 +1,219 @@ +import { execSync } from "child_process"; +import { existsSync, readdirSync, readFileSync, statSync } from "fs"; +import { join, extname } from "path"; + +/** + * Clone a git repository if it doesn't exist locally + */ +export function cloneRepoIfMissing(repoUrl: string, dir: string): void { + if (existsSync(dir)) { + console.log(`Repository already exists at ${dir}`); + return; + } + console.log(`Cloning ${repoUrl} to ${dir}...`); + execSync(`git clone --depth 1 ${repoUrl} ${dir}`, { stdio: "inherit" }); +} + +/** + * Recursively walk a directory and return all file paths + * Ignores .git, node_modules, dist, target directories + */ +export function walkFiles(dir: string): string[] { + const ignoreDirs = new Set([".git", "node_modules", "dist", "target", ".next", "build", "__pycache__"]); + const results: string[] = []; + + function walk(currentDir: string): void { + const entries = readdirSync(currentDir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = join(currentDir, entry.name); + + if (entry.isDirectory()) { + if (!ignoreDirs.has(entry.name)) { + walk(fullPath); + } + } else if (entry.isFile()) { + results.push(fullPath); + } + } + } + + walk(dir); + return results; +} + +/** + * Check if a file has a code-like extension + */ +export function isCodeFile(filePath: string): boolean { + const codeExtensions = new Set([ + ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", + ".py", ".rs", ".go", ".java", ".c", ".cpp", ".h", ".hpp", + ".rb", ".php", ".swift", ".kt", ".scala", ".cs", + ".vue", ".svelte", ".astro", + ".json", ".yaml", ".yml", ".toml", + ".md", ".mdx", ".txt", + ".sh", ".bash", ".zsh", + ".sql", ".graphql", ".prisma", + ".css", ".scss", ".less", + ".html", ".xml" + ]); + return codeExtensions.has(extname(filePath).toLowerCase()); +} + +/** + * Chunk file content by lines + */ +export function chunkFileByLines(text: string, linesPerChunk: number = 80): string[] { + const lines = text.split("\n"); + const chunks: string[] = []; + + for (let i = 0; i < lines.length; i += linesPerChunk) { + const chunk = lines.slice(i, i + linesPerChunk).join("\n"); + if (chunk.trim().length > 0) { + chunks.push(chunk); + } + } + + return chunks; +} + +/** + * Read a file and return its contents, handling errors gracefully + */ +export function readFileSafe(filePath: string): string | null { + try { + const stat = statSync(filePath); + // Skip files larger than 1MB + if (stat.size > 1024 * 1024) { + return null; + } + return readFileSync(filePath, "utf-8"); + } catch { + return null; + } +} + +/** + * Pick k candidates from chunks (for rerank benchmark) + */ +export function pickCandidates(chunks: string[], k: number): string[] { + return chunks.slice(0, k); +} + +/** + * Load all code chunks from a repository + */ +export function loadRepoChunks(repoDir: string, linesPerChunk: number = 80): string[] { + const files = walkFiles(repoDir); + const codeFiles = files.filter(isCodeFile); + const allChunks: string[] = []; + + for (const file of codeFiles) { + const content = readFileSafe(file); + if (content) { + const chunks = chunkFileByLines(content, linesPerChunk); + allChunks.push(...chunks); + } + } + + return allChunks; +} + +/** + * Chunk metadata for tracking file/line mapping + */ +export interface ChunkMeta { + id: number; + file: string; // relative to repo root + startLine: number; // 1-indexed + endLine: number; // 1-indexed, inclusive + text: string; +} + +/** + * Load all code chunks from a repository with metadata + * Returns chunks with file/line info for ground truth matching + * + * @param repoDir - The repository directory path + * @param linesPerChunk - Lines per chunk (default 80) + * @param repoName - Optional repo name to prefix paths (for CodeAtlas compatibility) + */ +export function loadRepoChunksWithMeta( + repoDir: string, + linesPerChunk: number = 80, + repoName?: string +): ChunkMeta[] { + const files = walkFiles(repoDir); + const codeFiles = files.filter(isCodeFile); + const allChunks: ChunkMeta[] = []; + let chunkId = 0; + + // Normalize repoDir - remove leading ./ and trailing / for consistent matching + const normalizedRepoDir = repoDir.replace(/^\.\//, '').replace(/\/+$/, ''); + + for (const file of codeFiles) { + const content = readFileSafe(file); + if (!content) continue; + + // Normalize file path too - remove leading ./ + const normalizedFile = file.replace(/^\.\//, ''); + + // Get relative path from repo root + let relativePath = normalizedFile.startsWith(normalizedRepoDir + '/') + ? normalizedFile.slice(normalizedRepoDir.length + 1) + : normalizedFile; + + // If repoName provided, prefix the path (for CodeAtlas format compatibility) + // CodeAtlas uses paths like "aiohttp/web_protocol.py" + if (repoName) { + relativePath = `${repoName}/${relativePath}`; + } + + const lines = content.split("\n"); + + for (let i = 0; i < lines.length; i += linesPerChunk) { + const chunkLines = lines.slice(i, i + linesPerChunk); + const text = chunkLines.join("\n"); + + if (text.trim().length > 0) { + allChunks.push({ + id: chunkId++, + file: relativePath, + startLine: i + 1, // 1-indexed + endLine: Math.min(i + linesPerChunk, lines.length), // 1-indexed, inclusive + text, + }); + } + } + } + + return allChunks; +} + +/** + * Check if a chunk overlaps with a positive span + * Handles inconsistent CodeAtlas file path formats (some have repo prefix, some don't) + */ +export function chunkOverlapsSpan( + chunk: ChunkMeta, + span: { file: string; start_line: number; end_line: number } +): boolean { + // Normalize paths - remove leading slashes + const chunkFile = chunk.file.replace(/^\//, ''); + const spanFile = span.file.replace(/^\//, ''); + + // Check file match: + // 1. Exact match: "aiohttp/web_protocol.py" === "aiohttp/web_protocol.py" + // 2. Chunk ends with span: "chi/mux.go" ends with "/mux.go" (for span "mux.go") + // 3. Span ends with chunk: "mux.go" (span without prefix) matches end of "chi/mux.go" + const filesMatch = + chunkFile === spanFile || + chunkFile.endsWith('/' + spanFile) || + spanFile.endsWith('/' + chunkFile); + + if (!filesMatch) return false; + + // Check line overlap: chunk.startLine <= span.end_line AND chunk.endLine >= span.start_line + return chunk.startLine <= span.end_line && chunk.endLine >= span.start_line; +} diff --git a/osgrep-core/bench/codeatlas-system.ts b/osgrep-core/bench/codeatlas-system.ts new file mode 100644 index 00000000..5f26173e --- /dev/null +++ b/osgrep-core/bench/codeatlas-system.ts @@ -0,0 +1,511 @@ +/** + * CodeAtlas System Benchmark + * + * Tests the full osgrep pipeline: + * 1. Chunk repo with actual chunker (80 lines) + * 2. Dense retrieval (ORT granite-30m) → top 100 candidates + * 3. ColBERT rerank (pre-indexed) → top 5 results + * 4. Score hits by checking chunk/span overlap + */ + +import { readFileSync, existsSync } from "fs"; +import { join, extname } from "path"; +import { + loadRepoChunksWithMeta, + ChunkMeta, + chunkOverlapsSpan, +} from "./_util.js"; + +// @ts-ignore - generated at build time +import { + initDenseOrt, + denseEmbedOrt, + initColbertOrt, + colbertPreindexDocs, + colbertRerankPreindexed, +} from "../index.js"; + +// ============================================================================= +// Configuration +// ============================================================================= + +const CODEATLAS_PATH = "./codeAtlas/artifacts/codeatlas.jsonl"; +const REPOS_DIR = "./codeAtlas/repos_to_test"; + +// Models +const DENSE_MODEL_REPO = "onnx-community/granite-embedding-30m-english-ONNX"; +const DENSE_HIDDEN_SIZE = 384; +const COLBERT_MODEL_REPO = "ryandono/osgrep-17m-v1-onnx"; +const COLBERT_HIDDEN_SIZE = 48; + +// Retrieval params +const DENSE_TOP_K = 100; +const COLBERT_TOP_K = 5; +const LINES_PER_CHUNK = Number(process.env.LINES_PER_CHUNK ?? 80); +const EXCLUDE_NON_CODE = (process.env.EXCLUDE_NON_CODE ?? "1") !== "0"; +const INCLUDE_PATH_HEADER = (process.env.INCLUDE_PATH_HEADER ?? "0") !== "0"; +const EVAL_DENSE_ONLY = (process.env.EVAL_DENSE_ONLY ?? "1") !== "0"; +const EVAL_COLBERT_ONLY = (process.env.EVAL_COLBERT_ONLY ?? "0") !== "0"; +const HYBRID_ALPHA = process.env.HYBRID_ALPHA ? Number(process.env.HYBRID_ALPHA) : null; + +// Repos to test (start with smaller repos for quick validation) +const TEST_REPOS = (() => { + const env = (process.env.TEST_REPOS ?? "").trim(); + if (!env) { + return new Set([ + "chi", + ]); + } + return new Set(env.split(",").map(s => s.trim()).filter(Boolean)); +})(); + +// ============================================================================= +// Types +// ============================================================================= + +interface CodeAtlasRow { + repo: string; + query: string; + positives: Array<{ file: string; start_line: number; end_line: number; snippet?: string }>; + negatives?: Array<{ file: string; start_line: number; end_line: number; snippet?: string }>; + category?: string; + difficulty?: string; +} + +interface QueryResult { + query: string; + repo: string; + category?: string; + difficulty?: string; + denseRecallAt100: boolean; + denseHitAt5: boolean; + denseMrrAt5: number; + colbertOnlyHitAt5?: boolean; + colbertOnlyMrrAt5?: number; + hybridHitAt5?: boolean; + hybridMrrAt5?: number; + hitAt1: boolean; + hitAt5: boolean; + mrrAt5: number; + bestRankAt5: number; + numPositives: number; + numChunks: number; + timeMs: number; +} + +// ============================================================================= +// Dense Retrieval +// ============================================================================= + +function cosineSimilarity(a: number[], b: number[]): number { + let dot = 0; + let normA = 0; + let normB = 0; + for (let i = 0; i < a.length; i++) { + dot += a[i] * b[i]; + normA += a[i] * a[i]; + normB += b[i] * b[i]; + } + return dot / (Math.sqrt(normA) * Math.sqrt(normB) + 1e-12); +} + +function denseRetrieveScored( + queryText: string, + docEmbeddings: number[][], + topK: number +): Array<{ idx: number; score: number }> { + // Encode query + const queryResult = denseEmbedOrt([queryText], true); + const queryEmb = Array.from(queryResult.embeddings); + + // Compute similarities + const scores: Array<{ idx: number; score: number }> = []; + for (let i = 0; i < docEmbeddings.length; i++) { + const score = cosineSimilarity(queryEmb, docEmbeddings[i]); + scores.push({ idx: i, score }); + } + + // Sort descending and take top K + scores.sort((a, b) => b.score - a.score); + return scores.slice(0, topK); +} + +function minMaxNormalize(values: number[]): number[] { + if (values.length === 0) return values; + let min = Infinity; + let max = -Infinity; + for (const v of values) { + if (v < min) min = v; + if (v > max) max = v; + } + const range = max - min; + if (range <= 1e-12) return values.map(() => 0.5); + return values.map(v => (v - min) / range); +} + +// ============================================================================= +// Metrics +// ============================================================================= + +function computeMetricsAtK( + rankedIndices: number[], + chunks: ChunkMeta[], + positives: CodeAtlasRow["positives"] +): { hitAt1: boolean; hitAt5: boolean; mrr: number; bestRank: number } { + let bestRank = Infinity; + + for (let rank = 0; rank < rankedIndices.length; rank++) { + const chunkIdx = rankedIndices[rank]; + const chunk = chunks[chunkIdx]; + + // Check if this chunk overlaps any positive span + const isHit = positives.some(pos => chunkOverlapsSpan(chunk, pos)); + if (isHit && rank + 1 < bestRank) { + bestRank = rank + 1; // 1-indexed rank + } + } + + if (bestRank === Infinity) { + return { hitAt1: false, hitAt5: false, mrr: 0, bestRank: -1 }; + } + + return { + hitAt1: bestRank === 1, + hitAt5: bestRank <= 5, + mrr: 1 / bestRank, + bestRank, + }; +} + +function positiveChunkIndices(chunks: ChunkMeta[], positives: CodeAtlasRow["positives"]): Set<number> { + const indices = new Set<number>(); + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]; + if (positives.some(pos => chunkOverlapsSpan(chunk, pos))) { + indices.add(i); + } + } + return indices; +} + +function isLikelyCodeFile(filePath: string): boolean { + const ext = extname(filePath).toLowerCase(); + // Allowlist focused on code; excludes docs/JSON noise that often dominates rerank. + const allow = new Set([ + ".py", ".pyx", + ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", + ".rs", ".go", + ".java", ".kt", ".scala", + ".cs", + ".rb", ".php", ".ex", ".exs", + ".c", ".cc", ".cpp", ".h", ".hpp", + ".swift", + ".sql", ".proto", + ]); + return allow.has(ext); +} + +// ============================================================================= +// Main Benchmark +// ============================================================================= + +async function main() { + console.log("=== CodeAtlas System Benchmark ===\n"); + + // 1. Load CodeAtlas data + console.log("Loading CodeAtlas data..."); + if (!existsSync(CODEATLAS_PATH)) { + console.error(`CodeAtlas file not found: ${CODEATLAS_PATH}`); + process.exit(1); + } + + const lines = readFileSync(CODEATLAS_PATH, "utf-8").split("\n").filter(Boolean); + const allRows: CodeAtlasRow[] = lines.map(l => JSON.parse(l)); + + // Filter to test repos + const rows = allRows.filter(r => TEST_REPOS.has(r.repo)); + console.log(` Total rows: ${allRows.length}`); + console.log(` Test repos: ${Array.from(TEST_REPOS).join(", ")}`); + console.log(` Filtered rows: ${rows.length}\n`); + + // 2. Group by repo + const byRepo = new Map<string, CodeAtlasRow[]>(); + for (const row of rows) { + if (!byRepo.has(row.repo)) byRepo.set(row.repo, []); + byRepo.get(row.repo)!.push(row); + } + + // 3. Initialize models + console.log("Initializing models..."); + const initStart = performance.now(); + initDenseOrt(DENSE_MODEL_REPO, DENSE_HIDDEN_SIZE); + initColbertOrt(COLBERT_MODEL_REPO, COLBERT_HIDDEN_SIZE); + const initTime = performance.now() - initStart; + console.log(` Models loaded in ${initTime.toFixed(0)}ms\n`); + + // 4. Process each repo + const allResults: QueryResult[] = []; + let totalIndexTime = 0; + let totalQueryTime = 0; + + for (const [repo, repoRows] of byRepo) { + const repoDir = join(REPOS_DIR, repo); + if (!existsSync(repoDir)) { + console.log(`⚠ Skipping ${repo} - repo not found at ${repoDir}`); + continue; + } + + console.log(`\n--- Processing ${repo} (${repoRows.length} queries) ---`); + + // 4a. Chunk the repo + console.log(` Chunking...`); + const chunkStart = performance.now(); + // Pass repo name to match CodeAtlas file path format (e.g., "aiohttp/web_protocol.py") + let chunks = loadRepoChunksWithMeta(repoDir, LINES_PER_CHUNK, repo); + if (EXCLUDE_NON_CODE) { + chunks = chunks.filter(c => isLikelyCodeFile(c.file)); + } + const chunkTime = performance.now() - chunkStart; + console.log(` ${chunks.length} chunks in ${chunkTime.toFixed(0)}ms`); + + if (chunks.length === 0) { + console.log(` ⚠ No chunks, skipping`); + continue; + } + + // 4b. Dense embed all chunks + console.log(` Dense indexing...`); + const denseStart = performance.now(); + const chunkTexts = chunks.map(c => { + if (!INCLUDE_PATH_HEADER) return c.text; + return `FILE: ${c.file}\nLINES: ${c.startLine}-${c.endLine}\n${c.text}`; + }); + + // Batch encode in groups of 64 to avoid memory issues + const batchSize = 64; + const allDocEmbeddings: number[][] = []; + + for (let i = 0; i < chunkTexts.length; i += batchSize) { + const batch = chunkTexts.slice(i, i + batchSize); + const result = denseEmbedOrt(batch, true); + const embeddings = Array.from(result.embeddings); + const hs = result.hiddenSize; + + for (let j = 0; j < batch.length; j++) { + const start = j * hs; + const end = start + hs; + allDocEmbeddings.push(embeddings.slice(start, end)); + } + } + + const denseTime = performance.now() - denseStart; + console.log(` Dense indexed in ${denseTime.toFixed(0)}ms`); + + // 4c. ColBERT preindex all chunks + console.log(` ColBERT preindexing...`); + const colbertStart = performance.now(); + colbertPreindexDocs(chunkTexts); + const colbertTime = performance.now() - colbertStart; + console.log(` ColBERT preindexed in ${colbertTime.toFixed(0)}ms`); + + totalIndexTime += denseTime + colbertTime; + + // 4d. Run queries + console.log(` Running queries...`); + for (const row of repoRows) { + const queryStart = performance.now(); + const posSet = positiveChunkIndices(chunks, row.positives); + + // Dense retrieval → top 100 + const denseTopKScored = denseRetrieveScored( + row.query, + allDocEmbeddings, + DENSE_TOP_K + ); + const denseTopK = denseTopKScored.map(s => s.idx); + const denseRecallAt100 = denseTopK.some(i => posSet.has(i)); + const denseTop5 = denseTopK.slice(0, Math.min(5, denseTopK.length)); + const denseMetricsAt5 = EVAL_DENSE_ONLY ? computeMetricsAtK(denseTop5, chunks, row.positives) : { hitAt1: false, hitAt5: false, mrr: 0, bestRank: -1 }; + + // ColBERT rerank → top 5 + // colbertRerankPreindexed returns indices into the candidate set (denseTopK) + // and those are already mapped back to original doc indices by the Rust code + const colbertResult = colbertRerankPreindexed( + row.query, + denseTopK, // Pass the candidate indices + HYBRID_ALPHA !== null ? denseTopK.length : COLBERT_TOP_K + ); + + // colbertResult.indices contains the original chunk indices (not indices into denseTopK) + const finalRanking = Array.from(colbertResult.indices) as number[]; + + const queryTime = performance.now() - queryStart; + totalQueryTime += queryTime; + + // Validate indices before computing metrics + const validRanking = finalRanking.filter(idx => idx >= 0 && idx < chunks.length); + if (validRanking.length === 0) { + console.log(` ⚠ No valid rankings for query: ${row.query.slice(0, 50)}...`); + continue; + } + + // Score + const metricsAt5 = computeMetricsAtK(validRanking.slice(0, 5), chunks, row.positives); + + // Optional hybrid: combine dense + colbert within denseTopK to stabilize reranking. + // Hybrid ranking computed as: alpha * norm(colbert) + (1-alpha) * norm(dense) + let hybridMetricsAt5: { hitAt1: boolean; hitAt5: boolean; mrr: number; bestRank: number } | null = null; + if (HYBRID_ALPHA !== null && !Number.isNaN(HYBRID_ALPHA) && HYBRID_ALPHA >= 0 && HYBRID_ALPHA <= 1) { + const alpha = HYBRID_ALPHA; + const denseScores = denseTopKScored.map(s => s.score); + const denseNorm = minMaxNormalize(denseScores); + + const colbertIdx = Array.from(colbertResult.indices) as number[]; + const colbertScores = Array.from(colbertResult.scores) as number[]; + const colbertScoreByChunk = new Map<number, number>(); + for (let i = 0; i < colbertIdx.length; i++) colbertScoreByChunk.set(colbertIdx[i], colbertScores[i]); + + const colbertScoresAligned = denseTopK.map(idx => colbertScoreByChunk.get(idx) ?? 0); + const colbertNorm = minMaxNormalize(colbertScoresAligned); + + const hybridScored = denseTopK.map((idx, i) => ({ + idx, + score: alpha * colbertNorm[i] + (1 - alpha) * denseNorm[i], + })); + hybridScored.sort((a, b) => b.score - a.score); + const hybridTop5 = hybridScored.slice(0, 5).map(s => s.idx); + hybridMetricsAt5 = computeMetricsAtK(hybridTop5, chunks, row.positives); + } + + let colbertOnlyMetricsAt5: { hitAt1: boolean; hitAt5: boolean; mrr: number; bestRank: number } | null = null; + if (EVAL_COLBERT_ONLY) { + const allIdx = Array.from({ length: chunks.length }, (_, i) => i); + const allRes = colbertRerankPreindexed(row.query, allIdx, 5); + const allRank = (Array.from(allRes.indices) as number[]).filter(idx => idx >= 0 && idx < chunks.length); + colbertOnlyMetricsAt5 = computeMetricsAtK(allRank, chunks, row.positives); + } + + // Debug first few queries + if (allResults.length < 3) { + console.log(`\n DEBUG Query: "${row.query.slice(0, 50)}..."`); + console.log(` Positives: ${row.positives.map(p => `${p.file}:${p.start_line}-${p.end_line}`).join(", ")}`); + console.log(` #positive chunks: ${posSet.size} dense@100 hit: ${denseRecallAt100}`); + console.log(` Top 5 chunks: ${validRanking.slice(0, 5).map(i => `${chunks[i].file}:${chunks[i].startLine}-${chunks[i].endLine}`).join(", ")}`); + console.log(` Best rank@5: ${metricsAt5.bestRank}, Hit@1: ${metricsAt5.hitAt1}, Hit@5: ${metricsAt5.hitAt5}`); + } + + allResults.push({ + query: row.query, + repo, + category: row.category, + difficulty: row.difficulty, + denseRecallAt100, + denseHitAt5: denseMetricsAt5.hitAt5, + denseMrrAt5: denseMetricsAt5.mrr, + colbertOnlyHitAt5: colbertOnlyMetricsAt5?.hitAt5, + colbertOnlyMrrAt5: colbertOnlyMetricsAt5?.mrr, + hybridHitAt5: hybridMetricsAt5?.hitAt5, + hybridMrrAt5: hybridMetricsAt5?.mrr, + hitAt1: metricsAt5.hitAt1, + hitAt5: metricsAt5.hitAt5, + mrrAt5: metricsAt5.mrr, + bestRankAt5: metricsAt5.bestRank, + numPositives: row.positives.length, + numChunks: chunks.length, + timeMs: queryTime, + }); + } + } + + // ============================================================================= + // Report Results + // ============================================================================= + + console.log("\n" + "=".repeat(60)); + console.log("=== RESULTS ==="); + console.log("=".repeat(60) + "\n"); + + const n = allResults.length; + if (n === 0) { + console.log("No results to report"); + return; + } + + // Overall metrics + const hitAt1 = allResults.filter(r => r.hitAt1).length / n; + const hitAt5 = allResults.filter(r => r.hitAt5).length / n; + const denseRecallAt100 = allResults.filter(r => r.denseRecallAt100).length / n; + const denseHitAt5 = allResults.filter(r => r.denseHitAt5).length / n; + const denseMrrAt5 = allResults.reduce((sum, r) => sum + r.denseMrrAt5, 0) / n; + const mrrAt5 = allResults.reduce((sum, r) => sum + r.mrrAt5, 0) / n; + const hybridHitAt5 = allResults.filter(r => r.hybridHitAt5).length / n; + const hybridMrrAt5 = allResults.reduce((sum, r) => sum + (r.hybridMrrAt5 ?? 0), 0) / n; + const avgTime = allResults.reduce((sum, r) => sum + r.timeMs, 0) / n; + + console.log(`OVERALL (n=${n})`); + console.log(` Dense@100: ${(denseRecallAt100 * 100).toFixed(1)}%`); + if (EVAL_DENSE_ONLY) { + console.log(` Dense Hit@5: ${(denseHitAt5 * 100).toFixed(1)}% Dense MRR@5: ${denseMrrAt5.toFixed(3)}`); + } + if (HYBRID_ALPHA !== null && !Number.isNaN(HYBRID_ALPHA)) { + console.log(` Hybrid(a=${HYBRID_ALPHA.toFixed(2)}) Hit@5: ${(hybridHitAt5 * 100).toFixed(1)}% Hybrid MRR@5: ${hybridMrrAt5.toFixed(3)}`); + } + console.log(` Hit@1: ${(hitAt1 * 100).toFixed(1)}%`); + console.log(` Hit@5: ${(hitAt5 * 100).toFixed(1)}%`); + console.log(` MRR@5: ${mrrAt5.toFixed(3)}`); + console.log(` Avg query: ${avgTime.toFixed(1)}ms`); + console.log(); + + // By difficulty + const byDiff = new Map<string, QueryResult[]>(); + for (const r of allResults) { + const d = r.difficulty || "unknown"; + if (!byDiff.has(d)) byDiff.set(d, []); + byDiff.get(d)!.push(r); + } + + console.log("BY DIFFICULTY"); + for (const [diff, results] of byDiff) { + const dn = results.length; + const h1 = results.filter(r => r.hitAt1).length / dn; + const h5 = results.filter(r => r.hitAt5).length / dn; + const dr = results.filter(r => r.denseRecallAt100).length / dn; + const m = results.reduce((sum, r) => sum + r.mrrAt5, 0) / dn; + const dh5 = results.filter(r => r.denseHitAt5).length / dn; + const dm = results.reduce((sum, r) => sum + r.denseMrrAt5, 0) / dn; + const densePart = EVAL_DENSE_ONLY ? ` Dense@5: ${(dh5 * 100).toFixed(1).padStart(5)}% dMRR@5: ${dm.toFixed(3)}` : ""; + const hh5 = results.filter(r => r.hybridHitAt5).length / dn; + const hm = results.reduce((sum, r) => sum + (r.hybridMrrAt5 ?? 0), 0) / dn; + const hybridPart = HYBRID_ALPHA !== null ? ` Hybrid@5: ${(hh5 * 100).toFixed(1).padStart(5)}% hMRR@5: ${hm.toFixed(3)}` : ""; + console.log(` ${diff.padEnd(10)} Dense@100: ${(dr * 100).toFixed(1).padStart(5)}%${densePart}${hybridPart} Hit@1: ${(h1 * 100).toFixed(1).padStart(5)}% Hit@5: ${(h5 * 100).toFixed(1).padStart(5)}% MRR@5: ${m.toFixed(3)} (n=${dn})`); + } + console.log(); + + // By repo + console.log("BY REPO"); + for (const [repo, repoRows] of byRepo) { + const results = allResults.filter(r => r.repo === repo); + if (results.length === 0) continue; + const rn = results.length; + const h1 = results.filter(r => r.hitAt1).length / rn; + const h5 = results.filter(r => r.hitAt5).length / rn; + const dr = results.filter(r => r.denseRecallAt100).length / rn; + const m = results.reduce((sum, r) => sum + r.mrrAt5, 0) / rn; + const dh5 = results.filter(r => r.denseHitAt5).length / rn; + const dm = results.reduce((sum, r) => sum + r.denseMrrAt5, 0) / rn; + const densePart = EVAL_DENSE_ONLY ? ` Dense@5: ${(dh5 * 100).toFixed(1).padStart(5)}% dMRR@5: ${dm.toFixed(3)}` : ""; + const hh5 = results.filter(r => r.hybridHitAt5).length / rn; + const hm = results.reduce((sum, r) => sum + (r.hybridMrrAt5 ?? 0), 0) / rn; + const hybridPart = HYBRID_ALPHA !== null ? ` Hybrid@5: ${(hh5 * 100).toFixed(1).padStart(5)}% hMRR@5: ${hm.toFixed(3)}` : ""; + console.log(` ${repo.padEnd(12)} Dense@100: ${(dr * 100).toFixed(1).padStart(5)}%${densePart}${hybridPart} Hit@1: ${(h1 * 100).toFixed(1).padStart(5)}% Hit@5: ${(h5 * 100).toFixed(1).padStart(5)}% MRR@5: ${m.toFixed(3)} (n=${rn})`); + } + console.log(); + + // Timing summary + console.log("TIMING"); + console.log(` Total index time: ${totalIndexTime.toFixed(0)}ms`); + console.log(` Total query time: ${totalQueryTime.toFixed(0)}ms`); + console.log(` Throughput: ${(1000 / avgTime).toFixed(1)} queries/sec`); +} + +main().catch(console.error); diff --git a/osgrep-core/bench/colbert-parity-rust.ts b/osgrep-core/bench/colbert-parity-rust.ts new file mode 100644 index 00000000..68ee7585 --- /dev/null +++ b/osgrep-core/bench/colbert-parity-rust.ts @@ -0,0 +1,121 @@ +/** + * ColBERT Parity (Rust vs Rust) + * + * Compares: + * - `colbertRerankPreindexed` (packed fast path) + * - `colbertRerankOrt` (encode docs at query-time) + * + * This avoids Python/onnxruntime issues and validates that packed scoring + index + * mapping are correct. + * + * Input: JSONL produced by `bench/colbert-parity.ts` + * + * Usage: + * source ~/.cargo/env + * bun run bench/colbert-parity-rust.ts --in /tmp/colbert-parity-chi.jsonl --topk 20 + */ + +import { readFileSync } from "fs"; + +// @ts-ignore - generated at build time +import { initColbertOrt, colbertPreindexDocs, colbertRerankOrt, colbertRerankPreindexed } from "../index.js"; + +function parseArgs(argv: string[]) { + const args: Record<string, string> = {}; + for (let i = 2; i < argv.length; i++) { + const a = argv[i]; + if (!a.startsWith("--")) continue; + const key = a.slice(2); + const val = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : "1"; + args[key] = val; + } + return args; +} + +type ParityMeta = { + type: "meta"; + repo: string; + colbertModelRepo: string; + colbertHiddenSize: number; + topk: number; +}; + +type ParityQuery = { + type: "query"; + queryIndex: number; + query: string; + candidates: Array<{ chunkIndex: number; text: string }>; + rust: { indices: number[]; scores: number[] }; +}; + +function topKOverlap(a: number[], b: number[], k: number): number { + const sa = new Set(a.slice(0, k)); + const sb = new Set(b.slice(0, k)); + let inter = 0; + for (const x of sa) if (sb.has(x)) inter++; + return inter / k; +} + +async function main() { + const args = parseArgs(process.argv); + const inPath = args.in; + const topk = Number(args.topk ?? 20); + if (!inPath) throw new Error("Missing --in /path/to/parity.jsonl"); + + const lines = readFileSync(inPath, "utf8").split("\n").filter(Boolean); + const meta = JSON.parse(lines[0]) as ParityMeta; + const queries = lines.slice(1).map(l => JSON.parse(l) as ParityQuery); + + initColbertOrt(meta.colbertModelRepo, meta.colbertHiddenSize); + + // Re-preindex docs in the same order as the export expects. + // We can just union all candidate docs (order stable within each query) into a global + // list and preindex them; but preindexed rerank expects global doc indices. + // The export already used repo-level chunk indices, so for this check we instead + // rebuild a *local* packed store per query: preindex the candidate docs only. + // + // That lets us compare packed-vs-nonpacked scoring without needing the full repo. + let n = 0; + let top1Match = 0; + let top5OverlapSum = 0; + + for (const q of queries) { + const docs = q.candidates.map(c => c.text); + if (docs.length === 0) continue; + + colbertPreindexDocs(docs); + + // Packed path (indices will be local [0..docs.length)) + const packed = colbertRerankPreindexed(q.query, docs.map((_, i) => i), topk); + const packedPos = Array.from(packed.indices) as number[]; + + // Non-packed path (indices are local [0..docs.length)) + const nonPacked = colbertRerankOrt(q.query, docs, topk); + const nonPackedPos = Array.from(nonPacked.indices) as number[]; + + if (packedPos.length > 0 && nonPackedPos.length > 0 && packedPos[0] === nonPackedPos[0]) { + top1Match++; + } + top5OverlapSum += topKOverlap(packedPos, nonPackedPos, Math.min(5, topk)); + n++; + } + + if (n === 0) { + console.log("No comparable queries"); + return; + } + + console.log("============================================================"); + console.log("Rust vs Rust Parity (preindexed vs query-time)"); + console.log("============================================================"); + console.log(`Input: ${inPath}`); + console.log(`Queries compared: ${n}`); + console.log(`Top-1 match: ${(top1Match / n * 100).toFixed(1)}%`); + console.log(`Top-5 overlap: ${(top5OverlapSum / n).toFixed(3)}`); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); + diff --git a/osgrep-core/bench/colbert-parity.ts b/osgrep-core/bench/colbert-parity.ts new file mode 100644 index 00000000..28760e04 --- /dev/null +++ b/osgrep-core/bench/colbert-parity.ts @@ -0,0 +1,188 @@ +/** + * ColBERT Parity Export + * + * Generates an apples-to-apples dataset to compare: + * - Rust/ORT implementation (preindexed rerank) + * - Python ONNX MaxSim scorer (should match Rust ranking) + * + * Output JSONL contains per-query candidate docs + Rust top-k. + * + * Usage: + * source ~/.cargo/env + * bun run bench/colbert-parity.ts --repo chi --n 30 --lines 20 --dense-topk 100 --topk 20 --out /tmp/parity.jsonl + */ +import { existsSync, writeFileSync } from "fs"; +import { join } from "path"; +import { loadRepoChunksWithMeta } from "./_util.js"; + +// @ts-ignore - generated at build time +import { + initDenseOrt, + denseEmbedOrt, + initColbertOrt, + colbertPreindexDocs, + colbertRerankPreindexed, +} from "../index.js"; + +type PositiveSpan = { file: string; start_line: number; end_line: number }; + +type CodeAtlasRow = { + repo: string; + query: string; + positives: PositiveSpan[]; + difficulty?: string; + category?: string; +}; + +function parseArgs(argv: string[]) { + const args: Record<string, string> = {}; + for (let i = 2; i < argv.length; i++) { + const a = argv[i]; + if (!a.startsWith("--")) continue; + const key = a.slice(2); + const val = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : "1"; + args[key] = val; + } + return args; +} + +function cosineSimilarity(a: number[], b: number[]): number { + let dot = 0; + let normA = 0; + let normB = 0; + for (let i = 0; i < a.length; i++) { + dot += a[i] * b[i]; + normA += a[i] * a[i]; + normB += b[i] * b[i]; + } + return dot / (Math.sqrt(normA) * Math.sqrt(normB) + 1e-12); +} + +function denseTopK(query: string, docEmbeddings: number[][], topK: number): number[] { + const queryResult = denseEmbedOrt([query], true); + const queryEmb = Array.from(queryResult.embeddings) as number[]; + + const scored: Array<{ idx: number; score: number }> = []; + for (let i = 0; i < docEmbeddings.length; i++) { + scored.push({ idx: i, score: cosineSimilarity(queryEmb, docEmbeddings[i]) }); + } + scored.sort((a, b) => b.score - a.score); + return scored.slice(0, topK).map(s => s.idx); +} + +async function main() { + const args = parseArgs(process.argv); + + const repo = args.repo ?? "chi"; + const n = Number(args.n ?? 30); + const linesPerChunk = Number(args.lines ?? 20); + const denseTopk = Number(args["dense-topk"] ?? 100); + const topk = Number(args.topk ?? 20); + const outPath = args.out ?? `/tmp/colbert-parity-${repo}.jsonl`; + + const codeatlasPath = args.codeatlas ?? "./codeAtlas/artifacts/codeatlas.jsonl"; + const reposDir = args.repos ?? "./codeAtlas/repos_to_test"; + + const denseModelRepo = args["dense-model"] ?? "onnx-community/granite-embedding-30m-english-ONNX"; + const denseHiddenSize = Number(args["dense-dim"] ?? 384); + const colbertModelRepo = args["colbert-model"] ?? "ryandono/osgrep-17m-v1-onnx"; + const colbertHiddenSize = Number(args["colbert-dim"] ?? 48); + + if (!existsSync(codeatlasPath)) { + throw new Error(`Missing CodeAtlas JSONL at ${codeatlasPath}`); + } + + const repoDir = join(reposDir, repo); + if (!existsSync(repoDir)) { + throw new Error(`Missing repo dir at ${repoDir}`); + } + + const rows: CodeAtlasRow[] = []; + for (const line of require("fs").readFileSync(codeatlasPath, "utf8").split("\n")) { + if (!line) continue; + const r = JSON.parse(line) as CodeAtlasRow; + if (r.repo === repo) rows.push(r); + } + const picked = rows.slice(0, n); + if (picked.length === 0) throw new Error(`No rows for repo=${repo}`); + + console.log(`[parity] repo=${repo} queries=${picked.length} linesPerChunk=${linesPerChunk}`); + + console.log(`[parity] init dense=${denseModelRepo}`); + initDenseOrt(denseModelRepo, denseHiddenSize); + console.log(`[parity] init colbert=${colbertModelRepo}`); + initColbertOrt(colbertModelRepo, colbertHiddenSize); + + console.log(`[parity] chunking...`); + const chunks = loadRepoChunksWithMeta(repoDir, linesPerChunk, repo); + const chunkTexts = chunks.map(c => c.text); + + console.log(`[parity] dense embedding docs=${chunkTexts.length}...`); + const batchSize = 64; + const allDocEmbeddings: number[][] = []; + for (let i = 0; i < chunkTexts.length; i += batchSize) { + const batch = chunkTexts.slice(i, i + batchSize); + const res = denseEmbedOrt(batch, true); + const flat = Array.from(res.embeddings) as number[]; + const hs = res.hiddenSize as number; + for (let j = 0; j < batch.length; j++) { + allDocEmbeddings.push(flat.slice(j * hs, (j + 1) * hs)); + } + } + + console.log(`[parity] preindex colbert docs=${chunkTexts.length}...`); + colbertPreindexDocs(chunkTexts); + + const outLines: string[] = []; + outLines.push(JSON.stringify({ + type: "meta", + repo, + out: outPath, + denseModelRepo, + denseHiddenSize, + colbertModelRepo, + colbertHiddenSize, + linesPerChunk, + denseTopk, + topk, + totalChunks: chunks.length, + })); + + for (let i = 0; i < picked.length; i++) { + const row = picked[i]; + const candIdx = denseTopK(row.query, allDocEmbeddings, Math.min(denseTopk, chunks.length)); + const rust = colbertRerankPreindexed(row.query, candIdx, topk); + + const candidates = candIdx.map(idx => ({ + chunkIndex: idx, + file: chunks[idx]?.file ?? "", + startLine: chunks[idx]?.startLine ?? 0, + endLine: chunks[idx]?.endLine ?? 0, + text: chunkTexts[idx] ?? "", + })); + + outLines.push(JSON.stringify({ + type: "query", + repo, + queryIndex: i, + query: row.query, + positives: row.positives, + category: row.category, + difficulty: row.difficulty, + candidates, + rust: { + indices: Array.from(rust.indices as number[]), + scores: Array.from(rust.scores as number[]), + }, + })); + } + + writeFileSync(outPath, outLines.join("\n") + "\n", "utf8"); + console.log(`[parity] wrote ${outLines.length} lines -> ${outPath}`); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); + diff --git a/osgrep-core/bench/dense-ort.ts b/osgrep-core/bench/dense-ort.ts new file mode 100644 index 00000000..7bdd2070 --- /dev/null +++ b/osgrep-core/bench/dense-ort.ts @@ -0,0 +1,69 @@ +import { cloneRepoIfMissing, loadRepoChunks, walkFiles, isCodeFile } from "./_util.js"; + +// Import the native addon (will be built by napi) +// @ts-ignore - generated at build time +import { initDenseOrt, denseEmbedChecksumOrt } from "../index.js"; + +const REPO_URL = "https://github.com/sst/opencode.git"; +const REPO_DIR = "./opencode"; +const MODEL_REPO = "onnx-community/granite-embedding-30m-english-ONNX"; +const HIDDEN_SIZE = 384; // granite-embedding-30m-english has 384 dim +const BATCH_SIZE = 64; // Larger batch for throughput +const LINES_PER_CHUNK = 80; // Original chunk size + +async function main() { + console.log("=== Dense Embedding Benchmark (ONNX Runtime) ===\n"); + + // 1. Ensure repo exists + cloneRepoIfMissing(REPO_URL, REPO_DIR); + + // 2. Load chunks + console.log("Loading code files and chunking..."); + const files = walkFiles(REPO_DIR); + const codeFiles = files.filter(isCodeFile); + const chunks = loadRepoChunks(REPO_DIR, LINES_PER_CHUNK); + + console.log(` Files found: ${files.length}`); + console.log(` Code files: ${codeFiles.length}`); + console.log(` Total chunks: ${chunks.length}`); + console.log(` Batch size: ${BATCH_SIZE}`); + console.log(); + + // 3. Initialize model from HuggingFace ONNX repo + console.log(`Initializing ORT model from: ${MODEL_REPO}`); + initDenseOrt(MODEL_REPO, HIDDEN_SIZE); + console.log("Model initialized.\n"); + + // 4. Benchmark encoding + console.log("Starting benchmark..."); + const startTime = performance.now(); + let totalChecksum = 0; + let batchCount = 0; + + for (let i = 0; i < chunks.length; i += BATCH_SIZE) { + const batch = chunks.slice(i, i + BATCH_SIZE); + const checksum = denseEmbedChecksumOrt(batch, true); + totalChecksum += checksum; + batchCount++; + + // Progress indicator every batch + const elapsed = (performance.now() - startTime) / 1000; + const chunksProcessed = Math.min(i + BATCH_SIZE, chunks.length); + const rate = chunksProcessed / elapsed; + process.stdout.write(`\r Processed ${chunksProcessed}/${chunks.length} chunks (${rate.toFixed(1)} chunks/sec)`); + } + + const endTime = performance.now(); + const totalSeconds = (endTime - startTime) / 1000; + const chunksPerSec = chunks.length / totalSeconds; + + console.log("\n"); + console.log("=== Results ==="); + console.log(` File count: ${codeFiles.length}`); + console.log(` Chunk count: ${chunks.length}`); + console.log(` Total time: ${totalSeconds.toFixed(2)} seconds`); + console.log(` Throughput: ${chunksPerSec.toFixed(1)} chunks/sec`); + console.log(` Checksum: ${totalChecksum.toFixed(6)} (for validation)`); +} + +main().catch(console.error); diff --git a/osgrep-core/bench/dense.ts b/osgrep-core/bench/dense.ts new file mode 100644 index 00000000..92dd5bbf --- /dev/null +++ b/osgrep-core/bench/dense.ts @@ -0,0 +1,68 @@ +import { cloneRepoIfMissing, loadRepoChunks, walkFiles, isCodeFile } from "./_util.js"; + +// Import the native addon (will be built by napi) +// @ts-ignore - generated at build time +import { initDense, denseEmbedChecksum } from "../index.js"; + +const REPO_URL = "https://github.com/sst/opencode.git"; +const REPO_DIR = "./opencode"; +const MODEL_REPO = "ibm-granite/granite-embedding-30m-english"; +const BATCH_SIZE = 64; // Larger batch for throughput +const LINES_PER_CHUNK = 80; // Original chunk size + +async function main() { + console.log("=== Dense Embedding Benchmark ===\n"); + + // 1. Ensure repo exists + cloneRepoIfMissing(REPO_URL, REPO_DIR); + + // 2. Load chunks + console.log("Loading code files and chunking..."); + const files = walkFiles(REPO_DIR); + const codeFiles = files.filter(isCodeFile); + const chunks = loadRepoChunks(REPO_DIR, LINES_PER_CHUNK); + + console.log(` Files found: ${files.length}`); + console.log(` Code files: ${codeFiles.length}`); + console.log(` Total chunks: ${chunks.length}`); + console.log(` Batch size: ${BATCH_SIZE}`); + console.log(); + + // 3. Initialize model + console.log(`Initializing dense model: ${MODEL_REPO}`); + initDense(MODEL_REPO); + console.log("Model initialized.\n"); + + // 4. Benchmark encoding + console.log("Starting benchmark..."); + const startTime = performance.now(); + let totalChecksum = 0; + let batchCount = 0; + + for (let i = 0; i < chunks.length; i += BATCH_SIZE) { + const batch = chunks.slice(i, i + BATCH_SIZE); + const checksum = denseEmbedChecksum(batch, true); + totalChecksum += checksum; + batchCount++; + + // Progress indicator every batch + const elapsed = (performance.now() - startTime) / 1000; + const chunksProcessed = Math.min(i + BATCH_SIZE, chunks.length); + const rate = chunksProcessed / elapsed; + process.stdout.write(`\r Processed ${chunksProcessed}/${chunks.length} chunks (${rate.toFixed(1)} chunks/sec)`); + } + + const endTime = performance.now(); + const totalSeconds = (endTime - startTime) / 1000; + const chunksPerSec = chunks.length / totalSeconds; + + console.log("\n"); + console.log("=== Results ==="); + console.log(` File count: ${codeFiles.length}`); + console.log(` Chunk count: ${chunks.length}`); + console.log(` Total time: ${totalSeconds.toFixed(2)} seconds`); + console.log(` Throughput: ${chunksPerSec.toFixed(1)} chunks/sec`); + console.log(` Checksum: ${totalChecksum.toFixed(6)} (for validation)`); +} + +main().catch(console.error); diff --git a/osgrep-core/bench/rerank-ort.ts b/osgrep-core/bench/rerank-ort.ts new file mode 100644 index 00000000..89d50c09 --- /dev/null +++ b/osgrep-core/bench/rerank-ort.ts @@ -0,0 +1,86 @@ +import { cloneRepoIfMissing, loadRepoChunks, walkFiles, isCodeFile } from "./_util.js"; + +// @ts-ignore - generated at build time +import { initColbertOrt, colbertRerankOrt } from "../index.js"; + +const REPO_URL = "https://github.com/sst/opencode.git"; +const REPO_DIR = "./opencode"; +const MODEL_REPO = "ryandono/osgrep-17m-v1-onnx"; +const HIDDEN_SIZE = 48; // osgrep-17m has 48-dim embeddings +const NUM_CANDIDATES = 100; // Rerank top 100 candidates +const TOP_K = 5; // Return top 5 results + +async function main() { + console.log("=== ColBERT Rerank Benchmark (ONNX Runtime) ===\n"); + + // 1. Ensure repo exists + cloneRepoIfMissing(REPO_URL, REPO_DIR); + + // 2. Load chunks + console.log("Loading code files and chunking..."); + const files = walkFiles(REPO_DIR); + const codeFiles = files.filter(isCodeFile); + const allChunks = loadRepoChunks(REPO_DIR, 80); + + console.log(` Files found: ${files.length}`); + console.log(` Code files: ${codeFiles.length}`); + console.log(` Total chunks: ${allChunks.length}`); + console.log(` Candidates per query: ${NUM_CANDIDATES}`); + console.log(` Top-K: ${TOP_K}`); + console.log(); + + // 3. Initialize model + console.log(`Initializing ColBERT ORT model from: ${MODEL_REPO}`); + const initStart = performance.now(); + initColbertOrt(MODEL_REPO, HIDDEN_SIZE); + const initTime = performance.now() - initStart; + console.log(`Model initialized in ${initTime.toFixed(0)}ms\n`); + + // 4. Test queries + const queries = [ + "where is authentication handled", + "how does the API route requests", + "database connection and query execution", + "error handling and logging", + "configuration and environment variables", + ]; + + // Take first NUM_CANDIDATES chunks as simulated candidates + const candidates = allChunks.slice(0, NUM_CANDIDATES); + + console.log("Starting benchmark...\n"); + + let totalRerankTime = 0; + const results: { query: string; topIndices: number[]; topScores: number[]; timeMs: number }[] = []; + + for (const query of queries) { + const startTime = performance.now(); + const result = colbertRerankOrt(query, candidates, TOP_K); + const elapsed = performance.now() - startTime; + totalRerankTime += elapsed; + + results.push({ + query, + topIndices: Array.from(result.indices), + topScores: Array.from(result.scores), + timeMs: elapsed, + }); + + console.log(`Query: "${query}"`); + console.log(` Time: ${elapsed.toFixed(1)}ms`); + console.log(` Top indices: [${result.indices.slice(0, 3).join(", ")}...]`); + console.log(` Top scores: [${result.scores.slice(0, 3).map((s: number) => s.toFixed(3)).join(", ")}...]`); + console.log(); + } + + const avgTime = totalRerankTime / queries.length; + + console.log("=== Summary ==="); + console.log(` Queries: ${queries.length}`); + console.log(` Candidates per query: ${NUM_CANDIDATES}`); + console.log(` Total rerank time: ${totalRerankTime.toFixed(1)}ms`); + console.log(` Avg time per query: ${avgTime.toFixed(1)}ms`); + console.log(` Throughput: ${(1000 / avgTime).toFixed(1)} queries/sec`); +} + +main().catch(console.error); diff --git a/osgrep-core/bench/rerank-preindexed.ts b/osgrep-core/bench/rerank-preindexed.ts new file mode 100644 index 00000000..1c1d3fcb --- /dev/null +++ b/osgrep-core/bench/rerank-preindexed.ts @@ -0,0 +1,94 @@ +import { cloneRepoIfMissing, loadRepoChunks, walkFiles, isCodeFile } from "./_util.js"; + +// @ts-ignore - generated at build time +import { initColbertOrt, colbertPreindexDocs, colbertRerankPreindexed } from "../index.js"; + +const REPO_URL = "https://github.com/sst/opencode.git"; +const REPO_DIR = "./opencode"; +const MODEL_REPO = "ryandono/osgrep-17m-v1-onnx"; +const HIDDEN_SIZE = 48; +const NUM_CANDIDATES = 100; +const TOP_K = 5; + +async function main() { + console.log("=== ColBERT Pre-indexed Rerank Benchmark ===\n"); + + // 1. Load repo + cloneRepoIfMissing(REPO_URL, REPO_DIR); + + console.log("Loading code files and chunking..."); + const files = walkFiles(REPO_DIR); + const codeFiles = files.filter(isCodeFile); + const allChunks = loadRepoChunks(REPO_DIR, 80); + + console.log(` Files found: ${files.length}`); + console.log(` Code files: ${codeFiles.length}`); + console.log(` Total chunks: ${allChunks.length}`); + console.log(); + + // 2. Initialize model + console.log(`Initializing ColBERT ORT model from: ${MODEL_REPO}`); + initColbertOrt(MODEL_REPO, HIDDEN_SIZE); + console.log("Model initialized.\n"); + + // 3. Pre-index first 500 chunks (simulating index time) + const indexChunks = allChunks.slice(0, 500); + console.log(`Pre-indexing ${indexChunks.length} chunks (INDEX TIME)...`); + const indexStart = performance.now(); + const numIndexed = colbertPreindexDocs(indexChunks); + const indexTime = performance.now() - indexStart; + console.log(` Indexed ${numIndexed} chunks in ${indexTime.toFixed(0)}ms`); + console.log(` Rate: ${((indexChunks.length / indexTime) * 1000).toFixed(1)} chunks/sec`); + console.log(); + + // 4. Simulate dense retrieval returning top 100 indices + // In real use, these would come from your dense ORT search + const candidateIndices = Array.from({ length: NUM_CANDIDATES }, (_, i) => i); + + // 5. Test queries (QUERY TIME) + const queries = [ + "where is authentication handled", + "how does the API route requests", + "database connection and query execution", + "error handling and logging", + "configuration and environment variables", + ]; + + console.log(`Starting QUERY TIME benchmark (${NUM_CANDIDATES} candidates per query)...\n`); + + let totalRerankTime = 0; + const results: { query: string; timeMs: number }[] = []; + + for (const query of queries) { + const startTime = performance.now(); + const result = colbertRerankPreindexed(query, candidateIndices, TOP_K); + const elapsed = performance.now() - startTime; + totalRerankTime += elapsed; + + results.push({ query, timeMs: elapsed }); + + console.log(`Query: "${query}"`); + console.log(` Time: ${elapsed.toFixed(1)}ms`); + console.log(` Top indices: [${result.indices.slice(0, 3).join(", ")}...]`); + console.log(` Top scores: [${result.scores.slice(0, 3).map((s: number) => s.toFixed(3)).join(", ")}...]`); + console.log(); + } + + const avgTime = totalRerankTime / queries.length; + + console.log("=== QUERY TIME Summary ==="); + console.log(` Queries: ${queries.length}`); + console.log(` Candidates per query: ${NUM_CANDIDATES}`); + console.log(` Total rerank time: ${totalRerankTime.toFixed(1)}ms`); + console.log(` Avg time per query: ${avgTime.toFixed(1)}ms`); + console.log(` Throughput: ${(1000 / avgTime).toFixed(1)} queries/sec`); + console.log(); + + if (avgTime < 25) { + console.log(` ✓ TARGET MET: <25ms per query!`); + } else { + console.log(` ✗ Target not met (want <25ms, got ${avgTime.toFixed(1)}ms)`); + } +} + +main().catch(console.error); diff --git a/osgrep-core/bench/rerank.ts b/osgrep-core/bench/rerank.ts new file mode 100644 index 00000000..d503b016 --- /dev/null +++ b/osgrep-core/bench/rerank.ts @@ -0,0 +1,60 @@ +import { cloneRepoIfMissing, loadRepoChunks, pickCandidates } from "./_util"; + +// Import the native addon (will be built by napi) +// @ts-ignore - generated at build time +import { initColbert, colbertRerankChecksum } from "../index"; + +const REPO_URL = "https://github.com/sst/opencode.git"; +const REPO_DIR = "./opencode"; +const MODEL_REPO = "mixedbread-ai/mxbai-edge-colbert-v0-17m"; +const NUM_CANDIDATES = 100; +const TOP_K = 5; +const QUERY = "where is auth handled"; +const LINES_PER_CHUNK = 80; + +async function main() { + console.log("=== ColBERT Rerank Benchmark ===\n"); + + // 1. Ensure repo exists + cloneRepoIfMissing(REPO_URL, REPO_DIR); + + // 2. Load chunks and pick candidates + console.log("Loading code files and selecting candidates..."); + const allChunks = loadRepoChunks(REPO_DIR, LINES_PER_CHUNK); + const candidates = pickCandidates(allChunks, NUM_CANDIDATES); + + console.log(` Total chunks available: ${allChunks.length}`); + console.log(` Candidates selected: ${candidates.length}`); + console.log(` Query: "${QUERY}"`); + console.log(` Top-K: ${TOP_K}`); + console.log(); + + // 3. Initialize model + console.log(`Initializing ColBERT model: ${MODEL_REPO}`); + initColbert(MODEL_REPO); + console.log("Model initialized.\n"); + + // 4. Benchmark reranking + console.log("Running rerank benchmark..."); + const startTime = performance.now(); + + const result = colbertRerankChecksum(QUERY, candidates, TOP_K); + + const endTime = performance.now(); + const totalMs = endTime - startTime; + + console.log("\n=== Results ==="); + console.log(` Total time: ${totalMs.toFixed(2)} ms`); + console.log(` Checksum: ${result.checksum.toFixed(6)} (for validation)`); + console.log(); + console.log(" Top-5 results:"); + for (let i = 0; i < result.indices.length; i++) { + const idx = result.indices[i]; + const score = result.scores[i]; + const preview = candidates[idx].slice(0, 80).replace(/\n/g, " ").trim(); + console.log(` [${i + 1}] idx=${idx}, score=${score.toFixed(4)}`); + console.log(` "${preview}..."`); + } +} + +main().catch(console.error); diff --git a/osgrep-core/build.rs b/osgrep-core/build.rs new file mode 100644 index 00000000..9fc23678 --- /dev/null +++ b/osgrep-core/build.rs @@ -0,0 +1,5 @@ +extern crate napi_build; + +fn main() { + napi_build::setup(); +} diff --git a/osgrep-core/bun.lockb b/osgrep-core/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..c62b9d3f99863913d538593fc6f03282f70f69aa GIT binary patch literal 59922 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 literal 0 HcmV?d00001 diff --git a/osgrep-core/index.d.ts b/osgrep-core/index.d.ts new file mode 100644 index 00000000..7b588f84 --- /dev/null +++ b/osgrep-core/index.d.ts @@ -0,0 +1,60 @@ +// TypeScript declarations for the `osgrep-core` native module (N-API). + +export interface DenseResult { + /** Flat array of embeddings [batch_size * 384] */ + embeddings: number[]; + /** Number of texts encoded */ + count: number; +} + +export interface ColbertPackedResult { + /** Packed embeddings as flat i8 array (all docs concatenated) */ + embeddings: Int8Array | number[]; + /** Token IDs for skiplist filtering */ + tokenIds: Uint32Array | number[]; + /** Number of tokens per document */ + lengths: Uint32Array | number[]; + /** Byte offsets into embeddings for each doc */ + offsets: Uint32Array | number[]; +} + +export interface RerankResult { + /** Original indices of top-k documents */ + indices: number[]; + /** MaxSim scores for top-k documents */ + scores: number[]; +} + +export interface EmbedResult { + /** Dense embeddings [batch_size * 384] */ + dense: number[]; + /** Packed ColBERT embeddings (i8) */ + colbertEmbeddings: Int8Array | number[]; + /** Token IDs for skiplist filtering (all docs concatenated) */ + colbertTokenIds: Uint32Array | number[]; + /** Token counts per document */ + colbertLengths: Uint32Array | number[]; + /** Byte offsets per document */ + colbertOffsets: Uint32Array | number[]; +} + +export function initModels(denseRepo: string, colbertRepo: string): void; +export function isInitialized(): boolean; + +export function embedDense(texts: string[]): DenseResult; +export function embedColbertPacked(texts: string[]): ColbertPackedResult; + +/** Returns query embeddings as a flat array [seq_len * 48]. */ +export function encodeQueryColbert(query: string): Float64Array | number[]; + +export function rerankColbert( + queryEmbeddings: Float64Array | number[], + docEmbeddings: Int8Array | number[], + docTokenIds: Uint32Array | number[], + docLengths: number[] | Uint32Array, + docOffsets: number[] | Uint32Array, + candidateIndices: number[] | Uint32Array, + topK: number, +): RerankResult; + +export function embedBatch(texts: string[]): EmbedResult; diff --git a/osgrep-core/index.js b/osgrep-core/index.js new file mode 100644 index 00000000..39323283 --- /dev/null +++ b/osgrep-core/index.js @@ -0,0 +1,124 @@ +// osgrep-core: Native embedding and reranking +// Auto-loads the correct binary for the current platform + +import { createRequire } from 'module'; +import { existsSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { execSync } from 'child_process'; + +const require = createRequire(import.meta.url); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const { platform, arch } = process; + +let nativeBinding = null; +let loadError = null; + +function isMusl() { + if (!process.report || typeof process.report.getReport !== 'function') { + try { + const lddPath = execSync('which ldd').toString().trim(); + return require('fs').readFileSync(lddPath, 'utf8').includes('musl'); + } catch (e) { + return true; + } + } else { + const { glibcVersionRuntime } = process.report.getReport().header; + return !glibcVersionRuntime; + } +} + +function tryLoad(localPath, packageName) { + if (existsSync(join(__dirname, localPath))) { + return require(`./${localPath}`); + } + return require(packageName); +} + +switch (platform) { + case 'darwin': + switch (arch) { + case 'arm64': + try { + nativeBinding = tryLoad('osgrep-core.darwin-arm64.node', '@osgrep-core/darwin-arm64'); + } catch (e) { + loadError = e; + } + break; + case 'x64': + try { + nativeBinding = tryLoad('osgrep-core.darwin-x64.node', '@osgrep-core/darwin-x64'); + } catch (e) { + loadError = e; + } + break; + default: + throw new Error(`Unsupported architecture on macOS: ${arch}`); + } + break; + case 'linux': + switch (arch) { + case 'x64': + if (isMusl()) { + try { + nativeBinding = tryLoad('osgrep-core.linux-x64-musl.node', '@osgrep-core/linux-x64-musl'); + } catch (e) { + loadError = e; + } + } else { + try { + nativeBinding = tryLoad('osgrep-core.linux-x64-gnu.node', '@osgrep-core/linux-x64-gnu'); + } catch (e) { + loadError = e; + } + } + break; + default: + throw new Error(`Unsupported architecture on Linux: ${arch}`); + } + break; + case 'win32': + switch (arch) { + case 'x64': + try { + nativeBinding = tryLoad('osgrep-core.win32-x64-msvc.node', '@osgrep-core/win32-x64-msvc'); + } catch (e) { + loadError = e; + } + break; + default: + throw new Error(`Unsupported architecture on Windows: ${arch}`); + } + break; + default: + throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`); +} + +if (!nativeBinding) { + if (loadError) { + throw loadError; + } + throw new Error(`Failed to load native binding`); +} + +// Clean API exports +export const { + // Initialization + initModels, + isInitialized, + + // Dense embeddings (384-dim) + embedDense, + + // ColBERT embeddings (48-dim per token, packed) + embedColbertPacked, + + // ColBERT reranking + encodeQueryColbert, + rerankColbert, + + // Convenience: both embeddings in one call + embedBatch, +} = nativeBinding; diff --git a/osgrep-core/package.json b/osgrep-core/package.json new file mode 100644 index 00000000..db0a9b0c --- /dev/null +++ b/osgrep-core/package.json @@ -0,0 +1,26 @@ +{ + "name": "osgrep-core", + "version": "0.1.0", + "type": "module", + "main": "index.js", + "types": "index.d.ts", + "napi": { + "name": "osgrep-core", + "triples": { + "defaults": false, + "additional": [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + "x86_64-pc-windows-msvc" + ] + } + }, + "scripts": { + "build": "napi build --platform", + "build:release": "napi build --platform --release" + }, + "devDependencies": { + "@napi-rs/cli": "^3.5.0" + } +} diff --git a/osgrep-core/src/colbert_ort.rs b/osgrep-core/src/colbert_ort.rs new file mode 100644 index 00000000..4d8e8731 --- /dev/null +++ b/osgrep-core/src/colbert_ort.rs @@ -0,0 +1,592 @@ +use ort::session::{Session, builder::GraphOptimizationLevel}; +use ort::value::Value; +use tokenizers::Tokenizer; +use hf_hub::{api::sync::Api, Repo, RepoType}; +use std::collections::HashSet; + +fn log_native(msg: impl AsRef<str>) { + // Intentionally no-op: native logging was polluting CLI output. + // If you need debugging, add structured logging at the JS layer instead. + let _ = msg.as_ref(); +} + +// ColBERT special tokens (these get added during fine-tuning) +const QUERY_MARKER: &str = "[Q]"; +const DOC_MARKER: &str = "[D]"; +const QUERY_MAXLEN: usize = 32; +// This directly caps how much of each chunk the reranker can "see". +// Keep this in sync with chunk sizing; very large values quickly blow up MaxSim cost. +const DOC_MAXLEN: usize = 96; + +pub struct ColbertEncoderOrt { + session: Session, + tokenizer: Tokenizer, + hidden_size: usize, + // Special token IDs + cls_id: u32, + sep_id: u32, + mask_id: u32, + pad_id: u32, + query_marker_id: Option<u32>, + doc_marker_id: Option<u32>, + // Skip list for MaxSim (punctuation, special tokens to ignore) + skip_ids: HashSet<u32>, +} + +impl ColbertEncoderOrt { + pub fn load_from_hf(repo_id: &str, hidden_size: usize) -> anyhow::Result<Self> { + log_native(format!("[ColBERT-ORT] Downloading model from HF hub: {}", repo_id)); + + let api = Api::new()?; + let repo = api.repo(Repo::new(repo_id.to_string(), RepoType::Model)); + + // Download model and tokenizer files + // Try int8 quantized model first for speed, fall back to fp32 + let model_path = repo.get("onnx/model_int8.onnx") + .or_else(|_| repo.get("onnx/model.onnx"))?; + let tokenizer_path = repo.get("tokenizer.json")?; + + // Try to load skiplist + let skip_ids = match repo.get("skiplist.json") { + Ok(skiplist_path) => { + let content = std::fs::read_to_string(&skiplist_path)?; + let ids: Vec<u32> = serde_json::from_str(&content)?; + log_native(format!("[ColBERT-ORT] Loaded skiplist with {} token IDs", ids.len())); + ids.into_iter().collect() + } + Err(_) => { + log_native("[ColBERT-ORT] No skiplist found, using empty"); + HashSet::new() + } + }; + + log_native(format!("[ColBERT-ORT] Loading model from {:?}", model_path)); + + let session = Session::builder()? + .with_optimization_level(GraphOptimizationLevel::Level3)? + .with_intra_threads(8)? // Use more threads for parallel ops + .commit_from_file(&model_path)?; + + let tokenizer = Tokenizer::from_file(&tokenizer_path) + .map_err(|e| anyhow::anyhow!("Failed to load tokenizer: {}", e))?; + + // Get special token IDs + let vocab = tokenizer.get_vocab(true); + + let cls_id = *vocab.get("[CLS]").unwrap_or(&0); + let sep_id = *vocab.get("[SEP]").unwrap_or(&0); + let mask_id = *vocab.get("[MASK]").unwrap_or(&0); + let pad_id = *vocab.get("[PAD]").unwrap_or(&mask_id); // Use MASK as PAD if no PAD + + // ColBERT marker tokens (may not exist in all tokenizers) + let query_marker_id = vocab.get(QUERY_MARKER).copied(); + let doc_marker_id = vocab.get(DOC_MARKER).copied(); + + log_native(format!("[ColBERT-ORT] Token IDs: CLS={}, SEP={}, MASK={}, PAD={}", + cls_id, sep_id, mask_id, pad_id)); + log_native(format!("[ColBERT-ORT] Marker IDs: [Q]={:?}, [D]={:?}", + query_marker_id, doc_marker_id)); + log_native("[ColBERT-ORT] Model loaded successfully"); + + Ok(Self { + session, + tokenizer, + hidden_size, + cls_id, + sep_id, + mask_id, + pad_id, + query_marker_id, + doc_marker_id, + skip_ids, + }) + } + + /// Encode a query with ColBERT format: [CLS] [Q] tokens... [SEP] [MASK]... + /// Pads with [MASK] tokens to QUERY_MAXLEN for query expansion + pub fn encode_query(&mut self, text: &str) -> anyhow::Result<QueryEmbedding> { + // If the tokenizer doesn't have a dedicated [Q] token, mimic the Python + // harness behavior by prefixing the literal string "[Q] ". + let text_for_tokenizer; + let text = if self.query_marker_id.is_none() && !text.starts_with("[Q]") { + text_for_tokenizer = format!("[Q] {}", text); + text_for_tokenizer.as_str() + } else { + text + }; + + // Tokenize without special tokens + let encoding = self.tokenizer + .encode(text, false) + .map_err(|e| anyhow::anyhow!("Tokenization failed: {}", e))?; + + let token_ids = encoding.get_ids(); + + // Build sequence: [CLS] [Q]? tokens... [SEP] [MASK]... + let mut final_ids: Vec<u32> = Vec::with_capacity(QUERY_MAXLEN); + final_ids.push(self.cls_id); + + if let Some(q_id) = self.query_marker_id { + final_ids.push(q_id); + } + + // Add tokens (truncate if needed, leaving room for SEP) + let max_tokens = QUERY_MAXLEN - final_ids.len() - 1; // -1 for SEP + for &id in token_ids.iter().take(max_tokens) { + final_ids.push(id); + } + + final_ids.push(self.sep_id); + + // Pad with [MASK] for query expansion + while final_ids.len() < QUERY_MAXLEN { + final_ids.push(self.mask_id); + } + + // Create attention mask (all 1s, MASK tokens are attended) + let attention_mask: Vec<i64> = vec![1i64; final_ids.len()]; + let input_ids: Vec<i64> = final_ids.iter().map(|&id| id as i64).collect(); + + let seq_len = input_ids.len(); + + // Create tensors + let input_ids_tensor = Value::from_array(([1usize, seq_len], input_ids))?; + let attention_mask_tensor = Value::from_array(([1usize, seq_len], attention_mask))?; + + // Run inference + let outputs = self.session.run(ort::inputs![ + "input_ids" => input_ids_tensor, + "attention_mask" => attention_mask_tensor + ])?; + + // Get embeddings [1, seq_len, hidden_size] + let embeddings_tensor = outputs[0].try_extract_tensor::<f32>()?; + let embeddings_data: &[f32] = embeddings_tensor.1; + + // Copy to owned vec and L2 normalize each token + let mut embeddings = vec![0.0f32; seq_len * self.hidden_size]; + for s in 0..seq_len { + let src_offset = s * self.hidden_size; + let dst_offset = s * self.hidden_size; + + // L2 normalize + let mut sum_sq = 0.0f32; + for d in 0..self.hidden_size { + let val = embeddings_data[src_offset + d]; + sum_sq += val * val; + } + let norm = sum_sq.sqrt().max(1e-12); + + for d in 0..self.hidden_size { + embeddings[dst_offset + d] = embeddings_data[src_offset + d] / norm; + } + } + + Ok(QueryEmbedding { + embeddings, + seq_len, + hidden_size: self.hidden_size, + }) + } + + /// Encode documents in a batch: [CLS] [D]? tokens... [SEP] + pub fn encode_docs(&mut self, texts: &[String]) -> anyhow::Result<Vec<DocEmbedding>> { + if texts.is_empty() { + return Ok(vec![]); + } + + let batch_size = texts.len(); + + // Tokenize all texts + let mut all_token_ids: Vec<Vec<u32>> = Vec::with_capacity(batch_size); + let mut max_len = 0usize; + + for text in texts { + // If the tokenizer doesn't have a dedicated [D] token, mimic the Python + // harness behavior by prefixing the literal string "[D] ". + let text_for_tokenizer; + let text = if self.doc_marker_id.is_none() && !text.starts_with("[D]") { + text_for_tokenizer = format!("[D] {}", text); + text_for_tokenizer.as_str() + } else { + text.as_str() + }; + + let encoding = self.tokenizer + .encode(text, false) + .map_err(|e| anyhow::anyhow!("Tokenization failed: {}", e))?; + + let token_ids = encoding.get_ids(); + + // Build sequence: [CLS] [D]? tokens... [SEP] + let mut final_ids: Vec<u32> = Vec::with_capacity(DOC_MAXLEN); + final_ids.push(self.cls_id); + + if let Some(d_id) = self.doc_marker_id { + final_ids.push(d_id); + } + + // Add tokens (truncate if needed) + let max_tokens = DOC_MAXLEN - final_ids.len() - 1; + for &id in token_ids.iter().take(max_tokens) { + final_ids.push(id); + } + + final_ids.push(self.sep_id); + + if final_ids.len() > max_len { + max_len = final_ids.len(); + } + + all_token_ids.push(final_ids); + } + + // Pad to max_len and create batched tensors + let mut input_ids_vec = vec![0i64; batch_size * max_len]; + let mut attention_mask_vec = vec![0i64; batch_size * max_len]; + let mut real_lengths: Vec<usize> = Vec::with_capacity(batch_size); + + for (i, ids) in all_token_ids.iter().enumerate() { + let real_len = ids.len(); + real_lengths.push(real_len); + + for (j, &id) in ids.iter().enumerate() { + input_ids_vec[i * max_len + j] = id as i64; + attention_mask_vec[i * max_len + j] = 1; + } + // Remaining positions stay 0 (padded) + } + + // Create tensors + let input_ids = Value::from_array(([batch_size, max_len], input_ids_vec))?; + let attention_mask = Value::from_array(([batch_size, max_len], attention_mask_vec))?; + + // Run inference + let outputs = self.session.run(ort::inputs![ + "input_ids" => input_ids, + "attention_mask" => attention_mask + ])?; + + // Get embeddings [batch, max_len, hidden_size] + let embeddings_tensor = outputs[0].try_extract_tensor::<f32>()?; + let embeddings_data: &[f32] = embeddings_tensor.1; + + // Extract per-document embeddings with L2 normalization + let mut results: Vec<DocEmbedding> = Vec::with_capacity(batch_size); + + for b in 0..batch_size { + let real_len = real_lengths[b]; + let token_ids = &all_token_ids[b]; + let mut embeddings = vec![0.0f32; real_len * self.hidden_size]; + + for s in 0..real_len { + let src_offset = b * max_len * self.hidden_size + s * self.hidden_size; + let dst_offset = s * self.hidden_size; + + // L2 normalize each token embedding + let mut sum_sq = 0.0f32; + for d in 0..self.hidden_size { + let val = embeddings_data[src_offset + d]; + sum_sq += val * val; + } + let norm = sum_sq.sqrt().max(1e-12); + + for d in 0..self.hidden_size { + embeddings[dst_offset + d] = embeddings_data[src_offset + d] / norm; + } + } + + results.push(DocEmbedding { + embeddings, + token_ids: token_ids.clone(), + seq_len: real_len, + hidden_size: self.hidden_size, + }); + } + + Ok(results) + } + + /// MaxSim scoring: for each query token, find max similarity with doc tokens, sum + pub fn max_sim(&self, query: &QueryEmbedding, doc: &DocEmbedding) -> f32 { + let mut total_score = 0.0f32; + + for q in 0..query.seq_len { + let q_offset = q * query.hidden_size; + let mut max_dot = f32::NEG_INFINITY; + + for d in 0..doc.seq_len { + // Skip tokens in skiplist (punctuation, special tokens) + if self.skip_ids.contains(&doc.token_ids[d]) { + continue; + } + + let d_offset = d * doc.hidden_size; + + // Dot product (vectors are already L2 normalized) + let mut dot = 0.0f32; + for k in 0..query.hidden_size { + dot += query.embeddings[q_offset + k] * doc.embeddings[d_offset + k]; + } + + if dot > max_dot { + max_dot = dot; + } + } + + if max_dot > f32::NEG_INFINITY { + total_score += max_dot; + } + } + + total_score + } + + /// Rerank documents against a query, return sorted indices and scores + pub fn rerank(&mut self, query: &str, docs: &[String], top_k: usize) -> anyhow::Result<RerankResultOrt> { + use std::time::Instant; + + // Encode query + let t0 = Instant::now(); + let query_emb = self.encode_query(query)?; + let query_time = t0.elapsed(); + + // Encode docs in batches (larger batch = better throughput) + let t1 = Instant::now(); + let batch_size = 64; + let mut all_doc_embs: Vec<DocEmbedding> = Vec::with_capacity(docs.len()); + + for chunk in docs.chunks(batch_size) { + let chunk_vec: Vec<String> = chunk.to_vec(); + let embs = self.encode_docs(&chunk_vec)?; + all_doc_embs.extend(embs); + } + let doc_time = t1.elapsed(); + + // Score all docs + let t2 = Instant::now(); + let mut scores: Vec<(usize, f32)> = all_doc_embs + .iter() + .enumerate() + .map(|(i, doc_emb)| (i, self.max_sim(&query_emb, doc_emb))) + .collect(); + let score_time = t2.elapsed(); + + // Log timing once + static LOGGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + if !LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) { + log_native(format!( + "[ColBERT-ORT] Timing: query={:?} docs={:?} maxsim={:?}", + query_time, doc_time, score_time + )); + } + + // Sort by score descending + scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); + + // Take top_k + let k = std::cmp::min(top_k, scores.len()); + let top_indices: Vec<u32> = scores[..k].iter().map(|(i, _)| *i as u32).collect(); + let top_scores: Vec<f64> = scores[..k].iter().map(|(_, s)| *s as f64).collect(); + let checksum: f64 = scores.iter().map(|(_, s)| *s as f64).sum(); + + Ok(RerankResultOrt { + indices: top_indices, + scores: top_scores, + checksum, + }) + } +} + +#[derive(Clone)] +pub struct QueryEmbedding { + pub embeddings: Vec<f32>, // [seq_len * hidden_size] flattened + pub seq_len: usize, + pub hidden_size: usize, +} + +#[derive(Clone)] +pub struct DocEmbedding { + pub embeddings: Vec<f32>, // [seq_len * hidden_size] flattened + pub token_ids: Vec<u32>, // For skiplist filtering + pub seq_len: usize, + pub hidden_size: usize, +} + +pub struct RerankResultOrt { + pub indices: Vec<u32>, + pub scores: Vec<f64>, + pub checksum: f64, +} + +/// Packed document embeddings for storage/retrieval +/// All embeddings are flattened into a single buffer with offsets +#[derive(Clone)] +pub struct PackedDocEmbeddings { + /// Flattened embeddings: all docs concatenated [sum(lengths) * hidden_size] + pub embeddings: Vec<f32>, + /// Token IDs for skiplist: all docs concatenated [sum(lengths)] + pub token_ids: Vec<u32>, + /// Number of tokens per document + pub lengths: Vec<u32>, + /// Byte offsets into embeddings buffer for each doc (for fast lookup) + pub offsets: Vec<u32>, + /// Hidden dimension + pub hidden_size: usize, +} + +impl ColbertEncoderOrt { + /// Encode documents and return packed embeddings for storage + /// This is for INDEX TIME - encode once, store, reuse at query time + pub fn encode_docs_packed(&mut self, texts: &[String]) -> anyhow::Result<PackedDocEmbeddings> { + if texts.is_empty() { + return Ok(PackedDocEmbeddings { + embeddings: vec![], + token_ids: vec![], + lengths: vec![], + offsets: vec![], + hidden_size: self.hidden_size, + }); + } + + // Encode in batches + let batch_size = 64; + let mut all_embeddings: Vec<f32> = Vec::new(); + let mut all_token_ids: Vec<u32> = Vec::new(); + let mut lengths: Vec<u32> = Vec::with_capacity(texts.len()); + let mut offsets: Vec<u32> = Vec::with_capacity(texts.len()); + + for chunk in texts.chunks(batch_size) { + let chunk_vec: Vec<String> = chunk.to_vec(); + let doc_embs = self.encode_docs(&chunk_vec)?; + + for doc in doc_embs { + offsets.push(all_embeddings.len() as u32); + lengths.push(doc.seq_len as u32); + all_embeddings.extend(doc.embeddings); + all_token_ids.extend(doc.token_ids); + } + } + + Ok(PackedDocEmbeddings { + embeddings: all_embeddings, + token_ids: all_token_ids, + lengths, + offsets, + hidden_size: self.hidden_size, + }) + } + + /// Score a query against pre-computed packed embeddings + /// This is for QUERY TIME - no doc encoding needed + pub fn score_packed( + &self, + query_emb: &QueryEmbedding, + packed: &PackedDocEmbeddings, + doc_indices: &[usize], // Which docs from packed to score + ) -> Vec<f32> { + let mut scores = Vec::with_capacity(doc_indices.len()); + + for &doc_idx in doc_indices { + if doc_idx >= packed.lengths.len() { + scores.push(0.0); + continue; + } + + let doc_len = packed.lengths[doc_idx] as usize; + let emb_offset = packed.offsets[doc_idx] as usize; + let token_offset: usize = packed.offsets[..doc_idx] + .iter() + .zip(&packed.lengths[..doc_idx]) + .map(|(&off, &len)| len as usize) + .sum(); + + // MaxSim scoring + let mut total_score = 0.0f32; + + for q in 0..query_emb.seq_len { + let q_offset = q * query_emb.hidden_size; + let mut max_dot = f32::NEG_INFINITY; + + for d in 0..doc_len { + // Check skiplist + let token_id = packed.token_ids[token_offset + d]; + if self.skip_ids.contains(&token_id) { + continue; + } + + let d_offset = emb_offset + d * packed.hidden_size; + + // Dot product + let mut dot = 0.0f32; + for k in 0..query_emb.hidden_size { + dot += query_emb.embeddings[q_offset + k] + * packed.embeddings[d_offset + k]; + } + + if dot > max_dot { + max_dot = dot; + } + } + + if max_dot > f32::NEG_INFINITY { + total_score += max_dot; + } + } + + scores.push(total_score); + } + + scores + } + + /// Rerank using pre-computed packed embeddings (FAST query-time path) + pub fn rerank_from_packed( + &mut self, + query: &str, + packed: &PackedDocEmbeddings, + doc_indices: &[usize], + top_k: usize, + ) -> anyhow::Result<RerankResultOrt> { + use std::time::Instant; + + // Encode query only + let t0 = Instant::now(); + let query_emb = self.encode_query(query)?; + let query_time = t0.elapsed(); + + // Score against packed embeddings + let t1 = Instant::now(); + let raw_scores = self.score_packed(&query_emb, packed, doc_indices); + let score_time = t1.elapsed(); + + // Log timing + static LOGGED_PACKED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + if !LOGGED_PACKED.swap(true, std::sync::atomic::Ordering::Relaxed) { + log_native(format!( + "[ColBERT-ORT] Packed timing: query={:?} maxsim={:?}", + query_time, score_time + )); + } + + // Sort by score descending + let mut indexed_scores: Vec<(usize, f32)> = raw_scores + .iter() + .enumerate() + .map(|(i, &s)| (doc_indices[i], s)) + .collect(); + indexed_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); + + // Take top_k + let k = std::cmp::min(top_k, indexed_scores.len()); + let top_indices: Vec<u32> = indexed_scores[..k].iter().map(|(i, _)| *i as u32).collect(); + let top_scores: Vec<f64> = indexed_scores[..k].iter().map(|(_, s)| *s as f64).collect(); + let checksum: f64 = raw_scores.iter().map(|&s| s as f64).sum(); + + Ok(RerankResultOrt { + indices: top_indices, + scores: top_scores, + checksum, + }) + } +} diff --git a/osgrep-core/src/dense_ort.rs b/osgrep-core/src/dense_ort.rs new file mode 100644 index 00000000..5903a2e9 --- /dev/null +++ b/osgrep-core/src/dense_ort.rs @@ -0,0 +1,208 @@ +use ort::session::{Session, builder::GraphOptimizationLevel}; +use ort::value::Value; +use tokenizers::Tokenizer; +use hf_hub::{api::sync::Api, Repo, RepoType}; + +fn log_native(msg: impl AsRef<str>) { + // Intentionally no-op: native logging was polluting CLI output. + // If you need debugging, add structured logging at the JS layer instead. + let _ = msg.as_ref(); +} + +pub struct DenseEncoderOrt { + session: Session, + tokenizer: Tokenizer, + hidden_size: usize, +} + +impl DenseEncoderOrt { + /// Load ONNX model and tokenizer from HuggingFace Hub + /// repo_id: HF repo like "onnx-community/granite-embedding-30m-english-ONNX" + pub fn load_from_hf(repo_id: &str, hidden_size: usize) -> anyhow::Result<Self> { + log_native(format!("[ORT] Downloading model from HF hub: {}", repo_id)); + + let api = Api::new()?; + let repo = api.repo(Repo::new(repo_id.to_string(), RepoType::Model)); + + // Download model and tokenizer files + // ONNX model is in onnx/ subdirectory for onnx-community repos + // Also download the external data file if it exists + let model_path = repo.get("onnx/model.onnx")?; + let _ = repo.get("onnx/model.onnx_data"); // External data, ignore if not found + let tokenizer_path = repo.get("tokenizer.json")?; + + log_native(format!("[ORT] Loading model from {:?}", model_path)); + + // Initialize ONNX Runtime session with CPU provider + let session = Session::builder()? + .with_optimization_level(GraphOptimizationLevel::Level3)? + .with_intra_threads(4)? // Use 4 threads for intra-op parallelism + .commit_from_file(&model_path)?; + + // Load tokenizer + let mut tokenizer = Tokenizer::from_file(&tokenizer_path) + .map_err(|e| anyhow::anyhow!("Failed to load tokenizer: {}", e))?; + + // Configure truncation/padding (same as Candle) + let max_len = 256usize; + tokenizer.with_truncation(Some(tokenizers::TruncationParams { + max_length: max_len, + ..Default::default() + })).map_err(|e| anyhow::anyhow!("Failed to set truncation: {}", e))?; + + tokenizer.with_padding(Some(tokenizers::PaddingParams { + strategy: tokenizers::PaddingStrategy::BatchLongest, + ..Default::default() + })); + + log_native(format!("[ORT] Tokenizer configured with max_seq_len={}", max_len)); + log_native("[ORT] Model loaded successfully"); + + Ok(Self { + session, + tokenizer, + hidden_size, + }) + } + + /// Load ONNX model and tokenizer from local paths + pub fn load(model_path: &str, tokenizer_path: &str, hidden_size: usize) -> anyhow::Result<Self> { + log_native(format!("[ORT] Loading model from {}", model_path)); + + // Initialize ONNX Runtime session with CPU provider + let session = Session::builder()? + .with_optimization_level(GraphOptimizationLevel::Level3)? + .with_intra_threads(4)? + .commit_from_file(model_path)?; + + // Load tokenizer + let mut tokenizer = Tokenizer::from_file(tokenizer_path) + .map_err(|e| anyhow::anyhow!("Failed to load tokenizer: {}", e))?; + + let max_len = 256usize; + tokenizer.with_truncation(Some(tokenizers::TruncationParams { + max_length: max_len, + ..Default::default() + })).map_err(|e| anyhow::anyhow!("Failed to set truncation: {}", e))?; + + tokenizer.with_padding(Some(tokenizers::PaddingParams { + strategy: tokenizers::PaddingStrategy::BatchLongest, + ..Default::default() + })); + + log_native(format!("[ORT] Tokenizer configured with max_seq_len={}", max_len)); + log_native("[ORT] Model loaded successfully"); + + Ok(Self { + session, + tokenizer, + hidden_size, + }) + } + + pub fn encode_batch(&mut self, texts: Vec<String>, normalize: bool) -> anyhow::Result<Vec<f32>> { + if texts.is_empty() { + return Err(anyhow::anyhow!("Empty input texts")); + } + + // Tokenize + let encodings = self.tokenizer + .encode_batch(texts.clone(), true) + .map_err(|e| anyhow::anyhow!("Tokenization failed: {}", e))?; + + let max_len = encodings.iter().map(|e| e.get_ids().len()).max().unwrap_or(0); + let batch_size = encodings.len(); + + // Log once + static LOGGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + if !LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) { + log_native(format!("[ORT] First batch: batch_size={} max_seq_len={}", batch_size, max_len)); + } + + // Prepare input tensors + let mut input_ids_vec = vec![0i64; batch_size * max_len]; + let mut attention_mask_vec = vec![0i64; batch_size * max_len]; + let mut token_type_ids_vec = vec![0i64; batch_size * max_len]; + + for (i, encoding) in encodings.iter().enumerate() { + let ids = encoding.get_ids(); + let mask = encoding.get_attention_mask(); + let type_ids = encoding.get_type_ids(); + + for (j, &id) in ids.iter().enumerate() { + input_ids_vec[i * max_len + j] = id as i64; + } + for (j, &m) in mask.iter().enumerate() { + attention_mask_vec[i * max_len + j] = m as i64; + } + for (j, &t) in type_ids.iter().enumerate() { + token_type_ids_vec[i * max_len + j] = t as i64; + } + } + + // Create ORT tensors - pass Vec directly, not slice + let input_ids = Value::from_array(([batch_size, max_len], input_ids_vec))?; + let attention_mask_tensor = Value::from_array(([batch_size, max_len], attention_mask_vec.clone()))?; + let token_type_ids = Value::from_array(([batch_size, max_len], token_type_ids_vec))?; + + // Run inference + let outputs = self.session.run(ort::inputs![ + "input_ids" => input_ids, + "attention_mask" => attention_mask_tensor, + "token_type_ids" => token_type_ids + ])?; + + // Get the last_hidden_state output (typically first output) + let embeddings_tensor = outputs[0].try_extract_tensor::<f32>()?; + let embeddings_data: &[f32] = embeddings_tensor.1; + + // Mean pooling + let mut pooled = vec![0.0f32; batch_size * self.hidden_size]; + for i in 0..batch_size { + let mut sum_hidden = vec![0.0f32; self.hidden_size]; + let mut sum_mask = 0.0f32; + + for j in 0..max_len { + let mask_val = attention_mask_vec[i * max_len + j] as f32; + sum_mask += mask_val; + + for k in 0..self.hidden_size { + let emb_val = embeddings_data[i * max_len * self.hidden_size + j * self.hidden_size + k]; + sum_hidden[k] += emb_val * mask_val; + } + } + + let denom = sum_mask.max(1e-9); + for k in 0..self.hidden_size { + pooled[i * self.hidden_size + k] = sum_hidden[k] / denom; + } + } + + // L2 normalize if requested + if normalize { + for i in 0..batch_size { + let start = i * self.hidden_size; + let end = start + self.hidden_size; + let slice = &mut pooled[start..end]; + + let norm = slice.iter().map(|x| x * x).sum::<f32>().sqrt().max(1e-12); + for val in slice.iter_mut() { + *val /= norm; + } + } + } + + Ok(pooled) + } + + pub fn compute_checksum(&mut self, texts: Vec<String>, normalize: bool) -> anyhow::Result<f64> { + let embeddings = self.encode_batch(texts, normalize)?; + let checksum: f64 = embeddings.iter().map(|&x| x as f64).sum(); + Ok(checksum) + } + + /// Get the hidden size (embedding dimension) + pub fn hidden_size(&self) -> usize { + self.hidden_size + } +} diff --git a/osgrep-core/src/lib.rs b/osgrep-core/src/lib.rs new file mode 100644 index 00000000..db8fae8a --- /dev/null +++ b/osgrep-core/src/lib.rs @@ -0,0 +1,265 @@ +//! osgrep-core: Fast embedding and reranking via ONNX Runtime +//! +//! This is the native performance core of osgrep. It provides: +//! - Dense embeddings (384-dim, granite-30m) +//! - ColBERT token embeddings (48-dim, mxbai-edge-colbert-17m) +//! - MaxSim reranking with pre-indexed documents + +#[macro_use] +extern crate napi_derive; + +use napi::bindgen_prelude::*; +use once_cell::sync::OnceCell; +use std::sync::Mutex; + +mod dense_ort; +mod colbert_ort; + +use dense_ort::DenseEncoderOrt; +use colbert_ort::{ColbertEncoderOrt, PackedDocEmbeddings}; + +// ============================================================================= +// Global Model Storage (initialized once, reused) +// ============================================================================= + +static DENSE_MODEL: OnceCell<Mutex<DenseEncoderOrt>> = OnceCell::new(); +static COLBERT_MODEL: OnceCell<Mutex<ColbertEncoderOrt>> = OnceCell::new(); + +// ============================================================================= +// Initialization +// ============================================================================= + +/// Initialize both models. Call once at startup. +/// +/// dense_repo: HF repo like "onnx-community/granite-embedding-30m-english-ONNX" +/// colbert_repo: HF repo like "ryandono/mxbai-edge-colbert-v0-17m-onnx-int8" +#[napi] +pub fn init_models(dense_repo: String, colbert_repo: String) -> Result<()> { + // Initialize dense model + if DENSE_MODEL.get().is_none() { + let encoder = DenseEncoderOrt::load_from_hf(&dense_repo, 384) + .map_err(|e| Error::from_reason(format!("Failed to load dense model: {:?}", e)))?; + DENSE_MODEL.set(Mutex::new(encoder)) + .map_err(|_| Error::from_reason("Dense model already initialized"))?; + } + + // Initialize ColBERT model + if COLBERT_MODEL.get().is_none() { + let encoder = ColbertEncoderOrt::load_from_hf(&colbert_repo, 48) + .map_err(|e| Error::from_reason(format!("Failed to load ColBERT model: {:?}", e)))?; + COLBERT_MODEL.set(Mutex::new(encoder)) + .map_err(|_| Error::from_reason("ColBERT model already initialized"))?; + } + + Ok(()) +} + +/// Check if models are initialized +#[napi] +pub fn is_initialized() -> bool { + DENSE_MODEL.get().is_some() && COLBERT_MODEL.get().is_some() +} + +// ============================================================================= +// Dense Embeddings +// ============================================================================= + +#[napi(object)] +pub struct DenseResult { + /// Flat array of embeddings [batch_size * 384] + pub embeddings: Vec<f64>, + /// Number of texts encoded + pub count: u32, +} + +/// Encode texts to dense vectors (384-dim, L2-normalized) +#[napi] +pub fn embed_dense(texts: Vec<String>) -> Result<DenseResult> { + let model = DENSE_MODEL.get() + .ok_or_else(|| Error::from_reason("Models not initialized. Call init_models() first."))?; + + let mut encoder = model.lock() + .map_err(|e| Error::from_reason(format!("Failed to lock dense model: {:?}", e)))?; + + let embeddings_f32 = encoder.encode_batch(texts.clone(), true) + .map_err(|e| Error::from_reason(format!("Dense encoding failed: {:?}", e)))?; + + Ok(DenseResult { + embeddings: embeddings_f32.iter().map(|&x| x as f64).collect(), + count: texts.len() as u32, + }) +} + +// ============================================================================= +// ColBERT Embeddings (for indexing) +// ============================================================================= + +#[napi(object)] +pub struct ColbertPackedResult { + /// Packed embeddings as flat i8 array (all docs concatenated) + pub embeddings: Vec<i8>, + /// Token IDs for skiplist filtering + pub token_ids: Vec<u32>, + /// Number of tokens per document + pub lengths: Vec<u32>, + /// Byte offsets into embeddings for each doc + pub offsets: Vec<u32>, +} + +/// Encode documents to ColBERT embeddings (48-dim per token, packed for storage) +/// Call this at INDEX TIME to pre-compute embeddings +#[napi] +pub fn embed_colbert_packed(texts: Vec<String>) -> Result<ColbertPackedResult> { + let model = COLBERT_MODEL.get() + .ok_or_else(|| Error::from_reason("Models not initialized. Call init_models() first."))?; + + let mut encoder = model.lock() + .map_err(|e| Error::from_reason(format!("Failed to lock ColBERT model: {:?}", e)))?; + + let packed = encoder.encode_docs_packed(&texts) + .map_err(|e| Error::from_reason(format!("ColBERT encoding failed: {:?}", e)))?; + + // Convert f32 embeddings to i8 (quantized) + let embeddings_i8: Vec<i8> = packed.embeddings.iter() + .map(|&x| (x * 127.0).clamp(-128.0, 127.0) as i8) + .collect(); + + Ok(ColbertPackedResult { + embeddings: embeddings_i8, + token_ids: packed.token_ids, + lengths: packed.lengths, + offsets: packed.offsets, + }) +} + +// ============================================================================= +// ColBERT Reranking (for search) +// ============================================================================= + +#[napi(object)] +pub struct RerankResult { + /// Original indices of top-k documents + pub indices: Vec<u32>, + /// MaxSim scores for top-k documents + pub scores: Vec<f64>, +} + +/// Encode a query for ColBERT reranking +/// Returns the query matrix as flat f64 array [seq_len * 48] +#[napi] +pub fn encode_query_colbert(query: String) -> Result<Vec<f64>> { + let model = COLBERT_MODEL.get() + .ok_or_else(|| Error::from_reason("Models not initialized. Call init_models() first."))?; + + let mut encoder = model.lock() + .map_err(|e| Error::from_reason(format!("Failed to lock ColBERT model: {:?}", e)))?; + + let query_emb = encoder.encode_query(&query) + .map_err(|e| Error::from_reason(format!("Query encoding failed: {:?}", e)))?; + + Ok(query_emb.embeddings.iter().map(|&x| x as f64).collect()) +} + +/// Rerank documents using pre-indexed ColBERT embeddings +/// +/// query_embeddings: flat f64 array from encode_query_colbert [seq_len * 48] +/// doc_embeddings: flat i8 array (packed ColBERT embeddings) +/// doc_lengths: number of tokens per document +/// doc_offsets: byte offset for each document in doc_embeddings +/// candidate_indices: which docs to rerank (e.g., top-100 from dense retrieval) +/// top_k: how many to return +#[napi] +pub fn rerank_colbert( + query_embeddings: Float64Array, + doc_embeddings: Int8Array, + doc_token_ids: Uint32Array, + doc_lengths: Vec<u32>, + doc_offsets: Vec<u32>, + candidate_indices: Vec<u32>, + top_k: u32, +) -> Result<RerankResult> { + let model = COLBERT_MODEL.get() + .ok_or_else(|| Error::from_reason("Models not initialized. Call init_models() first."))?; + + let encoder = model.lock() + .map_err(|e| Error::from_reason(format!("Failed to lock ColBERT model: {:?}", e)))?; + + let query_embeddings = query_embeddings.to_vec(); + let doc_embeddings = doc_embeddings.to_vec(); + let doc_token_ids = doc_token_ids.to_vec(); + + let hidden_size = 48usize; + let query_seq_len = query_embeddings.len() / hidden_size; + + // Reconstruct query embedding struct + let query_emb = colbert_ort::QueryEmbedding { + embeddings: query_embeddings.iter().map(|&x| x as f32).collect(), + seq_len: query_seq_len, + hidden_size, + }; + + // Reconstruct packed doc embeddings (convert i8 back to f32) + let doc_embeddings_f32: Vec<f32> = doc_embeddings.iter() + .map(|&x| (x as f32) / 127.0) + .collect(); + + let packed = PackedDocEmbeddings { + embeddings: doc_embeddings_f32, + token_ids: doc_token_ids, + lengths: doc_lengths, + offsets: doc_offsets, + hidden_size, + }; + + // Score candidates + let indices: Vec<usize> = candidate_indices.iter().map(|&i| i as usize).collect(); + let scores = encoder.score_packed(&query_emb, &packed, &indices); + + // Sort by score descending + let mut indexed_scores: Vec<(usize, f32)> = indices.iter() + .zip(scores.iter()) + .map(|(&i, &s)| (i, s)) + .collect(); + indexed_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); + + // Take top-k + let k = std::cmp::min(top_k as usize, indexed_scores.len()); + + Ok(RerankResult { + indices: indexed_scores[..k].iter().map(|(i, _)| *i as u32).collect(), + scores: indexed_scores[..k].iter().map(|(_, s)| *s as f64).collect(), + }) +} + +// ============================================================================= +// Convenience: Combined embed for indexing +// ============================================================================= + +#[napi(object)] +pub struct EmbedResult { + /// Dense embeddings [batch_size * 384] + pub dense: Vec<f64>, + /// Packed ColBERT embeddings (i8) + pub colbert_embeddings: Vec<i8>, + /// Token IDs for skiplist filtering (all docs concatenated) + pub colbert_token_ids: Vec<u32>, + /// Token counts per document + pub colbert_lengths: Vec<u32>, + /// Byte offsets per document + pub colbert_offsets: Vec<u32>, +} + +/// Embed texts for indexing (both dense and ColBERT in one call) +#[napi] +pub fn embed_batch(texts: Vec<String>) -> Result<EmbedResult> { + let dense = embed_dense(texts.clone())?; + let colbert = embed_colbert_packed(texts)?; + + Ok(EmbedResult { + dense: dense.embeddings, + colbert_embeddings: colbert.embeddings, + colbert_token_ids: colbert.token_ids, + colbert_lengths: colbert.lengths, + colbert_offsets: colbert.offsets, + }) +} diff --git a/osgrep-core/test.mjs b/osgrep-core/test.mjs new file mode 100644 index 00000000..3b37d7ab --- /dev/null +++ b/osgrep-core/test.mjs @@ -0,0 +1,42 @@ +// Quick smoke test for osgrep-core +import { initModels, isInitialized, embedDense, embedBatch } from './index.js'; + +const DENSE_REPO = 'onnx-community/granite-embedding-30m-english-ONNX'; +const COLBERT_REPO = 'ryandono/mxbai-edge-colbert-v0-17m-onnx-int8'; + +async function main() { + console.log('Testing osgrep-core...\n'); + + // Check initial state + console.log('isInitialized:', isInitialized()); + + // Initialize models + console.log('Initializing models...'); + const start = Date.now(); + initModels(DENSE_REPO, COLBERT_REPO); + console.log(`Models loaded in ${Date.now() - start}ms`); + console.log('isInitialized:', isInitialized()); + + // Test dense embedding + console.log('\nTesting dense embedding...'); + const texts = ['hello world', 'how does authentication work']; + const t0 = Date.now(); + const dense = embedDense(texts); + console.log(`Dense embed ${texts.length} texts in ${Date.now() - t0}ms`); + console.log(` count: ${dense.count}`); + console.log(` embeddings length: ${dense.embeddings.length} (expected: ${texts.length * 384})`); + + // Test combined embed + console.log('\nTesting combined embed (dense + colbert)...'); + const t1 = Date.now(); + const result = embedBatch(texts); + console.log(`Combined embed ${texts.length} texts in ${Date.now() - t1}ms`); + console.log(` dense length: ${result.dense.length}`); + console.log(` colbert_embeddings length: ${result.colbertEmbeddings.length}`); + console.log(` colbert_lengths: ${result.colbertLengths}`); + console.log(` colbert_offsets: ${result.colbertOffsets}`); + + console.log('\n✅ All tests passed!'); +} + +main().catch(console.error); diff --git a/osgrep-core/tsconfig.json b/osgrep-core/tsconfig.json new file mode 100644 index 00000000..238655f2 --- /dev/null +++ b/osgrep-core/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/package.json b/package.json index 32251ad0..d91b60ac 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "license": "Apache-2.0", "description": "Local grep-like search tool for your codebase.", "dependencies": { - "osgrep-core": "file:../osgrep_v2", + "osgrep-core": "file:./osgrep-core", "@clack/prompts": "^0.11.0", "@lancedb/lancedb": "^0.22.3", "@modelcontextprotocol/sdk": "^1.24.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4351871b..b04f1902 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,8 +45,8 @@ dependencies: specifier: ^5.4.1 version: 5.4.1 osgrep-core: - specifier: file:../osgrep_v2 - version: file:../osgrep_v2 + specifier: file:./osgrep-core + version: file:osgrep-core simsimd: specifier: ^6.5.5 version: 6.5.5 @@ -3429,7 +3429,7 @@ packages: /zod@4.1.13: resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} - file:../osgrep_v2: - resolution: {directory: ../osgrep_v2, type: directory} + file:osgrep-core: + resolution: {directory: osgrep-core, type: directory} name: osgrep-core dev: false diff --git a/src/lib/native/index.ts b/src/lib/native/index.ts index 48718923..64c30565 100644 --- a/src/lib/native/index.ts +++ b/src/lib/native/index.ts @@ -23,7 +23,7 @@ async function loadNative() { native = await import("osgrep-core"); } catch (e) { throw new Error( - `Failed to load osgrep-core native binding. Run 'npm run build:release' in osgrep_v2/: ${e}` + `Failed to load osgrep-core native binding. Run 'npm run build:release' in osgrep/osgrep-core/: ${e}` ); } From 70e48c1552a5ed02bc6abdd1214b728dd111d059 Mon Sep 17 00:00:00 2001 From: Ryan D'Onofrio <97113725+Ryandonofrio3@users.noreply.github.com> Date: Sat, 13 Dec 2025 19:32:23 -0800 Subject: [PATCH 05/19] cleanup --- osgrep-core/bench/PARITY.md | 45 -- osgrep-core/bench/_util.ts | 219 ---------- osgrep-core/bench/codeatlas-system.ts | 511 ----------------------- osgrep-core/bench/colbert-parity-rust.ts | 121 ------ osgrep-core/bench/colbert-parity.ts | 188 --------- osgrep-core/bench/dense-ort.ts | 69 --- osgrep-core/bench/dense.ts | 68 --- osgrep-core/bench/rerank-ort.ts | 86 ---- osgrep-core/bench/rerank-preindexed.ts | 94 ----- osgrep-core/bench/rerank.ts | 60 --- osgrep-core/bun.lockb | Bin 59922 -> 0 bytes osgrep-core/package.json | 5 + osgrep-core/test.mjs | 42 -- osgrep-core/tsconfig.json | 27 -- 14 files changed, 5 insertions(+), 1530 deletions(-) delete mode 100644 osgrep-core/bench/PARITY.md delete mode 100644 osgrep-core/bench/_util.ts delete mode 100644 osgrep-core/bench/codeatlas-system.ts delete mode 100644 osgrep-core/bench/colbert-parity-rust.ts delete mode 100644 osgrep-core/bench/colbert-parity.ts delete mode 100644 osgrep-core/bench/dense-ort.ts delete mode 100644 osgrep-core/bench/dense.ts delete mode 100644 osgrep-core/bench/rerank-ort.ts delete mode 100644 osgrep-core/bench/rerank-preindexed.ts delete mode 100644 osgrep-core/bench/rerank.ts delete mode 100755 osgrep-core/bun.lockb delete mode 100644 osgrep-core/test.mjs delete mode 100644 osgrep-core/tsconfig.json diff --git a/osgrep-core/bench/PARITY.md b/osgrep-core/bench/PARITY.md deleted file mode 100644 index 44840ffa..00000000 --- a/osgrep-core/bench/PARITY.md +++ /dev/null @@ -1,45 +0,0 @@ -# ColBERT Parity (Rust ORT vs Python ONNX) - -This repo has a small “apples-to-apples” parity harness to check whether the -Rust ORT ColBERT reranker matches a reference Python ONNX MaxSim implementation -on identical queries and identical candidate chunks. - -## 1) Export parity input (Node/Rust) - -From the repo root: - -```bash -source ~/.cargo/env -bun run bench/colbert-parity.ts --repo chi --n 30 --lines 20 --dense-topk 100 --topk 20 --out /tmp/colbert-parity-chi.jsonl -``` - -This writes JSONL with: -- the query text -- the dense candidate set (chunk texts + metadata) -- the Rust reranker’s returned indices/scores for the same candidate set - -## 2) Run Python ONNX scorer on the exported JSONL - -You need paths to: -- `model_int8.onnx` (or `model.onnx`) -- `tokenizer.json` -- optional `skiplist.json` - -If you’re using the same HF Hub model as Rust (`ryandono/osgrep-17m-v1-onnx`), -these should exist in your local HuggingFace cache under: - -`~/.cache/huggingface/hub/models--ryandono--osgrep-17m-v1-onnx/snapshots/<SNAPSHOT>/` - -Run: - -```bash -python3 codeAtlas/python/parity_rust_vs_onnx.py \ - --in /tmp/colbert-parity-chi.jsonl \ - --onnx-model ~/.cache/huggingface/hub/models--ryandono--osgrep-17m-v1-onnx/snapshots/<SNAPSHOT>/onnx/model_int8.onnx \ - --tokenizer-json ~/.cache/huggingface/hub/models--ryandono--osgrep-17m-v1-onnx/snapshots/<SNAPSHOT>/tokenizer.json \ - --skiplist-json ~/.cache/huggingface/hub/models--ryandono--osgrep-17m-v1-onnx/snapshots/<SNAPSHOT>/skiplist.json \ - --topk 20 -``` - -Expected: very high Top-1 match and Top-5 overlap if preprocessing is aligned. - diff --git a/osgrep-core/bench/_util.ts b/osgrep-core/bench/_util.ts deleted file mode 100644 index 94d5ed43..00000000 --- a/osgrep-core/bench/_util.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { execSync } from "child_process"; -import { existsSync, readdirSync, readFileSync, statSync } from "fs"; -import { join, extname } from "path"; - -/** - * Clone a git repository if it doesn't exist locally - */ -export function cloneRepoIfMissing(repoUrl: string, dir: string): void { - if (existsSync(dir)) { - console.log(`Repository already exists at ${dir}`); - return; - } - console.log(`Cloning ${repoUrl} to ${dir}...`); - execSync(`git clone --depth 1 ${repoUrl} ${dir}`, { stdio: "inherit" }); -} - -/** - * Recursively walk a directory and return all file paths - * Ignores .git, node_modules, dist, target directories - */ -export function walkFiles(dir: string): string[] { - const ignoreDirs = new Set([".git", "node_modules", "dist", "target", ".next", "build", "__pycache__"]); - const results: string[] = []; - - function walk(currentDir: string): void { - const entries = readdirSync(currentDir, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = join(currentDir, entry.name); - - if (entry.isDirectory()) { - if (!ignoreDirs.has(entry.name)) { - walk(fullPath); - } - } else if (entry.isFile()) { - results.push(fullPath); - } - } - } - - walk(dir); - return results; -} - -/** - * Check if a file has a code-like extension - */ -export function isCodeFile(filePath: string): boolean { - const codeExtensions = new Set([ - ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", - ".py", ".rs", ".go", ".java", ".c", ".cpp", ".h", ".hpp", - ".rb", ".php", ".swift", ".kt", ".scala", ".cs", - ".vue", ".svelte", ".astro", - ".json", ".yaml", ".yml", ".toml", - ".md", ".mdx", ".txt", - ".sh", ".bash", ".zsh", - ".sql", ".graphql", ".prisma", - ".css", ".scss", ".less", - ".html", ".xml" - ]); - return codeExtensions.has(extname(filePath).toLowerCase()); -} - -/** - * Chunk file content by lines - */ -export function chunkFileByLines(text: string, linesPerChunk: number = 80): string[] { - const lines = text.split("\n"); - const chunks: string[] = []; - - for (let i = 0; i < lines.length; i += linesPerChunk) { - const chunk = lines.slice(i, i + linesPerChunk).join("\n"); - if (chunk.trim().length > 0) { - chunks.push(chunk); - } - } - - return chunks; -} - -/** - * Read a file and return its contents, handling errors gracefully - */ -export function readFileSafe(filePath: string): string | null { - try { - const stat = statSync(filePath); - // Skip files larger than 1MB - if (stat.size > 1024 * 1024) { - return null; - } - return readFileSync(filePath, "utf-8"); - } catch { - return null; - } -} - -/** - * Pick k candidates from chunks (for rerank benchmark) - */ -export function pickCandidates(chunks: string[], k: number): string[] { - return chunks.slice(0, k); -} - -/** - * Load all code chunks from a repository - */ -export function loadRepoChunks(repoDir: string, linesPerChunk: number = 80): string[] { - const files = walkFiles(repoDir); - const codeFiles = files.filter(isCodeFile); - const allChunks: string[] = []; - - for (const file of codeFiles) { - const content = readFileSafe(file); - if (content) { - const chunks = chunkFileByLines(content, linesPerChunk); - allChunks.push(...chunks); - } - } - - return allChunks; -} - -/** - * Chunk metadata for tracking file/line mapping - */ -export interface ChunkMeta { - id: number; - file: string; // relative to repo root - startLine: number; // 1-indexed - endLine: number; // 1-indexed, inclusive - text: string; -} - -/** - * Load all code chunks from a repository with metadata - * Returns chunks with file/line info for ground truth matching - * - * @param repoDir - The repository directory path - * @param linesPerChunk - Lines per chunk (default 80) - * @param repoName - Optional repo name to prefix paths (for CodeAtlas compatibility) - */ -export function loadRepoChunksWithMeta( - repoDir: string, - linesPerChunk: number = 80, - repoName?: string -): ChunkMeta[] { - const files = walkFiles(repoDir); - const codeFiles = files.filter(isCodeFile); - const allChunks: ChunkMeta[] = []; - let chunkId = 0; - - // Normalize repoDir - remove leading ./ and trailing / for consistent matching - const normalizedRepoDir = repoDir.replace(/^\.\//, '').replace(/\/+$/, ''); - - for (const file of codeFiles) { - const content = readFileSafe(file); - if (!content) continue; - - // Normalize file path too - remove leading ./ - const normalizedFile = file.replace(/^\.\//, ''); - - // Get relative path from repo root - let relativePath = normalizedFile.startsWith(normalizedRepoDir + '/') - ? normalizedFile.slice(normalizedRepoDir.length + 1) - : normalizedFile; - - // If repoName provided, prefix the path (for CodeAtlas format compatibility) - // CodeAtlas uses paths like "aiohttp/web_protocol.py" - if (repoName) { - relativePath = `${repoName}/${relativePath}`; - } - - const lines = content.split("\n"); - - for (let i = 0; i < lines.length; i += linesPerChunk) { - const chunkLines = lines.slice(i, i + linesPerChunk); - const text = chunkLines.join("\n"); - - if (text.trim().length > 0) { - allChunks.push({ - id: chunkId++, - file: relativePath, - startLine: i + 1, // 1-indexed - endLine: Math.min(i + linesPerChunk, lines.length), // 1-indexed, inclusive - text, - }); - } - } - } - - return allChunks; -} - -/** - * Check if a chunk overlaps with a positive span - * Handles inconsistent CodeAtlas file path formats (some have repo prefix, some don't) - */ -export function chunkOverlapsSpan( - chunk: ChunkMeta, - span: { file: string; start_line: number; end_line: number } -): boolean { - // Normalize paths - remove leading slashes - const chunkFile = chunk.file.replace(/^\//, ''); - const spanFile = span.file.replace(/^\//, ''); - - // Check file match: - // 1. Exact match: "aiohttp/web_protocol.py" === "aiohttp/web_protocol.py" - // 2. Chunk ends with span: "chi/mux.go" ends with "/mux.go" (for span "mux.go") - // 3. Span ends with chunk: "mux.go" (span without prefix) matches end of "chi/mux.go" - const filesMatch = - chunkFile === spanFile || - chunkFile.endsWith('/' + spanFile) || - spanFile.endsWith('/' + chunkFile); - - if (!filesMatch) return false; - - // Check line overlap: chunk.startLine <= span.end_line AND chunk.endLine >= span.start_line - return chunk.startLine <= span.end_line && chunk.endLine >= span.start_line; -} diff --git a/osgrep-core/bench/codeatlas-system.ts b/osgrep-core/bench/codeatlas-system.ts deleted file mode 100644 index 5f26173e..00000000 --- a/osgrep-core/bench/codeatlas-system.ts +++ /dev/null @@ -1,511 +0,0 @@ -/** - * CodeAtlas System Benchmark - * - * Tests the full osgrep pipeline: - * 1. Chunk repo with actual chunker (80 lines) - * 2. Dense retrieval (ORT granite-30m) → top 100 candidates - * 3. ColBERT rerank (pre-indexed) → top 5 results - * 4. Score hits by checking chunk/span overlap - */ - -import { readFileSync, existsSync } from "fs"; -import { join, extname } from "path"; -import { - loadRepoChunksWithMeta, - ChunkMeta, - chunkOverlapsSpan, -} from "./_util.js"; - -// @ts-ignore - generated at build time -import { - initDenseOrt, - denseEmbedOrt, - initColbertOrt, - colbertPreindexDocs, - colbertRerankPreindexed, -} from "../index.js"; - -// ============================================================================= -// Configuration -// ============================================================================= - -const CODEATLAS_PATH = "./codeAtlas/artifacts/codeatlas.jsonl"; -const REPOS_DIR = "./codeAtlas/repos_to_test"; - -// Models -const DENSE_MODEL_REPO = "onnx-community/granite-embedding-30m-english-ONNX"; -const DENSE_HIDDEN_SIZE = 384; -const COLBERT_MODEL_REPO = "ryandono/osgrep-17m-v1-onnx"; -const COLBERT_HIDDEN_SIZE = 48; - -// Retrieval params -const DENSE_TOP_K = 100; -const COLBERT_TOP_K = 5; -const LINES_PER_CHUNK = Number(process.env.LINES_PER_CHUNK ?? 80); -const EXCLUDE_NON_CODE = (process.env.EXCLUDE_NON_CODE ?? "1") !== "0"; -const INCLUDE_PATH_HEADER = (process.env.INCLUDE_PATH_HEADER ?? "0") !== "0"; -const EVAL_DENSE_ONLY = (process.env.EVAL_DENSE_ONLY ?? "1") !== "0"; -const EVAL_COLBERT_ONLY = (process.env.EVAL_COLBERT_ONLY ?? "0") !== "0"; -const HYBRID_ALPHA = process.env.HYBRID_ALPHA ? Number(process.env.HYBRID_ALPHA) : null; - -// Repos to test (start with smaller repos for quick validation) -const TEST_REPOS = (() => { - const env = (process.env.TEST_REPOS ?? "").trim(); - if (!env) { - return new Set([ - "chi", - ]); - } - return new Set(env.split(",").map(s => s.trim()).filter(Boolean)); -})(); - -// ============================================================================= -// Types -// ============================================================================= - -interface CodeAtlasRow { - repo: string; - query: string; - positives: Array<{ file: string; start_line: number; end_line: number; snippet?: string }>; - negatives?: Array<{ file: string; start_line: number; end_line: number; snippet?: string }>; - category?: string; - difficulty?: string; -} - -interface QueryResult { - query: string; - repo: string; - category?: string; - difficulty?: string; - denseRecallAt100: boolean; - denseHitAt5: boolean; - denseMrrAt5: number; - colbertOnlyHitAt5?: boolean; - colbertOnlyMrrAt5?: number; - hybridHitAt5?: boolean; - hybridMrrAt5?: number; - hitAt1: boolean; - hitAt5: boolean; - mrrAt5: number; - bestRankAt5: number; - numPositives: number; - numChunks: number; - timeMs: number; -} - -// ============================================================================= -// Dense Retrieval -// ============================================================================= - -function cosineSimilarity(a: number[], b: number[]): number { - let dot = 0; - let normA = 0; - let normB = 0; - for (let i = 0; i < a.length; i++) { - dot += a[i] * b[i]; - normA += a[i] * a[i]; - normB += b[i] * b[i]; - } - return dot / (Math.sqrt(normA) * Math.sqrt(normB) + 1e-12); -} - -function denseRetrieveScored( - queryText: string, - docEmbeddings: number[][], - topK: number -): Array<{ idx: number; score: number }> { - // Encode query - const queryResult = denseEmbedOrt([queryText], true); - const queryEmb = Array.from(queryResult.embeddings); - - // Compute similarities - const scores: Array<{ idx: number; score: number }> = []; - for (let i = 0; i < docEmbeddings.length; i++) { - const score = cosineSimilarity(queryEmb, docEmbeddings[i]); - scores.push({ idx: i, score }); - } - - // Sort descending and take top K - scores.sort((a, b) => b.score - a.score); - return scores.slice(0, topK); -} - -function minMaxNormalize(values: number[]): number[] { - if (values.length === 0) return values; - let min = Infinity; - let max = -Infinity; - for (const v of values) { - if (v < min) min = v; - if (v > max) max = v; - } - const range = max - min; - if (range <= 1e-12) return values.map(() => 0.5); - return values.map(v => (v - min) / range); -} - -// ============================================================================= -// Metrics -// ============================================================================= - -function computeMetricsAtK( - rankedIndices: number[], - chunks: ChunkMeta[], - positives: CodeAtlasRow["positives"] -): { hitAt1: boolean; hitAt5: boolean; mrr: number; bestRank: number } { - let bestRank = Infinity; - - for (let rank = 0; rank < rankedIndices.length; rank++) { - const chunkIdx = rankedIndices[rank]; - const chunk = chunks[chunkIdx]; - - // Check if this chunk overlaps any positive span - const isHit = positives.some(pos => chunkOverlapsSpan(chunk, pos)); - if (isHit && rank + 1 < bestRank) { - bestRank = rank + 1; // 1-indexed rank - } - } - - if (bestRank === Infinity) { - return { hitAt1: false, hitAt5: false, mrr: 0, bestRank: -1 }; - } - - return { - hitAt1: bestRank === 1, - hitAt5: bestRank <= 5, - mrr: 1 / bestRank, - bestRank, - }; -} - -function positiveChunkIndices(chunks: ChunkMeta[], positives: CodeAtlasRow["positives"]): Set<number> { - const indices = new Set<number>(); - for (let i = 0; i < chunks.length; i++) { - const chunk = chunks[i]; - if (positives.some(pos => chunkOverlapsSpan(chunk, pos))) { - indices.add(i); - } - } - return indices; -} - -function isLikelyCodeFile(filePath: string): boolean { - const ext = extname(filePath).toLowerCase(); - // Allowlist focused on code; excludes docs/JSON noise that often dominates rerank. - const allow = new Set([ - ".py", ".pyx", - ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", - ".rs", ".go", - ".java", ".kt", ".scala", - ".cs", - ".rb", ".php", ".ex", ".exs", - ".c", ".cc", ".cpp", ".h", ".hpp", - ".swift", - ".sql", ".proto", - ]); - return allow.has(ext); -} - -// ============================================================================= -// Main Benchmark -// ============================================================================= - -async function main() { - console.log("=== CodeAtlas System Benchmark ===\n"); - - // 1. Load CodeAtlas data - console.log("Loading CodeAtlas data..."); - if (!existsSync(CODEATLAS_PATH)) { - console.error(`CodeAtlas file not found: ${CODEATLAS_PATH}`); - process.exit(1); - } - - const lines = readFileSync(CODEATLAS_PATH, "utf-8").split("\n").filter(Boolean); - const allRows: CodeAtlasRow[] = lines.map(l => JSON.parse(l)); - - // Filter to test repos - const rows = allRows.filter(r => TEST_REPOS.has(r.repo)); - console.log(` Total rows: ${allRows.length}`); - console.log(` Test repos: ${Array.from(TEST_REPOS).join(", ")}`); - console.log(` Filtered rows: ${rows.length}\n`); - - // 2. Group by repo - const byRepo = new Map<string, CodeAtlasRow[]>(); - for (const row of rows) { - if (!byRepo.has(row.repo)) byRepo.set(row.repo, []); - byRepo.get(row.repo)!.push(row); - } - - // 3. Initialize models - console.log("Initializing models..."); - const initStart = performance.now(); - initDenseOrt(DENSE_MODEL_REPO, DENSE_HIDDEN_SIZE); - initColbertOrt(COLBERT_MODEL_REPO, COLBERT_HIDDEN_SIZE); - const initTime = performance.now() - initStart; - console.log(` Models loaded in ${initTime.toFixed(0)}ms\n`); - - // 4. Process each repo - const allResults: QueryResult[] = []; - let totalIndexTime = 0; - let totalQueryTime = 0; - - for (const [repo, repoRows] of byRepo) { - const repoDir = join(REPOS_DIR, repo); - if (!existsSync(repoDir)) { - console.log(`⚠ Skipping ${repo} - repo not found at ${repoDir}`); - continue; - } - - console.log(`\n--- Processing ${repo} (${repoRows.length} queries) ---`); - - // 4a. Chunk the repo - console.log(` Chunking...`); - const chunkStart = performance.now(); - // Pass repo name to match CodeAtlas file path format (e.g., "aiohttp/web_protocol.py") - let chunks = loadRepoChunksWithMeta(repoDir, LINES_PER_CHUNK, repo); - if (EXCLUDE_NON_CODE) { - chunks = chunks.filter(c => isLikelyCodeFile(c.file)); - } - const chunkTime = performance.now() - chunkStart; - console.log(` ${chunks.length} chunks in ${chunkTime.toFixed(0)}ms`); - - if (chunks.length === 0) { - console.log(` ⚠ No chunks, skipping`); - continue; - } - - // 4b. Dense embed all chunks - console.log(` Dense indexing...`); - const denseStart = performance.now(); - const chunkTexts = chunks.map(c => { - if (!INCLUDE_PATH_HEADER) return c.text; - return `FILE: ${c.file}\nLINES: ${c.startLine}-${c.endLine}\n${c.text}`; - }); - - // Batch encode in groups of 64 to avoid memory issues - const batchSize = 64; - const allDocEmbeddings: number[][] = []; - - for (let i = 0; i < chunkTexts.length; i += batchSize) { - const batch = chunkTexts.slice(i, i + batchSize); - const result = denseEmbedOrt(batch, true); - const embeddings = Array.from(result.embeddings); - const hs = result.hiddenSize; - - for (let j = 0; j < batch.length; j++) { - const start = j * hs; - const end = start + hs; - allDocEmbeddings.push(embeddings.slice(start, end)); - } - } - - const denseTime = performance.now() - denseStart; - console.log(` Dense indexed in ${denseTime.toFixed(0)}ms`); - - // 4c. ColBERT preindex all chunks - console.log(` ColBERT preindexing...`); - const colbertStart = performance.now(); - colbertPreindexDocs(chunkTexts); - const colbertTime = performance.now() - colbertStart; - console.log(` ColBERT preindexed in ${colbertTime.toFixed(0)}ms`); - - totalIndexTime += denseTime + colbertTime; - - // 4d. Run queries - console.log(` Running queries...`); - for (const row of repoRows) { - const queryStart = performance.now(); - const posSet = positiveChunkIndices(chunks, row.positives); - - // Dense retrieval → top 100 - const denseTopKScored = denseRetrieveScored( - row.query, - allDocEmbeddings, - DENSE_TOP_K - ); - const denseTopK = denseTopKScored.map(s => s.idx); - const denseRecallAt100 = denseTopK.some(i => posSet.has(i)); - const denseTop5 = denseTopK.slice(0, Math.min(5, denseTopK.length)); - const denseMetricsAt5 = EVAL_DENSE_ONLY ? computeMetricsAtK(denseTop5, chunks, row.positives) : { hitAt1: false, hitAt5: false, mrr: 0, bestRank: -1 }; - - // ColBERT rerank → top 5 - // colbertRerankPreindexed returns indices into the candidate set (denseTopK) - // and those are already mapped back to original doc indices by the Rust code - const colbertResult = colbertRerankPreindexed( - row.query, - denseTopK, // Pass the candidate indices - HYBRID_ALPHA !== null ? denseTopK.length : COLBERT_TOP_K - ); - - // colbertResult.indices contains the original chunk indices (not indices into denseTopK) - const finalRanking = Array.from(colbertResult.indices) as number[]; - - const queryTime = performance.now() - queryStart; - totalQueryTime += queryTime; - - // Validate indices before computing metrics - const validRanking = finalRanking.filter(idx => idx >= 0 && idx < chunks.length); - if (validRanking.length === 0) { - console.log(` ⚠ No valid rankings for query: ${row.query.slice(0, 50)}...`); - continue; - } - - // Score - const metricsAt5 = computeMetricsAtK(validRanking.slice(0, 5), chunks, row.positives); - - // Optional hybrid: combine dense + colbert within denseTopK to stabilize reranking. - // Hybrid ranking computed as: alpha * norm(colbert) + (1-alpha) * norm(dense) - let hybridMetricsAt5: { hitAt1: boolean; hitAt5: boolean; mrr: number; bestRank: number } | null = null; - if (HYBRID_ALPHA !== null && !Number.isNaN(HYBRID_ALPHA) && HYBRID_ALPHA >= 0 && HYBRID_ALPHA <= 1) { - const alpha = HYBRID_ALPHA; - const denseScores = denseTopKScored.map(s => s.score); - const denseNorm = minMaxNormalize(denseScores); - - const colbertIdx = Array.from(colbertResult.indices) as number[]; - const colbertScores = Array.from(colbertResult.scores) as number[]; - const colbertScoreByChunk = new Map<number, number>(); - for (let i = 0; i < colbertIdx.length; i++) colbertScoreByChunk.set(colbertIdx[i], colbertScores[i]); - - const colbertScoresAligned = denseTopK.map(idx => colbertScoreByChunk.get(idx) ?? 0); - const colbertNorm = minMaxNormalize(colbertScoresAligned); - - const hybridScored = denseTopK.map((idx, i) => ({ - idx, - score: alpha * colbertNorm[i] + (1 - alpha) * denseNorm[i], - })); - hybridScored.sort((a, b) => b.score - a.score); - const hybridTop5 = hybridScored.slice(0, 5).map(s => s.idx); - hybridMetricsAt5 = computeMetricsAtK(hybridTop5, chunks, row.positives); - } - - let colbertOnlyMetricsAt5: { hitAt1: boolean; hitAt5: boolean; mrr: number; bestRank: number } | null = null; - if (EVAL_COLBERT_ONLY) { - const allIdx = Array.from({ length: chunks.length }, (_, i) => i); - const allRes = colbertRerankPreindexed(row.query, allIdx, 5); - const allRank = (Array.from(allRes.indices) as number[]).filter(idx => idx >= 0 && idx < chunks.length); - colbertOnlyMetricsAt5 = computeMetricsAtK(allRank, chunks, row.positives); - } - - // Debug first few queries - if (allResults.length < 3) { - console.log(`\n DEBUG Query: "${row.query.slice(0, 50)}..."`); - console.log(` Positives: ${row.positives.map(p => `${p.file}:${p.start_line}-${p.end_line}`).join(", ")}`); - console.log(` #positive chunks: ${posSet.size} dense@100 hit: ${denseRecallAt100}`); - console.log(` Top 5 chunks: ${validRanking.slice(0, 5).map(i => `${chunks[i].file}:${chunks[i].startLine}-${chunks[i].endLine}`).join(", ")}`); - console.log(` Best rank@5: ${metricsAt5.bestRank}, Hit@1: ${metricsAt5.hitAt1}, Hit@5: ${metricsAt5.hitAt5}`); - } - - allResults.push({ - query: row.query, - repo, - category: row.category, - difficulty: row.difficulty, - denseRecallAt100, - denseHitAt5: denseMetricsAt5.hitAt5, - denseMrrAt5: denseMetricsAt5.mrr, - colbertOnlyHitAt5: colbertOnlyMetricsAt5?.hitAt5, - colbertOnlyMrrAt5: colbertOnlyMetricsAt5?.mrr, - hybridHitAt5: hybridMetricsAt5?.hitAt5, - hybridMrrAt5: hybridMetricsAt5?.mrr, - hitAt1: metricsAt5.hitAt1, - hitAt5: metricsAt5.hitAt5, - mrrAt5: metricsAt5.mrr, - bestRankAt5: metricsAt5.bestRank, - numPositives: row.positives.length, - numChunks: chunks.length, - timeMs: queryTime, - }); - } - } - - // ============================================================================= - // Report Results - // ============================================================================= - - console.log("\n" + "=".repeat(60)); - console.log("=== RESULTS ==="); - console.log("=".repeat(60) + "\n"); - - const n = allResults.length; - if (n === 0) { - console.log("No results to report"); - return; - } - - // Overall metrics - const hitAt1 = allResults.filter(r => r.hitAt1).length / n; - const hitAt5 = allResults.filter(r => r.hitAt5).length / n; - const denseRecallAt100 = allResults.filter(r => r.denseRecallAt100).length / n; - const denseHitAt5 = allResults.filter(r => r.denseHitAt5).length / n; - const denseMrrAt5 = allResults.reduce((sum, r) => sum + r.denseMrrAt5, 0) / n; - const mrrAt5 = allResults.reduce((sum, r) => sum + r.mrrAt5, 0) / n; - const hybridHitAt5 = allResults.filter(r => r.hybridHitAt5).length / n; - const hybridMrrAt5 = allResults.reduce((sum, r) => sum + (r.hybridMrrAt5 ?? 0), 0) / n; - const avgTime = allResults.reduce((sum, r) => sum + r.timeMs, 0) / n; - - console.log(`OVERALL (n=${n})`); - console.log(` Dense@100: ${(denseRecallAt100 * 100).toFixed(1)}%`); - if (EVAL_DENSE_ONLY) { - console.log(` Dense Hit@5: ${(denseHitAt5 * 100).toFixed(1)}% Dense MRR@5: ${denseMrrAt5.toFixed(3)}`); - } - if (HYBRID_ALPHA !== null && !Number.isNaN(HYBRID_ALPHA)) { - console.log(` Hybrid(a=${HYBRID_ALPHA.toFixed(2)}) Hit@5: ${(hybridHitAt5 * 100).toFixed(1)}% Hybrid MRR@5: ${hybridMrrAt5.toFixed(3)}`); - } - console.log(` Hit@1: ${(hitAt1 * 100).toFixed(1)}%`); - console.log(` Hit@5: ${(hitAt5 * 100).toFixed(1)}%`); - console.log(` MRR@5: ${mrrAt5.toFixed(3)}`); - console.log(` Avg query: ${avgTime.toFixed(1)}ms`); - console.log(); - - // By difficulty - const byDiff = new Map<string, QueryResult[]>(); - for (const r of allResults) { - const d = r.difficulty || "unknown"; - if (!byDiff.has(d)) byDiff.set(d, []); - byDiff.get(d)!.push(r); - } - - console.log("BY DIFFICULTY"); - for (const [diff, results] of byDiff) { - const dn = results.length; - const h1 = results.filter(r => r.hitAt1).length / dn; - const h5 = results.filter(r => r.hitAt5).length / dn; - const dr = results.filter(r => r.denseRecallAt100).length / dn; - const m = results.reduce((sum, r) => sum + r.mrrAt5, 0) / dn; - const dh5 = results.filter(r => r.denseHitAt5).length / dn; - const dm = results.reduce((sum, r) => sum + r.denseMrrAt5, 0) / dn; - const densePart = EVAL_DENSE_ONLY ? ` Dense@5: ${(dh5 * 100).toFixed(1).padStart(5)}% dMRR@5: ${dm.toFixed(3)}` : ""; - const hh5 = results.filter(r => r.hybridHitAt5).length / dn; - const hm = results.reduce((sum, r) => sum + (r.hybridMrrAt5 ?? 0), 0) / dn; - const hybridPart = HYBRID_ALPHA !== null ? ` Hybrid@5: ${(hh5 * 100).toFixed(1).padStart(5)}% hMRR@5: ${hm.toFixed(3)}` : ""; - console.log(` ${diff.padEnd(10)} Dense@100: ${(dr * 100).toFixed(1).padStart(5)}%${densePart}${hybridPart} Hit@1: ${(h1 * 100).toFixed(1).padStart(5)}% Hit@5: ${(h5 * 100).toFixed(1).padStart(5)}% MRR@5: ${m.toFixed(3)} (n=${dn})`); - } - console.log(); - - // By repo - console.log("BY REPO"); - for (const [repo, repoRows] of byRepo) { - const results = allResults.filter(r => r.repo === repo); - if (results.length === 0) continue; - const rn = results.length; - const h1 = results.filter(r => r.hitAt1).length / rn; - const h5 = results.filter(r => r.hitAt5).length / rn; - const dr = results.filter(r => r.denseRecallAt100).length / rn; - const m = results.reduce((sum, r) => sum + r.mrrAt5, 0) / rn; - const dh5 = results.filter(r => r.denseHitAt5).length / rn; - const dm = results.reduce((sum, r) => sum + r.denseMrrAt5, 0) / rn; - const densePart = EVAL_DENSE_ONLY ? ` Dense@5: ${(dh5 * 100).toFixed(1).padStart(5)}% dMRR@5: ${dm.toFixed(3)}` : ""; - const hh5 = results.filter(r => r.hybridHitAt5).length / rn; - const hm = results.reduce((sum, r) => sum + (r.hybridMrrAt5 ?? 0), 0) / rn; - const hybridPart = HYBRID_ALPHA !== null ? ` Hybrid@5: ${(hh5 * 100).toFixed(1).padStart(5)}% hMRR@5: ${hm.toFixed(3)}` : ""; - console.log(` ${repo.padEnd(12)} Dense@100: ${(dr * 100).toFixed(1).padStart(5)}%${densePart}${hybridPart} Hit@1: ${(h1 * 100).toFixed(1).padStart(5)}% Hit@5: ${(h5 * 100).toFixed(1).padStart(5)}% MRR@5: ${m.toFixed(3)} (n=${rn})`); - } - console.log(); - - // Timing summary - console.log("TIMING"); - console.log(` Total index time: ${totalIndexTime.toFixed(0)}ms`); - console.log(` Total query time: ${totalQueryTime.toFixed(0)}ms`); - console.log(` Throughput: ${(1000 / avgTime).toFixed(1)} queries/sec`); -} - -main().catch(console.error); diff --git a/osgrep-core/bench/colbert-parity-rust.ts b/osgrep-core/bench/colbert-parity-rust.ts deleted file mode 100644 index 68ee7585..00000000 --- a/osgrep-core/bench/colbert-parity-rust.ts +++ /dev/null @@ -1,121 +0,0 @@ -/** - * ColBERT Parity (Rust vs Rust) - * - * Compares: - * - `colbertRerankPreindexed` (packed fast path) - * - `colbertRerankOrt` (encode docs at query-time) - * - * This avoids Python/onnxruntime issues and validates that packed scoring + index - * mapping are correct. - * - * Input: JSONL produced by `bench/colbert-parity.ts` - * - * Usage: - * source ~/.cargo/env - * bun run bench/colbert-parity-rust.ts --in /tmp/colbert-parity-chi.jsonl --topk 20 - */ - -import { readFileSync } from "fs"; - -// @ts-ignore - generated at build time -import { initColbertOrt, colbertPreindexDocs, colbertRerankOrt, colbertRerankPreindexed } from "../index.js"; - -function parseArgs(argv: string[]) { - const args: Record<string, string> = {}; - for (let i = 2; i < argv.length; i++) { - const a = argv[i]; - if (!a.startsWith("--")) continue; - const key = a.slice(2); - const val = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : "1"; - args[key] = val; - } - return args; -} - -type ParityMeta = { - type: "meta"; - repo: string; - colbertModelRepo: string; - colbertHiddenSize: number; - topk: number; -}; - -type ParityQuery = { - type: "query"; - queryIndex: number; - query: string; - candidates: Array<{ chunkIndex: number; text: string }>; - rust: { indices: number[]; scores: number[] }; -}; - -function topKOverlap(a: number[], b: number[], k: number): number { - const sa = new Set(a.slice(0, k)); - const sb = new Set(b.slice(0, k)); - let inter = 0; - for (const x of sa) if (sb.has(x)) inter++; - return inter / k; -} - -async function main() { - const args = parseArgs(process.argv); - const inPath = args.in; - const topk = Number(args.topk ?? 20); - if (!inPath) throw new Error("Missing --in /path/to/parity.jsonl"); - - const lines = readFileSync(inPath, "utf8").split("\n").filter(Boolean); - const meta = JSON.parse(lines[0]) as ParityMeta; - const queries = lines.slice(1).map(l => JSON.parse(l) as ParityQuery); - - initColbertOrt(meta.colbertModelRepo, meta.colbertHiddenSize); - - // Re-preindex docs in the same order as the export expects. - // We can just union all candidate docs (order stable within each query) into a global - // list and preindex them; but preindexed rerank expects global doc indices. - // The export already used repo-level chunk indices, so for this check we instead - // rebuild a *local* packed store per query: preindex the candidate docs only. - // - // That lets us compare packed-vs-nonpacked scoring without needing the full repo. - let n = 0; - let top1Match = 0; - let top5OverlapSum = 0; - - for (const q of queries) { - const docs = q.candidates.map(c => c.text); - if (docs.length === 0) continue; - - colbertPreindexDocs(docs); - - // Packed path (indices will be local [0..docs.length)) - const packed = colbertRerankPreindexed(q.query, docs.map((_, i) => i), topk); - const packedPos = Array.from(packed.indices) as number[]; - - // Non-packed path (indices are local [0..docs.length)) - const nonPacked = colbertRerankOrt(q.query, docs, topk); - const nonPackedPos = Array.from(nonPacked.indices) as number[]; - - if (packedPos.length > 0 && nonPackedPos.length > 0 && packedPos[0] === nonPackedPos[0]) { - top1Match++; - } - top5OverlapSum += topKOverlap(packedPos, nonPackedPos, Math.min(5, topk)); - n++; - } - - if (n === 0) { - console.log("No comparable queries"); - return; - } - - console.log("============================================================"); - console.log("Rust vs Rust Parity (preindexed vs query-time)"); - console.log("============================================================"); - console.log(`Input: ${inPath}`); - console.log(`Queries compared: ${n}`); - console.log(`Top-1 match: ${(top1Match / n * 100).toFixed(1)}%`); - console.log(`Top-5 overlap: ${(top5OverlapSum / n).toFixed(3)}`); -} - -main().catch((e) => { - console.error(e); - process.exit(1); -}); - diff --git a/osgrep-core/bench/colbert-parity.ts b/osgrep-core/bench/colbert-parity.ts deleted file mode 100644 index 28760e04..00000000 --- a/osgrep-core/bench/colbert-parity.ts +++ /dev/null @@ -1,188 +0,0 @@ -/** - * ColBERT Parity Export - * - * Generates an apples-to-apples dataset to compare: - * - Rust/ORT implementation (preindexed rerank) - * - Python ONNX MaxSim scorer (should match Rust ranking) - * - * Output JSONL contains per-query candidate docs + Rust top-k. - * - * Usage: - * source ~/.cargo/env - * bun run bench/colbert-parity.ts --repo chi --n 30 --lines 20 --dense-topk 100 --topk 20 --out /tmp/parity.jsonl - */ -import { existsSync, writeFileSync } from "fs"; -import { join } from "path"; -import { loadRepoChunksWithMeta } from "./_util.js"; - -// @ts-ignore - generated at build time -import { - initDenseOrt, - denseEmbedOrt, - initColbertOrt, - colbertPreindexDocs, - colbertRerankPreindexed, -} from "../index.js"; - -type PositiveSpan = { file: string; start_line: number; end_line: number }; - -type CodeAtlasRow = { - repo: string; - query: string; - positives: PositiveSpan[]; - difficulty?: string; - category?: string; -}; - -function parseArgs(argv: string[]) { - const args: Record<string, string> = {}; - for (let i = 2; i < argv.length; i++) { - const a = argv[i]; - if (!a.startsWith("--")) continue; - const key = a.slice(2); - const val = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : "1"; - args[key] = val; - } - return args; -} - -function cosineSimilarity(a: number[], b: number[]): number { - let dot = 0; - let normA = 0; - let normB = 0; - for (let i = 0; i < a.length; i++) { - dot += a[i] * b[i]; - normA += a[i] * a[i]; - normB += b[i] * b[i]; - } - return dot / (Math.sqrt(normA) * Math.sqrt(normB) + 1e-12); -} - -function denseTopK(query: string, docEmbeddings: number[][], topK: number): number[] { - const queryResult = denseEmbedOrt([query], true); - const queryEmb = Array.from(queryResult.embeddings) as number[]; - - const scored: Array<{ idx: number; score: number }> = []; - for (let i = 0; i < docEmbeddings.length; i++) { - scored.push({ idx: i, score: cosineSimilarity(queryEmb, docEmbeddings[i]) }); - } - scored.sort((a, b) => b.score - a.score); - return scored.slice(0, topK).map(s => s.idx); -} - -async function main() { - const args = parseArgs(process.argv); - - const repo = args.repo ?? "chi"; - const n = Number(args.n ?? 30); - const linesPerChunk = Number(args.lines ?? 20); - const denseTopk = Number(args["dense-topk"] ?? 100); - const topk = Number(args.topk ?? 20); - const outPath = args.out ?? `/tmp/colbert-parity-${repo}.jsonl`; - - const codeatlasPath = args.codeatlas ?? "./codeAtlas/artifacts/codeatlas.jsonl"; - const reposDir = args.repos ?? "./codeAtlas/repos_to_test"; - - const denseModelRepo = args["dense-model"] ?? "onnx-community/granite-embedding-30m-english-ONNX"; - const denseHiddenSize = Number(args["dense-dim"] ?? 384); - const colbertModelRepo = args["colbert-model"] ?? "ryandono/osgrep-17m-v1-onnx"; - const colbertHiddenSize = Number(args["colbert-dim"] ?? 48); - - if (!existsSync(codeatlasPath)) { - throw new Error(`Missing CodeAtlas JSONL at ${codeatlasPath}`); - } - - const repoDir = join(reposDir, repo); - if (!existsSync(repoDir)) { - throw new Error(`Missing repo dir at ${repoDir}`); - } - - const rows: CodeAtlasRow[] = []; - for (const line of require("fs").readFileSync(codeatlasPath, "utf8").split("\n")) { - if (!line) continue; - const r = JSON.parse(line) as CodeAtlasRow; - if (r.repo === repo) rows.push(r); - } - const picked = rows.slice(0, n); - if (picked.length === 0) throw new Error(`No rows for repo=${repo}`); - - console.log(`[parity] repo=${repo} queries=${picked.length} linesPerChunk=${linesPerChunk}`); - - console.log(`[parity] init dense=${denseModelRepo}`); - initDenseOrt(denseModelRepo, denseHiddenSize); - console.log(`[parity] init colbert=${colbertModelRepo}`); - initColbertOrt(colbertModelRepo, colbertHiddenSize); - - console.log(`[parity] chunking...`); - const chunks = loadRepoChunksWithMeta(repoDir, linesPerChunk, repo); - const chunkTexts = chunks.map(c => c.text); - - console.log(`[parity] dense embedding docs=${chunkTexts.length}...`); - const batchSize = 64; - const allDocEmbeddings: number[][] = []; - for (let i = 0; i < chunkTexts.length; i += batchSize) { - const batch = chunkTexts.slice(i, i + batchSize); - const res = denseEmbedOrt(batch, true); - const flat = Array.from(res.embeddings) as number[]; - const hs = res.hiddenSize as number; - for (let j = 0; j < batch.length; j++) { - allDocEmbeddings.push(flat.slice(j * hs, (j + 1) * hs)); - } - } - - console.log(`[parity] preindex colbert docs=${chunkTexts.length}...`); - colbertPreindexDocs(chunkTexts); - - const outLines: string[] = []; - outLines.push(JSON.stringify({ - type: "meta", - repo, - out: outPath, - denseModelRepo, - denseHiddenSize, - colbertModelRepo, - colbertHiddenSize, - linesPerChunk, - denseTopk, - topk, - totalChunks: chunks.length, - })); - - for (let i = 0; i < picked.length; i++) { - const row = picked[i]; - const candIdx = denseTopK(row.query, allDocEmbeddings, Math.min(denseTopk, chunks.length)); - const rust = colbertRerankPreindexed(row.query, candIdx, topk); - - const candidates = candIdx.map(idx => ({ - chunkIndex: idx, - file: chunks[idx]?.file ?? "", - startLine: chunks[idx]?.startLine ?? 0, - endLine: chunks[idx]?.endLine ?? 0, - text: chunkTexts[idx] ?? "", - })); - - outLines.push(JSON.stringify({ - type: "query", - repo, - queryIndex: i, - query: row.query, - positives: row.positives, - category: row.category, - difficulty: row.difficulty, - candidates, - rust: { - indices: Array.from(rust.indices as number[]), - scores: Array.from(rust.scores as number[]), - }, - })); - } - - writeFileSync(outPath, outLines.join("\n") + "\n", "utf8"); - console.log(`[parity] wrote ${outLines.length} lines -> ${outPath}`); -} - -main().catch((e) => { - console.error(e); - process.exit(1); -}); - diff --git a/osgrep-core/bench/dense-ort.ts b/osgrep-core/bench/dense-ort.ts deleted file mode 100644 index 7bdd2070..00000000 --- a/osgrep-core/bench/dense-ort.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { cloneRepoIfMissing, loadRepoChunks, walkFiles, isCodeFile } from "./_util.js"; - -// Import the native addon (will be built by napi) -// @ts-ignore - generated at build time -import { initDenseOrt, denseEmbedChecksumOrt } from "../index.js"; - -const REPO_URL = "https://github.com/sst/opencode.git"; -const REPO_DIR = "./opencode"; -const MODEL_REPO = "onnx-community/granite-embedding-30m-english-ONNX"; -const HIDDEN_SIZE = 384; // granite-embedding-30m-english has 384 dim -const BATCH_SIZE = 64; // Larger batch for throughput -const LINES_PER_CHUNK = 80; // Original chunk size - -async function main() { - console.log("=== Dense Embedding Benchmark (ONNX Runtime) ===\n"); - - // 1. Ensure repo exists - cloneRepoIfMissing(REPO_URL, REPO_DIR); - - // 2. Load chunks - console.log("Loading code files and chunking..."); - const files = walkFiles(REPO_DIR); - const codeFiles = files.filter(isCodeFile); - const chunks = loadRepoChunks(REPO_DIR, LINES_PER_CHUNK); - - console.log(` Files found: ${files.length}`); - console.log(` Code files: ${codeFiles.length}`); - console.log(` Total chunks: ${chunks.length}`); - console.log(` Batch size: ${BATCH_SIZE}`); - console.log(); - - // 3. Initialize model from HuggingFace ONNX repo - console.log(`Initializing ORT model from: ${MODEL_REPO}`); - initDenseOrt(MODEL_REPO, HIDDEN_SIZE); - console.log("Model initialized.\n"); - - // 4. Benchmark encoding - console.log("Starting benchmark..."); - const startTime = performance.now(); - let totalChecksum = 0; - let batchCount = 0; - - for (let i = 0; i < chunks.length; i += BATCH_SIZE) { - const batch = chunks.slice(i, i + BATCH_SIZE); - const checksum = denseEmbedChecksumOrt(batch, true); - totalChecksum += checksum; - batchCount++; - - // Progress indicator every batch - const elapsed = (performance.now() - startTime) / 1000; - const chunksProcessed = Math.min(i + BATCH_SIZE, chunks.length); - const rate = chunksProcessed / elapsed; - process.stdout.write(`\r Processed ${chunksProcessed}/${chunks.length} chunks (${rate.toFixed(1)} chunks/sec)`); - } - - const endTime = performance.now(); - const totalSeconds = (endTime - startTime) / 1000; - const chunksPerSec = chunks.length / totalSeconds; - - console.log("\n"); - console.log("=== Results ==="); - console.log(` File count: ${codeFiles.length}`); - console.log(` Chunk count: ${chunks.length}`); - console.log(` Total time: ${totalSeconds.toFixed(2)} seconds`); - console.log(` Throughput: ${chunksPerSec.toFixed(1)} chunks/sec`); - console.log(` Checksum: ${totalChecksum.toFixed(6)} (for validation)`); -} - -main().catch(console.error); diff --git a/osgrep-core/bench/dense.ts b/osgrep-core/bench/dense.ts deleted file mode 100644 index 92dd5bbf..00000000 --- a/osgrep-core/bench/dense.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { cloneRepoIfMissing, loadRepoChunks, walkFiles, isCodeFile } from "./_util.js"; - -// Import the native addon (will be built by napi) -// @ts-ignore - generated at build time -import { initDense, denseEmbedChecksum } from "../index.js"; - -const REPO_URL = "https://github.com/sst/opencode.git"; -const REPO_DIR = "./opencode"; -const MODEL_REPO = "ibm-granite/granite-embedding-30m-english"; -const BATCH_SIZE = 64; // Larger batch for throughput -const LINES_PER_CHUNK = 80; // Original chunk size - -async function main() { - console.log("=== Dense Embedding Benchmark ===\n"); - - // 1. Ensure repo exists - cloneRepoIfMissing(REPO_URL, REPO_DIR); - - // 2. Load chunks - console.log("Loading code files and chunking..."); - const files = walkFiles(REPO_DIR); - const codeFiles = files.filter(isCodeFile); - const chunks = loadRepoChunks(REPO_DIR, LINES_PER_CHUNK); - - console.log(` Files found: ${files.length}`); - console.log(` Code files: ${codeFiles.length}`); - console.log(` Total chunks: ${chunks.length}`); - console.log(` Batch size: ${BATCH_SIZE}`); - console.log(); - - // 3. Initialize model - console.log(`Initializing dense model: ${MODEL_REPO}`); - initDense(MODEL_REPO); - console.log("Model initialized.\n"); - - // 4. Benchmark encoding - console.log("Starting benchmark..."); - const startTime = performance.now(); - let totalChecksum = 0; - let batchCount = 0; - - for (let i = 0; i < chunks.length; i += BATCH_SIZE) { - const batch = chunks.slice(i, i + BATCH_SIZE); - const checksum = denseEmbedChecksum(batch, true); - totalChecksum += checksum; - batchCount++; - - // Progress indicator every batch - const elapsed = (performance.now() - startTime) / 1000; - const chunksProcessed = Math.min(i + BATCH_SIZE, chunks.length); - const rate = chunksProcessed / elapsed; - process.stdout.write(`\r Processed ${chunksProcessed}/${chunks.length} chunks (${rate.toFixed(1)} chunks/sec)`); - } - - const endTime = performance.now(); - const totalSeconds = (endTime - startTime) / 1000; - const chunksPerSec = chunks.length / totalSeconds; - - console.log("\n"); - console.log("=== Results ==="); - console.log(` File count: ${codeFiles.length}`); - console.log(` Chunk count: ${chunks.length}`); - console.log(` Total time: ${totalSeconds.toFixed(2)} seconds`); - console.log(` Throughput: ${chunksPerSec.toFixed(1)} chunks/sec`); - console.log(` Checksum: ${totalChecksum.toFixed(6)} (for validation)`); -} - -main().catch(console.error); diff --git a/osgrep-core/bench/rerank-ort.ts b/osgrep-core/bench/rerank-ort.ts deleted file mode 100644 index 89d50c09..00000000 --- a/osgrep-core/bench/rerank-ort.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { cloneRepoIfMissing, loadRepoChunks, walkFiles, isCodeFile } from "./_util.js"; - -// @ts-ignore - generated at build time -import { initColbertOrt, colbertRerankOrt } from "../index.js"; - -const REPO_URL = "https://github.com/sst/opencode.git"; -const REPO_DIR = "./opencode"; -const MODEL_REPO = "ryandono/osgrep-17m-v1-onnx"; -const HIDDEN_SIZE = 48; // osgrep-17m has 48-dim embeddings -const NUM_CANDIDATES = 100; // Rerank top 100 candidates -const TOP_K = 5; // Return top 5 results - -async function main() { - console.log("=== ColBERT Rerank Benchmark (ONNX Runtime) ===\n"); - - // 1. Ensure repo exists - cloneRepoIfMissing(REPO_URL, REPO_DIR); - - // 2. Load chunks - console.log("Loading code files and chunking..."); - const files = walkFiles(REPO_DIR); - const codeFiles = files.filter(isCodeFile); - const allChunks = loadRepoChunks(REPO_DIR, 80); - - console.log(` Files found: ${files.length}`); - console.log(` Code files: ${codeFiles.length}`); - console.log(` Total chunks: ${allChunks.length}`); - console.log(` Candidates per query: ${NUM_CANDIDATES}`); - console.log(` Top-K: ${TOP_K}`); - console.log(); - - // 3. Initialize model - console.log(`Initializing ColBERT ORT model from: ${MODEL_REPO}`); - const initStart = performance.now(); - initColbertOrt(MODEL_REPO, HIDDEN_SIZE); - const initTime = performance.now() - initStart; - console.log(`Model initialized in ${initTime.toFixed(0)}ms\n`); - - // 4. Test queries - const queries = [ - "where is authentication handled", - "how does the API route requests", - "database connection and query execution", - "error handling and logging", - "configuration and environment variables", - ]; - - // Take first NUM_CANDIDATES chunks as simulated candidates - const candidates = allChunks.slice(0, NUM_CANDIDATES); - - console.log("Starting benchmark...\n"); - - let totalRerankTime = 0; - const results: { query: string; topIndices: number[]; topScores: number[]; timeMs: number }[] = []; - - for (const query of queries) { - const startTime = performance.now(); - const result = colbertRerankOrt(query, candidates, TOP_K); - const elapsed = performance.now() - startTime; - totalRerankTime += elapsed; - - results.push({ - query, - topIndices: Array.from(result.indices), - topScores: Array.from(result.scores), - timeMs: elapsed, - }); - - console.log(`Query: "${query}"`); - console.log(` Time: ${elapsed.toFixed(1)}ms`); - console.log(` Top indices: [${result.indices.slice(0, 3).join(", ")}...]`); - console.log(` Top scores: [${result.scores.slice(0, 3).map((s: number) => s.toFixed(3)).join(", ")}...]`); - console.log(); - } - - const avgTime = totalRerankTime / queries.length; - - console.log("=== Summary ==="); - console.log(` Queries: ${queries.length}`); - console.log(` Candidates per query: ${NUM_CANDIDATES}`); - console.log(` Total rerank time: ${totalRerankTime.toFixed(1)}ms`); - console.log(` Avg time per query: ${avgTime.toFixed(1)}ms`); - console.log(` Throughput: ${(1000 / avgTime).toFixed(1)} queries/sec`); -} - -main().catch(console.error); diff --git a/osgrep-core/bench/rerank-preindexed.ts b/osgrep-core/bench/rerank-preindexed.ts deleted file mode 100644 index 1c1d3fcb..00000000 --- a/osgrep-core/bench/rerank-preindexed.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { cloneRepoIfMissing, loadRepoChunks, walkFiles, isCodeFile } from "./_util.js"; - -// @ts-ignore - generated at build time -import { initColbertOrt, colbertPreindexDocs, colbertRerankPreindexed } from "../index.js"; - -const REPO_URL = "https://github.com/sst/opencode.git"; -const REPO_DIR = "./opencode"; -const MODEL_REPO = "ryandono/osgrep-17m-v1-onnx"; -const HIDDEN_SIZE = 48; -const NUM_CANDIDATES = 100; -const TOP_K = 5; - -async function main() { - console.log("=== ColBERT Pre-indexed Rerank Benchmark ===\n"); - - // 1. Load repo - cloneRepoIfMissing(REPO_URL, REPO_DIR); - - console.log("Loading code files and chunking..."); - const files = walkFiles(REPO_DIR); - const codeFiles = files.filter(isCodeFile); - const allChunks = loadRepoChunks(REPO_DIR, 80); - - console.log(` Files found: ${files.length}`); - console.log(` Code files: ${codeFiles.length}`); - console.log(` Total chunks: ${allChunks.length}`); - console.log(); - - // 2. Initialize model - console.log(`Initializing ColBERT ORT model from: ${MODEL_REPO}`); - initColbertOrt(MODEL_REPO, HIDDEN_SIZE); - console.log("Model initialized.\n"); - - // 3. Pre-index first 500 chunks (simulating index time) - const indexChunks = allChunks.slice(0, 500); - console.log(`Pre-indexing ${indexChunks.length} chunks (INDEX TIME)...`); - const indexStart = performance.now(); - const numIndexed = colbertPreindexDocs(indexChunks); - const indexTime = performance.now() - indexStart; - console.log(` Indexed ${numIndexed} chunks in ${indexTime.toFixed(0)}ms`); - console.log(` Rate: ${((indexChunks.length / indexTime) * 1000).toFixed(1)} chunks/sec`); - console.log(); - - // 4. Simulate dense retrieval returning top 100 indices - // In real use, these would come from your dense ORT search - const candidateIndices = Array.from({ length: NUM_CANDIDATES }, (_, i) => i); - - // 5. Test queries (QUERY TIME) - const queries = [ - "where is authentication handled", - "how does the API route requests", - "database connection and query execution", - "error handling and logging", - "configuration and environment variables", - ]; - - console.log(`Starting QUERY TIME benchmark (${NUM_CANDIDATES} candidates per query)...\n`); - - let totalRerankTime = 0; - const results: { query: string; timeMs: number }[] = []; - - for (const query of queries) { - const startTime = performance.now(); - const result = colbertRerankPreindexed(query, candidateIndices, TOP_K); - const elapsed = performance.now() - startTime; - totalRerankTime += elapsed; - - results.push({ query, timeMs: elapsed }); - - console.log(`Query: "${query}"`); - console.log(` Time: ${elapsed.toFixed(1)}ms`); - console.log(` Top indices: [${result.indices.slice(0, 3).join(", ")}...]`); - console.log(` Top scores: [${result.scores.slice(0, 3).map((s: number) => s.toFixed(3)).join(", ")}...]`); - console.log(); - } - - const avgTime = totalRerankTime / queries.length; - - console.log("=== QUERY TIME Summary ==="); - console.log(` Queries: ${queries.length}`); - console.log(` Candidates per query: ${NUM_CANDIDATES}`); - console.log(` Total rerank time: ${totalRerankTime.toFixed(1)}ms`); - console.log(` Avg time per query: ${avgTime.toFixed(1)}ms`); - console.log(` Throughput: ${(1000 / avgTime).toFixed(1)} queries/sec`); - console.log(); - - if (avgTime < 25) { - console.log(` ✓ TARGET MET: <25ms per query!`); - } else { - console.log(` ✗ Target not met (want <25ms, got ${avgTime.toFixed(1)}ms)`); - } -} - -main().catch(console.error); diff --git a/osgrep-core/bench/rerank.ts b/osgrep-core/bench/rerank.ts deleted file mode 100644 index d503b016..00000000 --- a/osgrep-core/bench/rerank.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { cloneRepoIfMissing, loadRepoChunks, pickCandidates } from "./_util"; - -// Import the native addon (will be built by napi) -// @ts-ignore - generated at build time -import { initColbert, colbertRerankChecksum } from "../index"; - -const REPO_URL = "https://github.com/sst/opencode.git"; -const REPO_DIR = "./opencode"; -const MODEL_REPO = "mixedbread-ai/mxbai-edge-colbert-v0-17m"; -const NUM_CANDIDATES = 100; -const TOP_K = 5; -const QUERY = "where is auth handled"; -const LINES_PER_CHUNK = 80; - -async function main() { - console.log("=== ColBERT Rerank Benchmark ===\n"); - - // 1. Ensure repo exists - cloneRepoIfMissing(REPO_URL, REPO_DIR); - - // 2. Load chunks and pick candidates - console.log("Loading code files and selecting candidates..."); - const allChunks = loadRepoChunks(REPO_DIR, LINES_PER_CHUNK); - const candidates = pickCandidates(allChunks, NUM_CANDIDATES); - - console.log(` Total chunks available: ${allChunks.length}`); - console.log(` Candidates selected: ${candidates.length}`); - console.log(` Query: "${QUERY}"`); - console.log(` Top-K: ${TOP_K}`); - console.log(); - - // 3. Initialize model - console.log(`Initializing ColBERT model: ${MODEL_REPO}`); - initColbert(MODEL_REPO); - console.log("Model initialized.\n"); - - // 4. Benchmark reranking - console.log("Running rerank benchmark..."); - const startTime = performance.now(); - - const result = colbertRerankChecksum(QUERY, candidates, TOP_K); - - const endTime = performance.now(); - const totalMs = endTime - startTime; - - console.log("\n=== Results ==="); - console.log(` Total time: ${totalMs.toFixed(2)} ms`); - console.log(` Checksum: ${result.checksum.toFixed(6)} (for validation)`); - console.log(); - console.log(" Top-5 results:"); - for (let i = 0; i < result.indices.length; i++) { - const idx = result.indices[i]; - const score = result.scores[i]; - const preview = candidates[idx].slice(0, 80).replace(/\n/g, " ").trim(); - console.log(` [${i + 1}] idx=${idx}, score=${score.toFixed(4)}`); - console.log(` "${preview}..."`); - } -} - -main().catch(console.error); diff --git a/osgrep-core/bun.lockb b/osgrep-core/bun.lockb deleted file mode 100755 index c62b9d3f99863913d538593fc6f03282f70f69aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59922 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 diff --git a/osgrep-core/package.json b/osgrep-core/package.json index db0a9b0c..123a97ec 100644 --- a/osgrep-core/package.json +++ b/osgrep-core/package.json @@ -4,6 +4,11 @@ "type": "module", "main": "index.js", "types": "index.d.ts", + "files": [ + "index.js", + "index.d.ts", + "*.node" + ], "napi": { "name": "osgrep-core", "triples": { diff --git a/osgrep-core/test.mjs b/osgrep-core/test.mjs deleted file mode 100644 index 3b37d7ab..00000000 --- a/osgrep-core/test.mjs +++ /dev/null @@ -1,42 +0,0 @@ -// Quick smoke test for osgrep-core -import { initModels, isInitialized, embedDense, embedBatch } from './index.js'; - -const DENSE_REPO = 'onnx-community/granite-embedding-30m-english-ONNX'; -const COLBERT_REPO = 'ryandono/mxbai-edge-colbert-v0-17m-onnx-int8'; - -async function main() { - console.log('Testing osgrep-core...\n'); - - // Check initial state - console.log('isInitialized:', isInitialized()); - - // Initialize models - console.log('Initializing models...'); - const start = Date.now(); - initModels(DENSE_REPO, COLBERT_REPO); - console.log(`Models loaded in ${Date.now() - start}ms`); - console.log('isInitialized:', isInitialized()); - - // Test dense embedding - console.log('\nTesting dense embedding...'); - const texts = ['hello world', 'how does authentication work']; - const t0 = Date.now(); - const dense = embedDense(texts); - console.log(`Dense embed ${texts.length} texts in ${Date.now() - t0}ms`); - console.log(` count: ${dense.count}`); - console.log(` embeddings length: ${dense.embeddings.length} (expected: ${texts.length * 384})`); - - // Test combined embed - console.log('\nTesting combined embed (dense + colbert)...'); - const t1 = Date.now(); - const result = embedBatch(texts); - console.log(`Combined embed ${texts.length} texts in ${Date.now() - t1}ms`); - console.log(` dense length: ${result.dense.length}`); - console.log(` colbert_embeddings length: ${result.colbertEmbeddings.length}`); - console.log(` colbert_lengths: ${result.colbertLengths}`); - console.log(` colbert_offsets: ${result.colbertOffsets}`); - - console.log('\n✅ All tests passed!'); -} - -main().catch(console.error); diff --git a/osgrep-core/tsconfig.json b/osgrep-core/tsconfig.json deleted file mode 100644 index 238655f2..00000000 --- a/osgrep-core/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - // Enable latest features - "lib": ["ESNext", "DOM"], - "target": "ESNext", - "module": "ESNext", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } -} From 2303bca50adaac7e6921b0cad4b95a345e56f27d Mon Sep 17 00:00:00 2001 From: Ryan D'Onofrio <97113725+Ryandonofrio3@users.noreply.github.com> Date: Sat, 13 Dec 2025 19:45:33 -0800 Subject: [PATCH 06/19] repo cleanup deps removal --- .gitignore | 52 +- LICENSE | 202 -- NOTICE | 33 - README.md | 65 +- experiments/mrr-sweep.ts | 247 -- experiments/quick_check.ts | 31 - experiments/ranking-test.ts | 46 - experiments/verify-fix.ts | 42 - package.json | 21 +- plan.md | 332 --- plugins/osgrep/skills/osgrep/SKILL.md | 14 +- pnpm-lock.yaml | 3435 ------------------------- public/bench.png | Bin 140887 -> 0 bytes scripts/compare-engines.ts | 163 -- scripts/index-bench.sh | 54 - scripts/sync-eval-cases.ts | 35 - scripts/verify_skeleton.ts | 38 - src/lib/workers/orchestrator.ts | 4 +- test_skeleton.py | 2 - tools/eval.ts | 643 ----- 20 files changed, 47 insertions(+), 5412 deletions(-) delete mode 100644 LICENSE delete mode 100644 NOTICE delete mode 100644 experiments/mrr-sweep.ts delete mode 100644 experiments/quick_check.ts delete mode 100644 experiments/ranking-test.ts delete mode 100644 experiments/verify-fix.ts delete mode 100644 plan.md delete mode 100644 pnpm-lock.yaml delete mode 100644 public/bench.png delete mode 100644 scripts/compare-engines.ts delete mode 100755 scripts/index-bench.sh delete mode 100644 scripts/sync-eval-cases.ts delete mode 100644 scripts/verify_skeleton.ts delete mode 100644 test_skeleton.py delete mode 100644 tools/eval.ts diff --git a/.gitignore b/.gitignore index 5a8f09e9..17e4973d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,35 +3,49 @@ node_modules/ **/node_modules/ *.tsbuildinfo *.tgz +.DS_Store + +# Local osgrep data +.osgrep/ + +# Development files +CLAUDE.md +TODO.md +AGENTS.md ADVANCED.md -test-build.sh -TEST_SUITE_PROMPT.md -WATCH_MODE_PROMPT.md ENGINEERING_DIARY.md RELEASE_GUIDE.md -osgrep/run-benchmark.sh +TEST_SUITE_PROMPT.md +WATCH_MODE_PROMPT.md +AGENT_BENCHMARK.md +latest_review.md +plan.md + +# Benchmark & experiments (dev-only) +benchmark/ +experiments/ +scripts/ +tools/ +public/ +test_skeleton.py +test-build.sh run-agent-benchmark.sh setup-benchmark-repos.sh -AGENT_BENCHMARK.md benchmark-results.json -src/bench/benchmark-agent.ts -src/bench/generate-benchmark-chart.ts -src/.env.local -CLAUDE.md -.DS_Store -latest_review.md benchmarks/*.log -.osgrep/server.json -.osgrep/cache/meta.lmdb-lock +src/bench/ -.osgrep -TODO.md -AGENTS.md +# Env files +src/.env.local +.env* + +# Lockfiles (using bun) +pnpm-lock.yaml +package-lock.json +yarn.lock # Native core (Rust / N-API) build outputs osgrep-core/target/ **/target/ osgrep-core/*.node - -# Optional benchmark dependency (was a nested git repo) -osgrep-core/bench/opencode/ +osgrep-core/bench/ diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 6d69c9c6..00000000 --- a/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2025 Mixedbread AI (original mgrep work) - Copyright 2025 Ryan D'Onofrio (osgrep modifications and enhancements) - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/NOTICE b/NOTICE deleted file mode 100644 index 62b2f374..00000000 --- a/NOTICE +++ /dev/null @@ -1,33 +0,0 @@ -osgrep -Copyright 2025 Ryan Donofrio - -This product includes software originally developed by MixedBread as mgrep. -Original mgrep Copyright 2025 MixedBread -https://github.com/mixedbread-ai/mgrep - -================================================================================ - -MODIFICATIONS AND ENHANCEMENTS - -osgrep is a substantial modification and enhancement of the original mgrep -codebase. The following major changes and additions have been made by -Ryan Donofrio: - -- Complete rewrite to use local-only embeddings (removing remote API dependencies) -- Integration with @huggingface/transformers for local model inference -- Implementation of LanceDB for local vector storage -- New local storage architecture and indexing system -- Enhanced chunking and context management -- Addition of watch mode for real-time index updates -- New CLI commands and improved user experience -- Comprehensive benchmarking and evaluation tools -- Extensive test suite additions -- Documentation and setup improvements - - -================================================================================ - -THIRD-PARTY DEPENDENCIES - -This software depends on various open-source libraries. See package.json for -a complete list of dependencies and their respective licenses. diff --git a/README.md b/README.md index e88861c5..415a46f2 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,10 @@ Natural-language search that works like `grep`. Fast, local, and built for coding agents. - **Semantic:** Finds concepts ("where do transactions get created?"), not just strings. -- **Call Graph Tracing:** Map dependencies with `trace` to see who calls what. - **Role Detection:** Distinguishes `ORCHESTRATION` (high-level logic) from `DEFINITION` (types/classes). - **Local & Private:** 100% local embeddings via `onnxruntime-node`. - **Auto-Isolated:** Each repository gets its own index automatically. -- **Agent-Ready:** Native output with symbols, roles, and call graphs. +- **Agent-Ready:** Native output with symbols and roles. ## Quick Start @@ -50,26 +49,6 @@ Natural-language search that works like `grep`. Fast, local, and built for codin **Your first search will automatically index the repository.** Each repository is automatically isolated with its own index. Switching between repos "just works" — no manual configuration needed. If the background server is running (`osgrep serve`), search goes through the hot daemon; otherwise it falls back to on-demand indexing. -4. **Trace** (Call Graph) - - ```bash - osgrep trace "function_name" - ``` -See who calls a function (upstream dependencies) and what it calls (downstream dependencies). Perfect for impact analysis and understanding code flow. - -To find the symbols in your code base: - ```bash - osgrep symbols - ``` - -In our public benchmarks, `osgrep` can save about 20% of your LLM tokens and deliver a 30% speedup. - -<div align="center"> - <img src="public/bench.png" alt="osgrep benchmark" width="100%" style="border-radius: 8px; margin: 20px 0;" /> -</div> - - - ### Claude Code Plugin 1. Run `osgrep install-claude-code` @@ -215,32 +194,6 @@ osgrep list Shows store names, sizes, and last modified times. Useful for seeing what's indexed and cleaning up old stores. -### `osgrep skeleton` - -Generates a compressed "skeleton" of a file, showing only signatures, types, and class structures while eliding function bodies. - -```bash -osgrep skeleton src/lib/auth.ts -``` - -**Output:** -```typescript -class AuthService { - validate(token: string): boolean { - // → jwt.verify, checkScope, .. | C:5 | ORCH - } -} -``` - -**Modes:** -- `osgrep skeleton <file>`: Skeletonize specific file. -- `osgrep skeleton <Symbol>`: Find symbol in index and skeletonize its file. -- `osgrep skeleton "query"`: Search for query and skeletonize top matches. - -**Supported Languages:** -TypeScript, JavaScript, Python, Go, Rust, Java, C#, C++, C, Ruby, PHP. - - ### `osgrep doctor` Checks installation health, model paths, and database integrity. @@ -303,9 +256,9 @@ osgrep respects both `.gitignore` and `.osgrepignore` files when indexing. Creat ## Development ```bash -pnpm install -pnpm build # or pnpm dev -pnpm format # biome check +bun install +bun run build +bun run format # biome check ``` ## Troubleshooting @@ -316,15 +269,7 @@ pnpm format # biome check - **Want faster indexing?** Keep fallback disabled (default) to skip files without TreeSitter support. - **Need a fresh start?** Delete `~/.osgrep/data` and `~/.osgrep/meta.json` and run `osgrep index`. -## Attribution - -osgrep is built upon the foundation of [mgrep](https://github.com/mixedbread-ai/mgrep) by MixedBread. We acknowledge and appreciate the original architectural concepts and design decisions that informed this work. - - -See the [NOTICE](NOTICE) file for detailed attribution information. - ## License -Licensed under the Apache License, Version 2.0. -See [LICENSE](LICENSE) and [Apache-2.0](https://opensource.org/licenses/Apache-2.0) for details. +Licensed under the [Apache License, Version 2.0](https://opensource.org/licenses/Apache-2.0). diff --git a/experiments/mrr-sweep.ts b/experiments/mrr-sweep.ts deleted file mode 100644 index 063b601e..00000000 --- a/experiments/mrr-sweep.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { performance } from "node:perf_hooks"; -import { cases, evaluateCase } from "../src/eval"; -import { Searcher } from "../src/lib/search/searcher"; -import type { SearchFilter } from "../src/lib/store/types"; -import { VectorDB } from "../src/lib/store/vector-db"; -import { - ensureProjectPaths, - findProjectRoot, -} from "../src/lib/utils/project-root"; -import { destroyWorkerPool } from "../src/lib/workers/pool"; - -type Scenario = { - name: string; - env: Record<string, string>; - searchOptions?: { rerank?: boolean }; -}; - -const enableDiagnostics = - process.env.OSGREP_SWEEP_DEBUG === "1" || - process.env.OSGREP_SWEEP_DEBUG === "true"; - -type Ranked = { path: string; score?: number }; - -function topPaths(response: { data: any[] }, k = 3): Ranked[] { - return (response.data || []).slice(0, k).map((chunk) => ({ - path: chunk?.metadata?.path || "", - score: chunk?.score ?? chunk?.generated_metadata?._score, - })); -} - -function targetRank(response: { data: any[] }, expectedPath: string): number { - const expectedPaths = expectedPath - .split("|") - .map((p) => p.trim().toLowerCase()) - .filter(Boolean); - return response.data.findIndex((chunk: any) => { - const path = chunk?.metadata?.path?.toLowerCase?.() || ""; - return expectedPaths.some((expected) => path.includes(expected)); - }); -} - -// Keep runs consistent and serial. -process.env.OSGREP_WORKER_THREADS ??= "1"; -process.env.OSGREP_WORKER_COUNT ??= "1"; - -const scenarios: Scenario[] = [ - { - name: "baseline-rerank-on", - env: {}, - searchOptions: { rerank: true }, - }, - { - name: "oldish-no-rerank-strong-boosts", - env: { - OSGREP_CODE_BOOST: "1.25", - OSGREP_TEST_PENALTY: "0.85", - OSGREP_DOC_PENALTY: "0.5", - OSGREP_PRE_K: "300", - OSGREP_STAGE1_K: "400", - OSGREP_STAGE2_K: "0", // skip pooled filter - OSGREP_MAX_PER_FILE: "50", - }, - searchOptions: { rerank: false }, - }, - { - name: "small-rerank-strong-boosts", - env: { - OSGREP_CODE_BOOST: "1.25", - OSGREP_TEST_PENALTY: "0.85", - OSGREP_DOC_PENALTY: "0.5", - OSGREP_PRE_K: "300", - OSGREP_STAGE1_K: "400", - OSGREP_STAGE2_K: "80", - OSGREP_MAX_PER_FILE: "50", - }, - searchOptions: { rerank: true }, - }, - { - name: "no-diversify-rerank", - env: { - OSGREP_CODE_BOOST: "1.2", - OSGREP_TEST_PENALTY: "0.85", - OSGREP_DOC_PENALTY: "0.7", - OSGREP_PRE_K: "400", - OSGREP_STAGE1_K: "500", - OSGREP_STAGE2_K: "120", - OSGREP_MAX_PER_FILE: "1000", - }, - searchOptions: { rerank: true }, - }, -]; - -async function runScenario(scenario: Scenario) { - const touched: Record<string, string | undefined> = {}; - for (const [key, value] of Object.entries(scenario.env)) { - touched[key] = process.env[key]; - process.env[key] = value; - } - - const searchRoot = process.cwd(); - const projectRoot = findProjectRoot(searchRoot) ?? searchRoot; - const paths = ensureProjectPaths(projectRoot); - const vectorDb = new VectorDB(paths.lancedbDir); - const searcher = new Searcher(vectorDb); - - const results: ReturnType<typeof evaluateCase>[] = []; - let drops = 0; - let lifts = 0; - let total = 0; - let better = 0; - let worse = 0; - let unchanged = 0; - - for (const c of cases) { - const queryStart = performance.now(); - let fusedTop: Ranked[] = []; - let rerankTop: Ranked[] = []; - let fusedRank = -1; - - if (enableDiagnostics && scenario.searchOptions?.rerank !== false) { - const fusedOnly = await searcher.search( - c.query, - 20, - { rerank: false }, - undefined as SearchFilter | undefined, - ); - fusedTop = topPaths(fusedOnly); - fusedRank = targetRank(fusedOnly, c.expectedPath); - } - - const res = await searcher.search(c.query, 20, scenario.searchOptions); - rerankTop = topPaths(res); - const rerankRank = enableDiagnostics ? targetRank(res, c.expectedPath) : -1; - - if (enableDiagnostics && fusedTop.length && rerankTop.length) { - total += 1; - const fusedBest = fusedTop[0]?.path?.toLowerCase?.() || ""; - const rerankFirstThree = rerankTop - .slice(0, 3) - .map((r) => r.path?.toLowerCase?.() || ""); - if (fusedBest && !rerankFirstThree.some((p) => p.includes(fusedBest))) { - drops += 1; - console.log( - `[debug] ${c.query}\n fused#1: ${fusedTop - .map((r) => r.path) - .join(", ")}\n rerank#3: ${rerankTop - .map((r) => r.path) - .join(", ")}`, - ); - } - - const rerankBest = rerankTop[0]?.path?.toLowerCase?.() || ""; - const fusedFirstThree = fusedTop - .slice(0, 3) - .map((r) => r.path?.toLowerCase?.() || ""); - if ( - rerankBest && - !fusedFirstThree.some((p) => p.includes(rerankBest)) && - fusedTop.some((r) => - (r.path?.toLowerCase?.() || "").includes(rerankBest), - ) - ) { - lifts += 1; - console.log( - `[lift] ${c.query}\n fused#3: ${fusedTop - .map((r) => r.path) - .join(", ")}\n rerank#1: ${rerankTop - .map((r) => r.path) - .join(", ")}`, - ); - } - - if (fusedRank >= 0 && rerankRank >= 0) { - if (rerankRank < fusedRank) { - better += 1; - } else if (rerankRank > fusedRank) { - worse += 1; - console.log( - `[worse] ${c.query} fusedRank=${fusedRank + 1} rerankRank=${ - rerankRank + 1 - } path=${c.expectedPath}`, - ); - } else { - unchanged += 1; - } - } - } - - const timeMs = performance.now() - queryStart; - results.push(evaluateCase(res, c, timeMs)); - } - - const mrr = results.reduce((sum, r) => sum + r.rr, 0) / results.length; - const recallAt10 = - results.reduce((sum, r) => sum + r.recall, 0) / results.length; - const avgTime = - results.reduce((sum, r) => sum + r.timeMs, 0) / results.length; - const found = results.filter((r) => r.found).length; - - console.log(`\n=== ${scenario.name} ===`); - console.log( - `MRR: ${mrr.toFixed(3)} | Recall@10: ${recallAt10.toFixed(3)} | Avg ms: ${avgTime.toFixed(0)} | Found: ${found}/${results.length}`, - ); - if (enableDiagnostics && total > 0) { - console.log( - `Rerank dropped fused#1 out of top3 for ${drops}/${total} queries; promoted new #1 outside fused top3 for ${lifts}/${total} queries`, - ); - if (better + worse + unchanged > 0) { - console.log( - `Target rank delta: better ${better}, worse ${worse}, unchanged ${unchanged} (out of ${better + worse + unchanged})`, - ); - } - } - - // Restore env (best-effort) so each scenario is clean. - for (const [key, prev] of Object.entries(touched)) { - if (prev === undefined) delete process.env[key]; - else process.env[key] = prev; - } -} - -async function main() { - const filter = process.argv[2]; - const runList = filter - ? scenarios.filter((s) => s.name === filter) - : scenarios; - - for (const scenario of runList) { - try { - await runScenario(scenario); - } catch (err) { - console.error(`Scenario ${scenario.name} failed:`, err); - } - } - - try { - await new Promise((resolve) => setTimeout(resolve, 200)); - await destroyWorkerPool(); - } catch { - // Swallow cleanup errors for experiments. - } -} - -main().catch((err) => { - console.error(err); - process.exitCode = 1; -}); diff --git a/experiments/quick_check.ts b/experiments/quick_check.ts deleted file mode 100644 index 5274985e..00000000 --- a/experiments/quick_check.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { VectorDB } from "../src/lib/store/vector-db"; -import { - ensureProjectPaths, - findProjectRoot, -} from "../src/lib/utils/project-root"; - -async function main() { - const root = process.cwd(); - const paths = ensureProjectPaths(findProjectRoot(root) ?? root); - const db = new VectorDB(paths.lancedbDir); - const table = await db.ensureTable(); - const rows = (await table.query().limit(5).toArray()) as any[]; - rows.forEach((r, i) => { - const col = r.colbert; - let len = 0; - if (Buffer.isBuffer(col)) len = col.length; - else if (Array.isArray(col)) len = col.length; - else if ( - col && - typeof col === "object" && - "length" in (col as Record<string, unknown>) - ) - len = Number((col as { length: number }).length) || 0; - else if (col && col.type === "Buffer" && Array.isArray(col.data)) - len = col.data.length; - console.log( - `#${i} path=${r.path}, colbertLen=${len}, scale=${r.colbert_scale}`, - ); - }); -} -main().catch(console.error); diff --git a/experiments/ranking-test.ts b/experiments/ranking-test.ts deleted file mode 100644 index 6c4f7b48..00000000 --- a/experiments/ranking-test.ts +++ /dev/null @@ -1,46 +0,0 @@ -// experiments/ranking-test.ts - -import { Searcher } from "../src/lib/search/searcher"; -import { VectorDB } from "../src/lib/store/vector-db"; -import { ensureProjectPaths } from "../src/lib/utils/project-root"; - -async function run() { - const root = process.cwd(); - const paths = ensureProjectPaths(root); - const db = new VectorDB(paths.lancedbDir); - const searcher = new Searcher(db); - - const cases = [ - { query: "Request Validation", expected: "request_body_to_args" }, - { query: "Dependency Injection", expected: "solve_dependencies" }, - ]; - - for (const c of cases) { - console.log(`\n🔎 Query: "${c.query}"`); - const results = await searcher.search(c.query, 10, { rerank: true }); - - // Filter out this test file - const filtered = results.data.filter( - (r) => !r.metadata?.path.includes("ranking-test.ts"), - ); - - const found = filtered.findIndex( - (r) => - r.metadata?.path.includes(c.expected) || r.text?.includes(c.expected), - ); - - if (found === 0) console.log(`✅ PASS: Found '${c.expected}' at rank #1`); - else if (found > 0) - console.log(`⚠️ WARN: Found '${c.expected}' at rank #${found + 1}`); - else console.log(`❌ FAIL: Did not find '${c.expected}' in top results`); - - // Debug: Show top 3 roles/scores - filtered.slice(0, 3).forEach((r, i) => { - console.log( - ` ${i + 1}. [${r.role ?? "UNK"}] ${r.metadata?.path} (Score: ${r.score.toFixed(3)})`, - ); - }); - } -} - -run(); diff --git a/experiments/verify-fix.ts b/experiments/verify-fix.ts deleted file mode 100644 index c4773ac9..00000000 --- a/experiments/verify-fix.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { performance } from "node:perf_hooks"; -import { Searcher } from "../src/lib/search/searcher"; -import { VectorDB } from "../src/lib/store/vector-db"; -import { - ensureProjectPaths, - findProjectRoot, -} from "../src/lib/utils/project-root"; - -// Force old-engine style boosts for this check. -process.env.OSGREP_CODE_BOOST = "1.25"; -process.env.OSGREP_TEST_PENALTY = "0.85"; -process.env.OSGREP_DOC_PENALTY = "0.5"; - -async function main() { - const root = process.cwd(); - const projectRoot = findProjectRoot(root) ?? root; - const paths = ensureProjectPaths(projectRoot); - const db = new VectorDB(paths.lancedbDir); - const searcher = new Searcher(db); - - const warmStart = performance.now(); - await searcher.search("test query", 1, { rerank: true }); - console.log( - `Warmup Query Time: ${(performance.now() - warmStart).toFixed(0)}ms`, - ); - - const res = await searcher.search("TreeSitterParser", 5, { rerank: true }); - console.log("\nContext Visibility Check:"); - res.data.forEach((r, i) => { - const path = r.metadata?.path ?? ""; - const score = typeof r.score === "number" ? r.score.toFixed(3) : "n/a"; - console.log(`${i + 1}. ${path} (Score: ${score})`); - if (!r.text?.includes("TreeSitterParser")) { - console.log(" (Matched via vectorized context, not literal text)"); - } - }); -} - -main().catch((err) => { - console.error(err); - process.exitCode = 1; -}); diff --git a/package.json b/package.json index d91b60ac..f95f0207 100644 --- a/package.json +++ b/package.json @@ -21,15 +21,11 @@ "dev": "npx tsc && node dist/index.js", "test": "vitest run", "test:watch": "vitest", - "benchmark": "./run-benchmark.sh", - "benchmark:index": "./run-benchmark.sh $HOME/osgrep-benchmarks --index", - "benchmark:agent": "npx tsx src/bench/benchmark-agent.ts", - "benchmark:chart": "npx tsx src/bench/generate-benchmark-chart.ts", "format": "biome check --write .", "format:check": "biome check .", "lint": "biome lint .", "typecheck": "tsc --noEmit", - "prepublishOnly": "pnpm build" + "prepublishOnly": "bun run build" }, "keywords": [ "osgrep", @@ -40,15 +36,12 @@ "files": [ "dist", "plugins", - "README.md", - "LICENSE", - "NOTICE" + "README.md" ], "license": "Apache-2.0", "description": "Local grep-like search tool for your codebase.", "dependencies": { "osgrep-core": "file:./osgrep-core", - "@clack/prompts": "^0.11.0", "@lancedb/lancedb": "^0.22.3", "@modelcontextprotocol/sdk": "^1.24.3", "apache-arrow": "^18.1.0", @@ -56,23 +49,15 @@ "chokidar": "^4.0.3", "cli-highlight": "^2.1.11", "commander": "^14.0.2", - "dotenv": "^17.2.3", "fast-glob": "^3.3.3", "ignore": "^5.0.0", "lmdb": "^3.4.4", "ora": "^5.4.1", - "simsimd": "^6.5.5", - "uuid": "^9.0.1", - "web-tree-sitter": "^0.25.10", - "zod": "^4.1.12" + "web-tree-sitter": "^0.25.10" }, "devDependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.1.50", "@biomejs/biome": "2.3.4", "@types/node": "^24.10.1", - "@types/uuid": "^9.0.8", - "node-gyp": "^12.1.0", - "ts-node": "^10.9.2", "typescript": "^5.9.3", "vitest": "^1.6.0" } diff --git a/plan.md b/plan.md deleted file mode 100644 index a2f437bf..00000000 --- a/plan.md +++ /dev/null @@ -1,332 +0,0 @@ -# osgrep v3 Simplification Plan - -## Current State (Post-Migration) - -We've completed the major architectural change: -- **Rust core (osgrep-core)**: ONNX Runtime for dense + ColBERT embeddings -- **TypeScript orchestration**: syncer, watcher, chunker, searcher, LanceDB -- **Bridge**: `src/lib/native/index.ts` connects TS to Rust - -**Results so far:** -- Code reduced from ~6,600 to ~4,400 lines (34% reduction) -- Binary size reduced from 58MB to 27MB (53% smaller) -- Removed: worker pool, skeleton, trace, graph modules - ---- - -## Phase 1: Dependency Cleanup - -### 1.1 Remove Dead Dependencies from package.json - -**File:** `package.json` - -Remove these dependencies that are no longer used: - -```json -// REMOVE from dependencies: -"onnxruntime-node": "1.21.0", // Now in Rust -"@huggingface/transformers": "^3.8.0", // Now in Rust -"piscina": "^5.1.4", // Worker pool deleted -``` - -**Why:** These were only used by the old TS embedding pipeline which is now handled by Rust. - -### 1.2 Verify No Imports Remain - -Search for any remaining imports of removed packages: -```bash -grep -r "onnxruntime" src/ -grep -r "@huggingface/transformers" src/ -grep -r "piscina" src/ -``` - ---- - -## Phase 2: Simplify Search Pipeline - -### 2.1 Delete Intent Detection - -**Delete file:** `src/lib/search/intent.ts` - -The intent detection system adds complexity without clear value: -- Regex-based query classification ("where is" → DEFINITION, "how does" → FLOW) -- Intent-based boosting in search results -- Not actually improving search quality in practice - -### 2.2 Simplify Searcher - -**File:** `src/lib/search/searcher.ts` - -**Current complexity to remove:** - -1. **Intent-based boosting** (lines 186-196): - ```typescript - // DELETE THIS BLOCK - if (intent) { - if (intent.type === "DEFINITION" && record.role === "DEFINITION") { - boostFactor *= 1.2; - } - // ... more intent conditions - } - ``` - -2. **Role-based boosting** (lines 165-180): - ```typescript - // SIMPLIFY: Remove or reduce this complexity - if (record.role === "ORCHESTRATION") { - boostFactor *= 1.5; - } else if (record.role === "DEFINITION") { - boostFactor *= 1.2; - } - // ... - ``` - -3. **Reference count boosting** (lines 174-180): - ```typescript - // DELETE: Over-engineered - if (refs > 5) boostFactor *= 1.1; - if (refs > 15) boostFactor *= 1.25; - ``` - -**Simplified `applyStructureBoost` function:** - -```typescript -private applyStructureBoost( - record: Partial<VectorRecord>, - score: number, -): number { - let adjusted = score; - - // Anchor penalty (anchors are recall helpers, not results) - if (record.is_anchor) { - adjusted *= 0.99; - } - - // Test file penalty - const pathStr = (record.path || "").toLowerCase(); - const isTestPath = - /(^|\/)(__tests__|tests?|specs?|benchmark)(\/|$)/i.test(pathStr) || - /\.(test|spec)\.[cm]?[jt]sx?$/i.test(pathStr); - if (isTestPath) { - adjusted *= 0.5; - } - - // Docs/config penalty - if ( - pathStr.endsWith(".md") || - pathStr.endsWith(".json") || - pathStr.endsWith(".lock") || - pathStr.includes("/docs/") - ) { - adjusted *= 0.6; - } - - return adjusted; -} -``` - -### 2.3 Remove Intent from Search Signature - -**Current:** -```typescript -async search( - query: string, - top_k?: number, - _search_options?: { rerank?: boolean }, - _filters?: SearchFilter, - pathPrefix?: string, - intent?: SearchIntent, // REMOVE - signal?: AbortSignal, -): Promise<SearchResponse> -``` - -**After:** -```typescript -async search( - query: string, - top_k?: number, - options?: { rerank?: boolean }, - filters?: SearchFilter, - pathPrefix?: string, - signal?: AbortSignal, -): Promise<SearchResponse> -``` - -### 2.4 Update Search Command - -**File:** `src/commands/search.ts` - -Remove any intent detection calls and simplify the search invocation. - ---- - -## Phase 3: Code Quality Cleanup - -### 3.1 Remove Environment Variable Overrides - -The searcher has many env var overrides that add cognitive load: - -```typescript -// Consider removing or documenting these: -OSGREP_ANCHOR_PENALTY -OSGREP_TEST_PENALTY -OSGREP_DOC_PENALTY -OSGREP_PRE_K -OSGREP_STAGE1_K -OSGREP_STAGE2_K -OSGREP_RERANK_TOP -OSGREP_RERANK_BLEND -OSGREP_MAX_PER_FILE -``` - -**Decision:** Either: -1. Remove all env vars and use fixed reasonable defaults -2. Keep only 2-3 most useful ones (e.g., `OSGREP_TOP_K`, `OSGREP_RERANK`) - -### 3.2 Audit Remaining Files - -Check for dead code in: -- `src/lib/store/` - vector-db.ts, types.ts -- `src/lib/core/` - chunker, languages -- `src/lib/utils/` - filter-builder, etc. - -### 3.3 Type Cleanup - -Remove unused types from `src/lib/store/types.ts` related to deleted features. - ---- - -## Phase 4: Testing & Validation - -### 4.1 Manual Testing - -```bash -# Clean slate -rm -rf ~/.osgrep/indices/* - -# Index a test repo -bun src/index.ts index --path . --reset - -# Search queries -bun src/index.ts "how does embedding work" -bun src/index.ts "where is the config" -bun src/index.ts "vector search implementation" -``` - -### 4.2 Run Existing Tests - -```bash -bun test -``` - -### 4.3 Typecheck - -```bash -bunx tsc --noEmit -``` - ---- - -## Phase 5: Optional Further Simplifications - -### 5.1 Consider Removing Two-Stage Rerank - -The current pipeline has: -1. Vector search → top 500 -2. RRF fusion (vector + FTS) -3. Stage 1: Pooled cosine filter → top 200 -4. Stage 2: ColBERT MaxSim rerank → top 40 -5. Structure boost + diversification - -**Simpler alternative:** -1. Vector search → top 100 -2. ColBERT rerank → top 20 -3. Basic penalties (test/docs) - -### 5.2 Consider Removing FTS Fallback - -The FTS (full-text search) adds code but may not improve results significantly. Consider making it optional or removing entirely. - -### 5.3 Simplify Output Formatting - -`src/lib/output/formatter.ts` has complex role coloring and breadcrumb logic. Could be simplified to just show: -- File path + line number -- Code snippet -- Score - ---- - -## File Inventory (Post-Simplification) - -### Keep (Core) -``` -src/ -├── index.ts # CLI entry -├── config.ts # Constants -├── commands/ -│ ├── index.ts # Index command -│ ├── search.ts # Search command -│ └── serve.ts # MCP server -├── lib/ -│ ├── native/ -│ │ └── index.ts # Rust bridge -│ ├── index/ -│ │ ├── syncer.ts # File sync -│ │ └── watcher.ts # File watcher -│ ├── search/ -│ │ └── searcher.ts # Search logic (simplified) -│ ├── store/ -│ │ ├── vector-db.ts # LanceDB wrapper -│ │ └── types.ts # Types -│ ├── core/ -│ │ ├── chunker.ts # Tree-sitter chunking -│ │ └── languages.ts # Language configs -│ ├── output/ -│ │ ├── formatter.ts # Human output -│ │ └── json-formatter.ts # JSON output -│ ├── utils/ -│ │ └── ... # Utilities -│ └── workers/ -│ └── orchestrator.ts # Embedding orchestrator -``` - -### Delete (This Phase) -``` -src/lib/search/intent.ts # Intent detection -``` - -### Already Deleted (Previous Phase) -``` -src/lib/skeleton/ # Skeleton feature -src/lib/graph/ # Call graph -src/lib/workers/pool.ts # Worker pool -src/lib/workers/embeddings/ # TS embedding code -src/commands/skeleton.ts # Skeleton command -src/commands/trace.ts # Trace command -``` - ---- - -## Execution Checklist - -- [ ] Phase 1.1: Remove dead deps from package.json -- [ ] Phase 1.2: Verify no imports remain -- [ ] Phase 2.1: Delete intent.ts -- [ ] Phase 2.2: Simplify applyStructureBoost -- [ ] Phase 2.3: Remove intent from search signature -- [ ] Phase 2.4: Update search command -- [ ] Phase 3.1: Decide on env var strategy -- [ ] Phase 4.1: Manual testing -- [ ] Phase 4.2: Run tests -- [ ] Phase 4.3: Typecheck -- [ ] Phase 5: Evaluate optional simplifications - ---- - -## Success Criteria - -1. **Code size:** < 4,000 lines of TypeScript -2. **Dependencies:** Remove 3 unused packages -3. **Complexity:** No intent detection, simplified boosting -4. **Functionality:** Index and search work correctly -5. **Maintainability:** Easy to understand search pipeline diff --git a/plugins/osgrep/skills/osgrep/SKILL.md b/plugins/osgrep/skills/osgrep/SKILL.md index ab2549a1..d56b159c 100644 --- a/plugins/osgrep/skills/osgrep/SKILL.md +++ b/plugins/osgrep/skills/osgrep/SKILL.md @@ -46,16 +46,13 @@ Read src/auth/handler.ts:45-120 Read the specific line range, not the whole file. -## Other commands +## Other options ```bash -# Trace call graph (who calls X, what X calls) -osgrep trace handleAuth - -# Skeleton of a huge file (to find which ranges to read) -osgrep skeleton src/giant-2000-line-file.ts - # Just file paths when you only need locations osgrep "authentication" --compact + +# Show more results +osgrep "error handling" -m 20 ``` ## Workflow: architecture questions @@ -66,9 +63,6 @@ osgrep "where do requests enter the server" # 2. If you need deeper context on a specific function Read src/server/handler.ts:45-120 - -# 3. Trace to understand call flow -osgrep trace handleRequest ``` ## Tips diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index b04f1902..00000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,3435 +0,0 @@ -lockfileVersion: '6.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -dependencies: - '@clack/prompts': - specifier: ^0.11.0 - version: 0.11.0 - '@lancedb/lancedb': - specifier: ^0.22.3 - version: 0.22.3(apache-arrow@18.1.0) - '@modelcontextprotocol/sdk': - specifier: ^1.24.3 - version: 1.24.3(zod@4.1.13) - apache-arrow: - specifier: ^18.1.0 - version: 18.1.0 - chalk: - specifier: ^5.6.2 - version: 5.6.2 - chokidar: - specifier: ^4.0.3 - version: 4.0.3 - cli-highlight: - specifier: ^2.1.11 - version: 2.1.11 - commander: - specifier: ^14.0.2 - version: 14.0.2 - dotenv: - specifier: ^17.2.3 - version: 17.2.3 - fast-glob: - specifier: ^3.3.3 - version: 3.3.3 - ignore: - specifier: ^5.0.0 - version: 5.3.2 - lmdb: - specifier: ^3.4.4 - version: 3.4.4 - ora: - specifier: ^5.4.1 - version: 5.4.1 - osgrep-core: - specifier: file:./osgrep-core - version: file:osgrep-core - simsimd: - specifier: ^6.5.5 - version: 6.5.5 - uuid: - specifier: ^9.0.1 - version: 9.0.1 - web-tree-sitter: - specifier: ^0.25.10 - version: 0.25.10 - zod: - specifier: ^4.1.12 - version: 4.1.13 - -devDependencies: - '@anthropic-ai/claude-agent-sdk': - specifier: ^0.1.50 - version: 0.1.56(zod@4.1.13) - '@biomejs/biome': - specifier: 2.3.4 - version: 2.3.4 - '@types/node': - specifier: ^24.10.1 - version: 24.10.1 - '@types/uuid': - specifier: ^9.0.8 - version: 9.0.8 - node-gyp: - specifier: ^12.1.0 - version: 12.1.0 - ts-node: - specifier: ^10.9.2 - version: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - vitest: - specifier: ^1.6.0 - version: 1.6.1(@types/node@24.10.1) - -packages: - - /@anthropic-ai/claude-agent-sdk@0.1.56(zod@4.1.13): - resolution: {integrity: sha512-r4IAJ3Wht2iTyS3qoDYbCEbkTNXM2W33DEt9q5tkDDD9UR0dU5U7qLPwA8TTTX4wyHkZic5a0jLDuD7xU3ZrZg==} - engines: {node: '>=18.0.0'} - peerDependencies: - zod: ^3.24.1 - dependencies: - zod: 4.1.13 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.33.5 - '@img/sharp-darwin-x64': 0.33.5 - '@img/sharp-linux-arm': 0.33.5 - '@img/sharp-linux-arm64': 0.33.5 - '@img/sharp-linux-x64': 0.33.5 - '@img/sharp-linuxmusl-arm64': 0.33.5 - '@img/sharp-linuxmusl-x64': 0.33.5 - '@img/sharp-win32-x64': 0.33.5 - dev: true - - /@babel/runtime@7.28.4: - resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} - engines: {node: '>=6.9.0'} - requiresBuild: true - dev: false - optional: true - - /@biomejs/biome@2.3.4: - resolution: {integrity: sha512-TU08LXjBHdy0mEY9APtEtZdNQQijXUDSXR7IK1i45wgoPD5R0muK7s61QcFir6FpOj/RP1+YkPx5QJlycXUU3w==} - engines: {node: '>=14.21.3'} - hasBin: true - optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.3.4 - '@biomejs/cli-darwin-x64': 2.3.4 - '@biomejs/cli-linux-arm64': 2.3.4 - '@biomejs/cli-linux-arm64-musl': 2.3.4 - '@biomejs/cli-linux-x64': 2.3.4 - '@biomejs/cli-linux-x64-musl': 2.3.4 - '@biomejs/cli-win32-arm64': 2.3.4 - '@biomejs/cli-win32-x64': 2.3.4 - dev: true - - /@biomejs/cli-darwin-arm64@2.3.4: - resolution: {integrity: sha512-w40GvlNzLaqmuWYiDU6Ys9FNhJiclngKqcGld3iJIiy2bpJ0Q+8n3haiaC81uTPY/NA0d8Q/I3Z9+ajc14102Q==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@biomejs/cli-darwin-x64@2.3.4: - resolution: {integrity: sha512-3s7TLVtjJ7ni1xADXsS7x7GMUrLBZXg8SemXc3T0XLslzvqKj/dq1xGeBQ+pOWQzng9MaozfacIHdK2UlJ3jGA==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@biomejs/cli-linux-arm64-musl@2.3.4: - resolution: {integrity: sha512-IruVGQRwMURivWazchiq7gKAqZSFs5so6gi0hJyxk7x6HR+iwZbO2IxNOqyLURBvL06qkIHs7Wffl6Bw30vCbQ==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@biomejs/cli-linux-arm64@2.3.4: - resolution: {integrity: sha512-y7efHyyM2gYmHy/AdWEip+VgTMe9973aP7XYKPzu/j8JxnPHuSUXftzmPhkVw0lfm4ECGbdBdGD6+rLmTgNZaA==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@biomejs/cli-linux-x64-musl@2.3.4: - resolution: {integrity: sha512-mzKFFv/w66e4/jCobFmD3kymCqG+FuWE7sVa4Yjqd9v7qt2UhXo67MSZKY9Ih18V2IwPzRKQPCw6KwdZs6AXSA==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@biomejs/cli-linux-x64@2.3.4: - resolution: {integrity: sha512-gKfjWR/6/dfIxPJCw8REdEowiXCkIpl9jycpNVHux8aX2yhWPLjydOshkDL6Y/82PcQJHn95VCj7J+BRcE5o1Q==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@biomejs/cli-win32-arm64@2.3.4: - resolution: {integrity: sha512-5TJ6JfVez+yyupJ/iGUici2wzKf0RrSAxJhghQXtAEsc67OIpdwSKAQboemILrwKfHDi5s6mu7mX+VTCTUydkw==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@biomejs/cli-win32-x64@2.3.4: - resolution: {integrity: sha512-FGCijXecmC4IedQ0esdYNlMpx0Jxgf4zceCaMu6fkjWyjgn50ZQtMiqZZQ0Q/77yqPxvtkgZAvt5uGw0gAAjig==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@clack/core@0.5.0: - resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==} - dependencies: - picocolors: 1.1.1 - sisteransi: 1.0.5 - dev: false - - /@clack/prompts@0.11.0: - resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==} - dependencies: - '@clack/core': 0.5.0 - picocolors: 1.1.1 - sisteransi: 1.0.5 - dev: false - - /@cspotcode/source-map-support@0.8.1: - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - dev: true - - /@esbuild/aix-ppc64@0.21.5: - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm64@0.21.5: - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm@0.21.5: - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-x64@0.21.5: - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-arm64@0.21.5: - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-x64@0.21.5: - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-arm64@0.21.5: - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-x64@0.21.5: - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm64@0.21.5: - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm@0.21.5: - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ia32@0.21.5: - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-loong64@0.21.5: - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-mips64el@0.21.5: - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ppc64@0.21.5: - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-riscv64@0.21.5: - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-s390x@0.21.5: - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-x64@0.21.5: - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/netbsd-x64@0.21.5: - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/openbsd-x64@0.21.5: - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/sunos-x64@0.21.5: - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-arm64@0.21.5: - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-ia32@0.21.5: - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-x64@0.21.5: - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@img/sharp-darwin-arm64@0.33.5: - resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.0.4 - dev: true - optional: true - - /@img/sharp-darwin-x64@0.33.5: - resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.0.4 - dev: true - optional: true - - /@img/sharp-libvips-darwin-arm64@1.0.4: - resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@img/sharp-libvips-darwin-x64@1.0.4: - resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@img/sharp-libvips-linux-arm64@1.0.4: - resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@img/sharp-libvips-linux-arm@1.0.5: - resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@img/sharp-libvips-linux-x64@1.0.4: - resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@img/sharp-libvips-linuxmusl-arm64@1.0.4: - resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@img/sharp-libvips-linuxmusl-x64@1.0.4: - resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@img/sharp-linux-arm64@0.33.5: - resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.0.4 - dev: true - optional: true - - /@img/sharp-linux-arm@0.33.5: - resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.0.5 - dev: true - optional: true - - /@img/sharp-linux-x64@0.33.5: - resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.0.4 - dev: true - optional: true - - /@img/sharp-linuxmusl-arm64@0.33.5: - resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 - dev: true - optional: true - - /@img/sharp-linuxmusl-x64@0.33.5: - resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 - dev: true - optional: true - - /@img/sharp-win32-x64@0.33.5: - resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@isaacs/balanced-match@4.0.1: - resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} - engines: {node: 20 || >=22} - dev: true - - /@isaacs/brace-expansion@5.0.0: - resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} - engines: {node: 20 || >=22} - dependencies: - '@isaacs/balanced-match': 4.0.1 - dev: true - - /@isaacs/fs-minipass@4.0.1: - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} - engines: {node: '>=18.0.0'} - dependencies: - minipass: 7.1.2 - dev: true - - /@jest/schemas@29.6.3: - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@sinclair/typebox': 0.27.8 - dev: true - - /@jridgewell/resolve-uri@3.1.2: - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - dev: true - - /@jridgewell/sourcemap-codec@1.5.5: - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - dev: true - - /@jridgewell/trace-mapping@0.3.9: - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - dev: true - - /@lancedb/lancedb-darwin-arm64@0.22.3: - resolution: {integrity: sha512-oP2Kic51nLqs27Xo+AzSVlcMgmmfZbU/PseQ3KBtU92rczO5DYU2St1Y7qDUWcjw+RF3H2v/DKzYed16h1wCBQ==} - engines: {node: '>= 18'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@lancedb/lancedb-darwin-x64@0.22.3: - resolution: {integrity: sha512-wOwgZkvBgQM8asjolz4NeyPa8W/AjZv4fwyQxJhTqKTGlB3ntrpdn1m84K5qncTmFFDcDfGgZ4DkNVkVK+ydoQ==} - engines: {node: '>= 18'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@lancedb/lancedb-linux-arm64-gnu@0.22.3: - resolution: {integrity: sha512-YUbFuBKQniTZOR9h2/es1f7lDzdHNt8qXs5GaqFmLQv2GNWpnvKXVA/vVffhCNpFB5nV132o1VhXW3KoMubPsw==} - engines: {node: '>= 18'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@lancedb/lancedb-linux-arm64-musl@0.22.3: - resolution: {integrity: sha512-jVRMtXxxYaDlZSaclCIHB2N+NJvQ1Fj9EaPeBx+HxG2VqUg0vXKef+yiaD2aGo9sAH6mMmkKJsrPhwABpUC4rQ==} - engines: {node: '>= 18'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@lancedb/lancedb-linux-x64-gnu@0.22.3: - resolution: {integrity: sha512-co7idTwvNAtbFoFHojhHlTpKsydOm5sZfbtAsQRdoa7g6a61yIrqrMm8D7Ngh756JfzZLFQBMkDUZEW3X4vP/g==} - engines: {node: '>= 18'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@lancedb/lancedb-linux-x64-musl@0.22.3: - resolution: {integrity: sha512-+ipFsn5PCODK7mOMq1gZ5OAZWks5YlgmjAlnYMmU8XxvaK0b6lZdA3s1hTmBaBO9+wv+31ulO55oBN4/U8Yldg==} - engines: {node: '>= 18'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@lancedb/lancedb-win32-arm64-msvc@0.22.3: - resolution: {integrity: sha512-E0XywJYnelIe4pzOlvog+aMHKt5ChW27tgmT2V80Z6PXcX6eN9I69Fj0Q6DK6z1YCTPIPu6Na1Hd6d4GqUNKPw==} - engines: {node: '>= 18'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@lancedb/lancedb-win32-x64-msvc@0.22.3: - resolution: {integrity: sha512-/1feFnjz5MIhzXOEU4+1OeGwpAFYczGfefuOGZRsmGWDdt4V6/fza7Hkkxyb2OnTzqpBfy6BdW2+iBguE1JMyQ==} - engines: {node: '>= 18'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@lancedb/lancedb@0.22.3(apache-arrow@18.1.0): - resolution: {integrity: sha512-nRC0fkg+d7dzCtudKHT+VH7znk6KUXRZyuS6HJYNnIrbvXBxaT6wAPjEbf70KTuqvP2znj48Zg+kiwRqkRnAJw==} - engines: {node: '>= 18'} - os: [darwin, linux, win32] - peerDependencies: - apache-arrow: '>=15.0.0 <=18.1.0' - dependencies: - apache-arrow: 18.1.0 - reflect-metadata: 0.2.2 - optionalDependencies: - '@lancedb/lancedb-darwin-arm64': 0.22.3 - '@lancedb/lancedb-darwin-x64': 0.22.3 - '@lancedb/lancedb-linux-arm64-gnu': 0.22.3 - '@lancedb/lancedb-linux-arm64-musl': 0.22.3 - '@lancedb/lancedb-linux-x64-gnu': 0.22.3 - '@lancedb/lancedb-linux-x64-musl': 0.22.3 - '@lancedb/lancedb-win32-arm64-msvc': 0.22.3 - '@lancedb/lancedb-win32-x64-msvc': 0.22.3 - dev: false - - /@lmdb/lmdb-darwin-arm64@3.4.4: - resolution: {integrity: sha512-XaKL705gDWd6XVls3ATDj13ZdML/LqSIxwgnYpG8xTzH2ifArx8fMMDdvqGE/Emd+W6R90W2fveZcJ0AyS8Y0w==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@lmdb/lmdb-darwin-x64@3.4.4: - resolution: {integrity: sha512-GPHGEVcwJlkD01GmIr7B4kvbIcUDS2+kBadVEd7lU4can1RZaZQLDDBJRrrNfS2Kavvl0VLI/cMv7UASAXGrww==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@lmdb/lmdb-linux-arm64@3.4.4: - resolution: {integrity: sha512-mALqr7DE42HsiwVTKpQWxacjHoJk+e9p00RWIJqTACh/hpucxp/0lK/XMh5XzWnU/TDCZLukq1+vNqnNumTP/Q==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@lmdb/lmdb-linux-arm@3.4.4: - resolution: {integrity: sha512-cmev5/dZr5ACKri9f6GU6lZCXTjMhV72xujlbOhFCgFXrt4W0TxGsmY8kA1BITvH60JBKE50cSxsiulybAbrrw==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@lmdb/lmdb-linux-x64@3.4.4: - resolution: {integrity: sha512-QjLs8OcmCNcraAcLoZyFlo0atzBJniQLLwhtR+ymQqS5kLYpV5RqwriL87BW+ZiR9ZiGgZx3evrz5vnWPtJ1fQ==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@lmdb/lmdb-win32-arm64@3.4.4: - resolution: {integrity: sha512-tr/pwHDlZ33forLGAr0tI04cRmP4SgF93yHbb+2zvZiDEyln5yMHhbKDySxY66aUOkhvBvTuHq9q/3YmTj6ZHQ==} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@lmdb/lmdb-win32-x64@3.4.4: - resolution: {integrity: sha512-KRzfocJzB/mgoTCqnMawuLSKheHRVTqWfSmouIgYpFs6Hx4zvZSvsZKSCEb5gHmICy7qsx9l06jk3MFTtiFVAQ==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@modelcontextprotocol/sdk@1.24.3(zod@4.1.13): - resolution: {integrity: sha512-YgSHW29fuzKKAHTGe9zjNoo+yF8KaQPzDC2W9Pv41E7/57IfY+AMGJ/aDFlgTLcVVELoggKE4syABCE75u3NCw==} - engines: {node: '>=18'} - peerDependencies: - '@cfworker/json-schema': ^4.1.1 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - '@cfworker/json-schema': - optional: true - dependencies: - ajv: 8.17.1 - ajv-formats: 3.0.1(ajv@8.17.1) - content-type: 1.0.5 - cors: 2.8.5 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - eventsource-parser: 3.0.6 - express: 5.2.1 - express-rate-limit: 7.5.1(express@5.2.1) - jose: 6.1.3 - pkce-challenge: 5.0.1 - raw-body: 3.0.2 - zod: 4.1.13 - zod-to-json-schema: 3.25.0(zod@4.1.13) - transitivePeerDependencies: - - supports-color - dev: false - - /@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3: - resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3: - resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3: - resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3: - resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3: - resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3: - resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@nodelib/fs.scandir@2.1.5: - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - dev: false - - /@nodelib/fs.stat@2.0.5: - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - dev: false - - /@nodelib/fs.walk@1.2.8: - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 - dev: false - - /@npmcli/agent@4.0.0: - resolution: {integrity: sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==} - engines: {node: ^20.17.0 || >=22.9.0} - dependencies: - agent-base: 7.1.4 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - lru-cache: 11.2.4 - socks-proxy-agent: 8.0.5 - transitivePeerDependencies: - - supports-color - dev: true - - /@npmcli/fs@5.0.0: - resolution: {integrity: sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==} - engines: {node: ^20.17.0 || >=22.9.0} - dependencies: - semver: 7.7.3 - dev: true - - /@rollup/rollup-android-arm-eabi@4.53.3: - resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-android-arm64@4.53.3: - resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-darwin-arm64@4.53.3: - resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-darwin-x64@4.53.3: - resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-freebsd-arm64@4.53.3: - resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-freebsd-x64@4.53.3: - resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm-gnueabihf@4.53.3: - resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm-musleabihf@4.53.3: - resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm64-gnu@4.53.3: - resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm64-musl@4.53.3: - resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-loong64-gnu@4.53.3: - resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-ppc64-gnu@4.53.3: - resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-riscv64-gnu@4.53.3: - resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-riscv64-musl@4.53.3: - resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-s390x-gnu@4.53.3: - resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-x64-gnu@4.53.3: - resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-x64-musl@4.53.3: - resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-openharmony-arm64@4.53.3: - resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} - cpu: [arm64] - os: [openharmony] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-arm64-msvc@4.53.3: - resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-ia32-msvc@4.53.3: - resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-x64-gnu@4.53.3: - resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-x64-msvc@4.53.3: - resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@sinclair/typebox@0.27.8: - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - dev: true - - /@swc/helpers@0.5.17: - resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} - dependencies: - tslib: 2.8.1 - dev: false - - /@tsconfig/node10@1.0.12: - resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} - dev: true - - /@tsconfig/node12@1.0.11: - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} - dev: true - - /@tsconfig/node14@1.0.3: - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} - dev: true - - /@tsconfig/node16@1.0.4: - resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - dev: true - - /@types/command-line-args@5.2.3: - resolution: {integrity: sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==} - dev: false - - /@types/command-line-usage@5.0.4: - resolution: {integrity: sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==} - dev: false - - /@types/estree@1.0.8: - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - dev: true - - /@types/node@20.19.25: - resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==} - dependencies: - undici-types: 6.21.0 - dev: false - - /@types/node@24.10.1: - resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} - dependencies: - undici-types: 7.16.0 - dev: true - - /@types/uuid@9.0.8: - resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} - dev: true - - /@vitest/expect@1.6.1: - resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==} - dependencies: - '@vitest/spy': 1.6.1 - '@vitest/utils': 1.6.1 - chai: 4.5.0 - dev: true - - /@vitest/runner@1.6.1: - resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} - dependencies: - '@vitest/utils': 1.6.1 - p-limit: 5.0.0 - pathe: 1.1.2 - dev: true - - /@vitest/snapshot@1.6.1: - resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} - dependencies: - magic-string: 0.30.21 - pathe: 1.1.2 - pretty-format: 29.7.0 - dev: true - - /@vitest/spy@1.6.1: - resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==} - dependencies: - tinyspy: 2.2.1 - dev: true - - /@vitest/utils@1.6.1: - resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==} - dependencies: - diff-sequences: 29.6.3 - estree-walker: 3.0.3 - loupe: 2.3.7 - pretty-format: 29.7.0 - dev: true - - /abbrev@4.0.0: - resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} - engines: {node: ^20.17.0 || >=22.9.0} - dev: true - - /accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} - dependencies: - mime-types: 3.0.2 - negotiator: 1.0.0 - dev: false - - /acorn-walk@8.3.4: - resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} - engines: {node: '>=0.4.0'} - dependencies: - acorn: 8.15.0 - dev: true - - /acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true - - /agent-base@7.1.4: - resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} - engines: {node: '>= 14'} - dev: true - - /ajv-formats@3.0.1(ajv@8.17.1): - resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - dependencies: - ajv: 8.17.1 - dev: false - - /ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - dev: false - - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - dev: false - - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - dependencies: - color-convert: 2.0.1 - dev: false - - /ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - dev: true - - /any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - dev: false - - /apache-arrow@18.1.0: - resolution: {integrity: sha512-v/ShMp57iBnBp4lDgV8Jx3d3Q5/Hac25FWmQ98eMahUiHPXcvwIMKJD0hBIgclm/FCG+LwPkAKtkRO1O/W0YGg==} - hasBin: true - dependencies: - '@swc/helpers': 0.5.17 - '@types/command-line-args': 5.2.3 - '@types/command-line-usage': 5.0.4 - '@types/node': 20.19.25 - command-line-args: 5.2.1 - command-line-usage: 7.0.3 - flatbuffers: 24.12.23 - json-bignum: 0.0.3 - tslib: 2.8.1 - dev: false - - /arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - dev: true - - /array-back@3.1.0: - resolution: {integrity: sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==} - engines: {node: '>=6'} - dev: false - - /array-back@6.2.2: - resolution: {integrity: sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==} - engines: {node: '>=12.17'} - dev: false - - /assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} - dev: true - - /base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: false - - /benchmark@2.1.4: - resolution: {integrity: sha512-l9MlfN4M1K/H2fbhfMy3B7vJd6AGKJVQn2h6Sg/Yx+KckoUA7ewS5Vv6TjSq18ooE1kS9hhAlQRH3AkXIh/aOQ==} - requiresBuild: true - dependencies: - lodash: 4.17.21 - platform: 1.3.6 - dev: false - optional: true - - /bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - dependencies: - file-uri-to-path: 1.0.0 - dev: false - - /bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.2 - dev: false - - /body-parser@2.2.1: - resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} - engines: {node: '>=18'} - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 4.4.3 - http-errors: 2.0.1 - iconv-lite: 0.7.0 - on-finished: 2.4.1 - qs: 6.14.0 - raw-body: 3.0.2 - type-is: 2.0.1 - transitivePeerDependencies: - - supports-color - dev: false - - /braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - dependencies: - fill-range: 7.1.1 - dev: false - - /buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - dev: false - - /bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - dev: false - - /cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - dev: true - - /cacache@20.0.3: - resolution: {integrity: sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==} - engines: {node: ^20.17.0 || >=22.9.0} - dependencies: - '@npmcli/fs': 5.0.0 - fs-minipass: 3.0.3 - glob: 13.0.0 - lru-cache: 11.2.4 - minipass: 7.1.2 - minipass-collect: 2.0.1 - minipass-flush: 1.0.5 - minipass-pipeline: 1.2.4 - p-map: 7.0.4 - ssri: 13.0.0 - unique-filename: 5.0.0 - dev: true - - /call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - dev: false - - /call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - dev: false - - /chai@4.5.0: - resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} - engines: {node: '>=4'} - dependencies: - assertion-error: 1.1.0 - check-error: 1.0.3 - deep-eql: 4.1.4 - get-func-name: 2.0.2 - loupe: 2.3.7 - pathval: 1.1.1 - type-detect: 4.1.0 - dev: true - - /chalk-template@0.4.0: - resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} - engines: {node: '>=12'} - dependencies: - chalk: 4.1.2 - dev: false - - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - dev: false - - /chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - dev: false - - /check-error@1.0.3: - resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} - dependencies: - get-func-name: 2.0.2 - dev: true - - /chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - dependencies: - readdirp: 4.1.2 - dev: false - - /chownr@3.0.0: - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} - engines: {node: '>=18'} - dev: true - - /cli-cursor@3.1.0: - resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} - engines: {node: '>=8'} - dependencies: - restore-cursor: 3.1.0 - dev: false - - /cli-highlight@2.1.11: - resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} - engines: {node: '>=8.0.0', npm: '>=5.0.0'} - hasBin: true - dependencies: - chalk: 4.1.2 - highlight.js: 10.7.3 - mz: 2.7.0 - parse5: 5.1.1 - parse5-htmlparser2-tree-adapter: 6.0.1 - yargs: 16.2.0 - dev: false - - /cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} - dev: false - - /cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - dev: false - - /clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} - dev: false - - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - dependencies: - color-name: 1.1.4 - dev: false - - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: false - - /command-line-args@5.2.1: - resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==} - engines: {node: '>=4.0.0'} - dependencies: - array-back: 3.1.0 - find-replace: 3.0.0 - lodash.camelcase: 4.3.0 - typical: 4.0.0 - dev: false - - /command-line-usage@7.0.3: - resolution: {integrity: sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==} - engines: {node: '>=12.20.0'} - dependencies: - array-back: 6.2.2 - chalk-template: 0.4.0 - table-layout: 4.1.1 - typical: 7.3.0 - dev: false - - /commander@14.0.2: - resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} - engines: {node: '>=20'} - dev: false - - /complex.js@2.4.3: - resolution: {integrity: sha512-UrQVSUur14tNX6tiP4y8T4w4FeJAX3bi2cIv0pu/DTLFNxoq7z2Yh83Vfzztj6Px3X/lubqQ9IrPp7Bpn6p4MQ==} - requiresBuild: true - dev: false - optional: true - - /confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - dev: true - - /content-disposition@1.0.1: - resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} - engines: {node: '>=18'} - dev: false - - /content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - dev: false - - /cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - dev: false - - /cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - dev: false - - /cors@2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} - engines: {node: '>= 0.10'} - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - dev: false - - /create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - dev: true - - /cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - /debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.3 - - /decimal.js@10.6.0: - resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} - requiresBuild: true - dev: false - optional: true - - /deep-eql@4.1.4: - resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} - engines: {node: '>=6'} - dependencies: - type-detect: 4.1.0 - dev: true - - /defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - dependencies: - clone: 1.0.4 - dev: false - - /depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - dev: false - - /detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - dev: false - - /diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true - - /diff@4.0.2: - resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} - engines: {node: '>=0.3.1'} - dev: true - - /dotenv@17.2.3: - resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} - engines: {node: '>=12'} - dev: false - - /dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - dev: false - - /ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - dev: false - - /emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: false - - /encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - dev: false - - /encoding@0.1.13: - resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} - requiresBuild: true - dependencies: - iconv-lite: 0.6.3 - dev: true - optional: true - - /env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} - dev: true - - /err-code@2.0.3: - resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} - dev: true - - /es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - dev: false - - /es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - dev: false - - /es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - dependencies: - es-errors: 1.3.0 - dev: false - - /esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 - dev: true - - /escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - dev: false - - /escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - dev: false - - /escape-latex@1.2.0: - resolution: {integrity: sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==} - requiresBuild: true - dev: false - optional: true - - /estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - dependencies: - '@types/estree': 1.0.8 - dev: true - - /etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - dev: false - - /eventsource-parser@3.0.6: - resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} - engines: {node: '>=18.0.0'} - dev: false - - /eventsource@3.0.7: - resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} - engines: {node: '>=18.0.0'} - dependencies: - eventsource-parser: 3.0.6 - dev: false - - /execa@8.0.1: - resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} - engines: {node: '>=16.17'} - dependencies: - cross-spawn: 7.0.6 - get-stream: 8.0.1 - human-signals: 5.0.0 - is-stream: 3.0.0 - merge-stream: 2.0.0 - npm-run-path: 5.3.0 - onetime: 6.0.0 - signal-exit: 4.1.0 - strip-final-newline: 3.0.0 - dev: true - - /exponential-backoff@3.1.3: - resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} - dev: true - - /express-rate-limit@7.5.1(express@5.2.1): - resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} - engines: {node: '>= 16'} - peerDependencies: - express: '>= 4.11' - dependencies: - express: 5.2.1 - dev: false - - /express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} - engines: {node: '>= 18'} - dependencies: - accepts: 2.0.0 - body-parser: 2.2.1 - content-disposition: 1.0.1 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.3 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.1 - fresh: 2.0.0 - http-errors: 2.0.1 - merge-descriptors: 2.0.0 - mime-types: 3.0.2 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.14.0 - range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.0 - serve-static: 2.2.0 - statuses: 2.0.2 - type-is: 2.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - dev: false - - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: false - - /fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - dev: false - - /fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - dev: false - - /fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - dependencies: - reusify: 1.1.0 - dev: false - - /fdir@6.5.0(picomatch@4.0.3): - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - dependencies: - picomatch: 4.0.3 - dev: true - - /file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - dev: false - - /fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - dependencies: - to-regex-range: 5.0.1 - dev: false - - /finalhandler@2.1.1: - resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} - engines: {node: '>= 18.0.0'} - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - dev: false - - /find-replace@3.0.0: - resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==} - engines: {node: '>=4.0.0'} - dependencies: - array-back: 3.1.0 - dev: false - - /flatbuffers@24.12.23: - resolution: {integrity: sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA==} - dev: false - - /forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - dev: false - - /fraction.js@5.3.4: - resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - requiresBuild: true - dev: false - optional: true - - /fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} - dev: false - - /fs-minipass@3.0.3: - resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - minipass: 7.1.2 - dev: true - - /fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - dev: false - - /get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - dev: false - - /get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} - dev: true - - /get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - dev: false - - /get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - dev: false - - /get-stream@8.0.1: - resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} - engines: {node: '>=16'} - dev: true - - /glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - dependencies: - is-glob: 4.0.3 - dev: false - - /glob@13.0.0: - resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} - engines: {node: 20 || >=22} - dependencies: - minimatch: 10.1.1 - minipass: 7.1.2 - path-scurry: 2.0.1 - dev: true - - /gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - dev: false - - /graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - dev: true - - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - dev: false - - /has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - dev: false - - /hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - dependencies: - function-bind: 1.1.2 - dev: false - - /highlight.js@10.7.3: - resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - dev: false - - /http-cache-semantics@4.2.0: - resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} - dev: true - - /http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 - dev: false - - /http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - dev: true - - /https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - dev: true - - /human-signals@5.0.0: - resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} - engines: {node: '>=16.17.0'} - dev: true - - /iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - requiresBuild: true - dependencies: - safer-buffer: 2.1.2 - dev: true - optional: true - - /iconv-lite@0.7.0: - resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} - engines: {node: '>=0.10.0'} - dependencies: - safer-buffer: 2.1.2 - dev: false - - /ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: false - - /ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - dev: false - - /imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - dev: true - - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: false - - /ip-address@10.1.0: - resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} - engines: {node: '>= 12'} - dev: true - - /ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - dev: false - - /is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - dev: false - - /is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - dev: false - - /is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - dependencies: - is-extglob: 2.1.1 - dev: false - - /is-interactive@1.0.0: - resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} - engines: {node: '>=8'} - dev: false - - /is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - dev: false - - /is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - dev: false - - /is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - - /is-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} - dev: false - - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - /isexe@3.1.1: - resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} - engines: {node: '>=16'} - dev: true - - /javascript-natural-sort@0.7.1: - resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} - requiresBuild: true - dev: false - optional: true - - /jose@6.1.3: - resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} - dev: false - - /js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - dev: true - - /json-bignum@0.0.3: - resolution: {integrity: sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==} - engines: {node: '>=0.8'} - dev: false - - /json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - dev: false - - /lmdb@3.4.4: - resolution: {integrity: sha512-+Y2DqovevLkb6DrSQ6SXTYLEd6kvlRbhsxzgJrk7BUfOVA/mt21ak6pFDZDKxiAczHMWxrb02kXBTSTIA0O94A==} - hasBin: true - requiresBuild: true - dependencies: - msgpackr: 1.11.5 - node-addon-api: 6.1.0 - node-gyp-build-optional-packages: 5.2.2 - ordered-binary: 1.6.0 - weak-lru-cache: 1.2.2 - optionalDependencies: - '@lmdb/lmdb-darwin-arm64': 3.4.4 - '@lmdb/lmdb-darwin-x64': 3.4.4 - '@lmdb/lmdb-linux-arm': 3.4.4 - '@lmdb/lmdb-linux-arm64': 3.4.4 - '@lmdb/lmdb-linux-x64': 3.4.4 - '@lmdb/lmdb-win32-arm64': 3.4.4 - '@lmdb/lmdb-win32-x64': 3.4.4 - dev: false - - /local-pkg@0.5.1: - resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} - engines: {node: '>=14'} - dependencies: - mlly: 1.8.0 - pkg-types: 1.3.1 - dev: true - - /lodash.camelcase@4.3.0: - resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - dev: false - - /lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - requiresBuild: true - dev: false - optional: true - - /log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} - dependencies: - chalk: 4.1.2 - is-unicode-supported: 0.1.0 - dev: false - - /loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} - dependencies: - get-func-name: 2.0.2 - dev: true - - /lru-cache@11.2.4: - resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} - engines: {node: 20 || >=22} - dev: true - - /magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - dev: true - - /make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - dev: true - - /make-fetch-happen@15.0.3: - resolution: {integrity: sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==} - engines: {node: ^20.17.0 || >=22.9.0} - dependencies: - '@npmcli/agent': 4.0.0 - cacache: 20.0.3 - http-cache-semantics: 4.2.0 - minipass: 7.1.2 - minipass-fetch: 5.0.0 - minipass-flush: 1.0.5 - minipass-pipeline: 1.2.4 - negotiator: 1.0.0 - proc-log: 6.1.0 - promise-retry: 2.0.1 - ssri: 13.0.0 - transitivePeerDependencies: - - supports-color - dev: true - - /math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - dev: false - - /mathjs@14.9.1: - resolution: {integrity: sha512-xhqv8Xjf+caWG3WlaPekg4v8QFOR3D5+8ycfcjMcPcnCNDgAONQLaLfyGgrggJrcHx2yUGCpACRpiD4GmXwX+Q==} - engines: {node: '>= 18'} - hasBin: true - requiresBuild: true - dependencies: - '@babel/runtime': 7.28.4 - complex.js: 2.4.3 - decimal.js: 10.6.0 - escape-latex: 1.2.0 - fraction.js: 5.3.4 - javascript-natural-sort: 0.7.1 - seedrandom: 3.0.5 - tiny-emitter: 2.1.0 - typed-function: 4.2.2 - dev: false - optional: true - - /media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} - dev: false - - /merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} - dev: false - - /merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - dev: true - - /merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - dev: false - - /micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - dev: false - - /mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} - dev: false - - /mime-types@3.0.2: - resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} - engines: {node: '>=18'} - dependencies: - mime-db: 1.54.0 - dev: false - - /mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - dev: false - - /mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - dev: true - - /minimatch@10.1.1: - resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} - engines: {node: 20 || >=22} - dependencies: - '@isaacs/brace-expansion': 5.0.0 - dev: true - - /minipass-collect@2.0.1: - resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} - engines: {node: '>=16 || 14 >=14.17'} - dependencies: - minipass: 7.1.2 - dev: true - - /minipass-fetch@5.0.0: - resolution: {integrity: sha512-fiCdUALipqgPWrOVTz9fw0XhcazULXOSU6ie40DDbX1F49p1dBrSRBuswndTx1x3vEb/g0FT7vC4c4C2u/mh3A==} - engines: {node: ^20.17.0 || >=22.9.0} - dependencies: - minipass: 7.1.2 - minipass-sized: 1.0.3 - minizlib: 3.1.0 - optionalDependencies: - encoding: 0.1.13 - dev: true - - /minipass-flush@1.0.5: - resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} - engines: {node: '>= 8'} - dependencies: - minipass: 3.3.6 - dev: true - - /minipass-pipeline@1.2.4: - resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} - engines: {node: '>=8'} - dependencies: - minipass: 3.3.6 - dev: true - - /minipass-sized@1.0.3: - resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} - engines: {node: '>=8'} - dependencies: - minipass: 3.3.6 - dev: true - - /minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} - dependencies: - yallist: 4.0.0 - dev: true - - /minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - dev: true - - /minizlib@3.1.0: - resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} - engines: {node: '>= 18'} - dependencies: - minipass: 7.1.2 - dev: true - - /mlly@1.8.0: - resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} - dependencies: - acorn: 8.15.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.1 - dev: true - - /ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - /msgpackr-extract@3.0.3: - resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} - hasBin: true - requiresBuild: true - dependencies: - node-gyp-build-optional-packages: 5.2.2 - optionalDependencies: - '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 - dev: false - optional: true - - /msgpackr@1.11.5: - resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} - optionalDependencies: - msgpackr-extract: 3.0.3 - dev: false - - /mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - dev: false - - /nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - dev: true - - /negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - - /node-addon-api@6.1.0: - resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} - dev: false - - /node-addon-api@8.5.0: - resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==} - engines: {node: ^18 || ^20 || >= 21} - dev: false - - /node-gyp-build-optional-packages@5.2.2: - resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} - hasBin: true - dependencies: - detect-libc: 2.1.2 - dev: false - - /node-gyp-build@4.8.4: - resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} - hasBin: true - dev: false - - /node-gyp@12.1.0: - resolution: {integrity: sha512-W+RYA8jBnhSr2vrTtlPYPc1K+CSjGpVDRZxcqJcERZ8ND3A1ThWPHRwctTx3qC3oW99jt726jhdz3Y6ky87J4g==} - engines: {node: ^20.17.0 || >=22.9.0} - hasBin: true - dependencies: - env-paths: 2.2.1 - exponential-backoff: 3.1.3 - graceful-fs: 4.2.11 - make-fetch-happen: 15.0.3 - nopt: 9.0.0 - proc-log: 6.1.0 - semver: 7.7.3 - tar: 7.5.2 - tinyglobby: 0.2.15 - which: 6.0.0 - transitivePeerDependencies: - - supports-color - dev: true - - /nopt@9.0.0: - resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==} - engines: {node: ^20.17.0 || >=22.9.0} - hasBin: true - dependencies: - abbrev: 4.0.0 - dev: true - - /npm-run-path@5.3.0: - resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - path-key: 4.0.0 - dev: true - - /object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - dev: false - - /object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - dev: false - - /on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - dependencies: - ee-first: 1.1.1 - dev: false - - /once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - dependencies: - wrappy: 1.0.2 - dev: false - - /onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - dependencies: - mimic-fn: 2.1.0 - dev: false - - /onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} - dependencies: - mimic-fn: 4.0.0 - dev: true - - /ora@5.4.1: - resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} - engines: {node: '>=10'} - dependencies: - bl: 4.1.0 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-spinners: 2.9.2 - is-interactive: 1.0.0 - is-unicode-supported: 0.1.0 - log-symbols: 4.1.0 - strip-ansi: 6.0.1 - wcwidth: 1.0.1 - dev: false - - /ordered-binary@1.6.0: - resolution: {integrity: sha512-IQh2aMfMIDbPjI/8a3Edr+PiOpcsB7yo8NdW7aHWVaoR/pcDldunMvnnwbk/auPGqmKeAdxtZl7MHX/QmPwhvQ==} - dev: false - - /p-limit@5.0.0: - resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} - engines: {node: '>=18'} - dependencies: - yocto-queue: 1.2.2 - dev: true - - /p-map@7.0.4: - resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} - engines: {node: '>=18'} - dev: true - - /parse5-htmlparser2-tree-adapter@6.0.1: - resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} - dependencies: - parse5: 6.0.1 - dev: false - - /parse5@5.1.1: - resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} - dev: false - - /parse5@6.0.1: - resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} - dev: false - - /parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - dev: false - - /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - /path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - dev: true - - /path-scurry@2.0.1: - resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} - engines: {node: 20 || >=22} - dependencies: - lru-cache: 11.2.4 - minipass: 7.1.2 - dev: true - - /path-to-regexp@8.3.0: - resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} - dev: false - - /pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - dev: true - - /pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - dev: true - - /pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} - dev: true - - /picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - /picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - dev: false - - /picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} - dev: true - - /pkce-challenge@5.0.1: - resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} - engines: {node: '>=16.20.0'} - dev: false - - /pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - dependencies: - confbox: 0.1.8 - mlly: 1.8.0 - pathe: 2.0.3 - dev: true - - /platform@1.3.6: - resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} - requiresBuild: true - dev: false - optional: true - - /postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} - engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - dev: true - - /pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/schemas': 29.6.3 - ansi-styles: 5.2.0 - react-is: 18.3.1 - dev: true - - /proc-log@6.1.0: - resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} - engines: {node: ^20.17.0 || >=22.9.0} - dev: true - - /promise-retry@2.0.1: - resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} - engines: {node: '>=10'} - dependencies: - err-code: 2.0.3 - retry: 0.12.0 - dev: true - - /proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - dev: false - - /qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} - engines: {node: '>=0.6'} - dependencies: - side-channel: 1.1.0 - dev: false - - /queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: false - - /range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - dev: false - - /raw-body@3.0.2: - resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} - engines: {node: '>= 0.10'} - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.7.0 - unpipe: 1.0.0 - dev: false - - /react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - dev: true - - /readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - dev: false - - /readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} - dev: false - - /reflect-metadata@0.2.2: - resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} - dev: false - - /require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - dev: false - - /require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - dev: false - - /restore-cursor@3.1.0: - resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} - engines: {node: '>=8'} - dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 - dev: false - - /retry@0.12.0: - resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} - engines: {node: '>= 4'} - dev: true - - /reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: false - - /rollup@4.53.3: - resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.53.3 - '@rollup/rollup-android-arm64': 4.53.3 - '@rollup/rollup-darwin-arm64': 4.53.3 - '@rollup/rollup-darwin-x64': 4.53.3 - '@rollup/rollup-freebsd-arm64': 4.53.3 - '@rollup/rollup-freebsd-x64': 4.53.3 - '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 - '@rollup/rollup-linux-arm-musleabihf': 4.53.3 - '@rollup/rollup-linux-arm64-gnu': 4.53.3 - '@rollup/rollup-linux-arm64-musl': 4.53.3 - '@rollup/rollup-linux-loong64-gnu': 4.53.3 - '@rollup/rollup-linux-ppc64-gnu': 4.53.3 - '@rollup/rollup-linux-riscv64-gnu': 4.53.3 - '@rollup/rollup-linux-riscv64-musl': 4.53.3 - '@rollup/rollup-linux-s390x-gnu': 4.53.3 - '@rollup/rollup-linux-x64-gnu': 4.53.3 - '@rollup/rollup-linux-x64-musl': 4.53.3 - '@rollup/rollup-openharmony-arm64': 4.53.3 - '@rollup/rollup-win32-arm64-msvc': 4.53.3 - '@rollup/rollup-win32-ia32-msvc': 4.53.3 - '@rollup/rollup-win32-x64-gnu': 4.53.3 - '@rollup/rollup-win32-x64-msvc': 4.53.3 - fsevents: 2.3.3 - dev: true - - /router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - dependencies: - debug: 4.4.3 - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.3.0 - transitivePeerDependencies: - - supports-color - dev: false - - /run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - dependencies: - queue-microtask: 1.2.3 - dev: false - - /safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - dev: false - - /safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - requiresBuild: true - - /seedrandom@3.0.5: - resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==} - requiresBuild: true - dev: false - optional: true - - /semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} - engines: {node: '>=10'} - hasBin: true - dev: true - - /send@1.2.0: - resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} - engines: {node: '>= 18'} - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.1 - mime-types: 3.0.2 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - dev: false - - /serve-static@2.2.0: - resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} - engines: {node: '>= 18'} - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.0 - transitivePeerDependencies: - - supports-color - dev: false - - /setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - dev: false - - /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - dependencies: - shebang-regex: 3.0.0 - - /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - /side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} - engines: {node: '>= 0.4'} - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - dev: false - - /side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - dev: false - - /side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - dev: false - - /side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.0 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - dev: false - - /siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - dev: true - - /signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: false - - /signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - dev: true - - /simsimd@6.5.5: - resolution: {integrity: sha512-iMZLxr/rWOOhg+L0QimMMvehntzJrPMLizvOXr3VZtQHazskaIRuBtui9QtlZf8iXuPFDBwzLwAdrm/J3U/HwA==} - engines: {node: ~10 >=10.20 || >=12.17} - requiresBuild: true - dependencies: - bindings: 1.5.0 - node-addon-api: 8.5.0 - node-gyp-build: 4.8.4 - optionalDependencies: - benchmark: 2.1.4 - mathjs: 14.9.1 - usearch: 2.21.4 - dev: false - - /sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - dev: false - - /smart-buffer@4.2.0: - resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} - engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - dev: true - - /socks-proxy-agent@8.0.5: - resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} - engines: {node: '>= 14'} - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - socks: 2.8.7 - transitivePeerDependencies: - - supports-color - dev: true - - /socks@2.8.7: - resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} - engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} - dependencies: - ip-address: 10.1.0 - smart-buffer: 4.2.0 - dev: true - - /source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - dev: true - - /ssri@13.0.0: - resolution: {integrity: sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==} - engines: {node: ^20.17.0 || >=22.9.0} - dependencies: - minipass: 7.1.2 - dev: true - - /stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - dev: true - - /statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} - dev: false - - /std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - dev: true - - /string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - dev: false - - /string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - dependencies: - safe-buffer: 5.2.1 - dev: false - - /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - dependencies: - ansi-regex: 5.0.1 - dev: false - - /strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} - dev: true - - /strip-literal@2.1.1: - resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} - dependencies: - js-tokens: 9.0.1 - dev: true - - /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - dependencies: - has-flag: 4.0.0 - dev: false - - /table-layout@4.1.1: - resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==} - engines: {node: '>=12.17'} - dependencies: - array-back: 6.2.2 - wordwrapjs: 5.1.1 - dev: false - - /tar@7.5.2: - resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} - engines: {node: '>=18'} - dependencies: - '@isaacs/fs-minipass': 4.0.1 - chownr: 3.0.0 - minipass: 7.1.2 - minizlib: 3.1.0 - yallist: 5.0.0 - dev: true - - /thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - dependencies: - thenify: 3.3.1 - dev: false - - /thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - dependencies: - any-promise: 1.3.0 - dev: false - - /tiny-emitter@2.1.0: - resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==} - requiresBuild: true - dev: false - optional: true - - /tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - dev: true - - /tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} - dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - dev: true - - /tinypool@0.8.4: - resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} - engines: {node: '>=14.0.0'} - dev: true - - /tinyspy@2.2.1: - resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} - engines: {node: '>=14.0.0'} - dev: true - - /to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - dependencies: - is-number: 7.0.0 - dev: false - - /toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - dev: false - - /ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3): - resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} - hasBin: true - peerDependencies: - '@swc/core': '>=1.2.50' - '@swc/wasm': '>=1.2.50' - '@types/node': '*' - typescript: '>=2.7' - peerDependenciesMeta: - '@swc/core': - optional: true - '@swc/wasm': - optional: true - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.12 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 24.10.1 - acorn: 8.15.0 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.9.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - dev: true - - /tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - dev: false - - /type-detect@4.1.0: - resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} - engines: {node: '>=4'} - dev: true - - /type-is@2.0.1: - resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} - engines: {node: '>= 0.6'} - dependencies: - content-type: 1.0.5 - media-typer: 1.1.0 - mime-types: 3.0.2 - dev: false - - /typed-function@4.2.2: - resolution: {integrity: sha512-VwaXim9Gp1bngi/q3do8hgttYn2uC3MoT/gfuMWylnj1IeZBUAyPddHZlo1K05BDoj8DYPpMdiHqH1dDYdJf2A==} - engines: {node: '>= 18'} - requiresBuild: true - dev: false - optional: true - - /typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - dev: true - - /typical@4.0.0: - resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==} - engines: {node: '>=8'} - dev: false - - /typical@7.3.0: - resolution: {integrity: sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==} - engines: {node: '>=12.17'} - dev: false - - /ufo@1.6.1: - resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} - dev: true - - /undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - dev: false - - /undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - dev: true - - /unique-filename@5.0.0: - resolution: {integrity: sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==} - engines: {node: ^20.17.0 || >=22.9.0} - dependencies: - unique-slug: 6.0.0 - dev: true - - /unique-slug@6.0.0: - resolution: {integrity: sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==} - engines: {node: ^20.17.0 || >=22.9.0} - dependencies: - imurmurhash: 0.1.4 - dev: true - - /unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - dev: false - - /usearch@2.21.4: - resolution: {integrity: sha512-AzavmhAfGubKOLdR3S6Rh/6dvgXqxL+6Fzs1fsgKneQG8i7oLX2Gpqsc4EfdSyKb4sQXhavIiKIguMA2R3cRaA==} - engines: {node: ~10 >=10.20 || >=12.17} - requiresBuild: true - dependencies: - bindings: 1.5.0 - node-addon-api: 8.5.0 - node-gyp-build: 4.8.4 - dev: false - optional: true - - /util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: false - - /uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - hasBin: true - dev: false - - /v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - dev: true - - /vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - dev: false - - /vite-node@1.6.1(@types/node@24.10.1): - resolution: {integrity: sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - dependencies: - cac: 6.7.14 - debug: 4.4.3 - pathe: 1.1.2 - picocolors: 1.1.1 - vite: 5.4.21(@types/node@24.10.1) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - dev: true - - /vite@5.4.21(@types/node@24.10.1): - resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - dependencies: - '@types/node': 24.10.1 - esbuild: 0.21.5 - postcss: 8.5.6 - rollup: 4.53.3 - optionalDependencies: - fsevents: 2.3.3 - dev: true - - /vitest@1.6.1(@types/node@24.10.1): - resolution: {integrity: sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 1.6.1 - '@vitest/ui': 1.6.1 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - dependencies: - '@types/node': 24.10.1 - '@vitest/expect': 1.6.1 - '@vitest/runner': 1.6.1 - '@vitest/snapshot': 1.6.1 - '@vitest/spy': 1.6.1 - '@vitest/utils': 1.6.1 - acorn-walk: 8.3.4 - chai: 4.5.0 - debug: 4.4.3 - execa: 8.0.1 - local-pkg: 0.5.1 - magic-string: 0.30.21 - pathe: 1.1.2 - picocolors: 1.1.1 - std-env: 3.10.0 - strip-literal: 2.1.1 - tinybench: 2.9.0 - tinypool: 0.8.4 - vite: 5.4.21(@types/node@24.10.1) - vite-node: 1.6.1(@types/node@24.10.1) - why-is-node-running: 2.3.0 - transitivePeerDependencies: - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - dev: true - - /wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - dependencies: - defaults: 1.0.4 - dev: false - - /weak-lru-cache@1.2.2: - resolution: {integrity: sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==} - dev: false - - /web-tree-sitter@0.25.10: - resolution: {integrity: sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA==} - peerDependencies: - '@types/emscripten': ^1.40.0 - peerDependenciesMeta: - '@types/emscripten': - optional: true - dev: false - - /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - dependencies: - isexe: 2.0.0 - - /which@6.0.0: - resolution: {integrity: sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==} - engines: {node: ^20.17.0 || >=22.9.0} - hasBin: true - dependencies: - isexe: 3.1.1 - dev: true - - /why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - dev: true - - /wordwrapjs@5.1.1: - resolution: {integrity: sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==} - engines: {node: '>=12.17'} - dev: false - - /wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - dev: false - - /wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: false - - /y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - dev: false - - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true - - /yallist@5.0.0: - resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} - engines: {node: '>=18'} - dev: true - - /yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} - dev: false - - /yargs@16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} - engines: {node: '>=10'} - dependencies: - cliui: 7.0.4 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 20.2.9 - dev: false - - /yn@3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} - engines: {node: '>=6'} - dev: true - - /yocto-queue@1.2.2: - resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} - engines: {node: '>=12.20'} - dev: true - - /zod-to-json-schema@3.25.0(zod@4.1.13): - resolution: {integrity: sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==} - peerDependencies: - zod: ^3.25 || ^4 - dependencies: - zod: 4.1.13 - dev: false - - /zod@4.1.13: - resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} - - file:osgrep-core: - resolution: {directory: osgrep-core, type: directory} - name: osgrep-core - dev: false diff --git a/public/bench.png b/public/bench.png deleted file mode 100644 index 94f56bba3bae96b2f7a64dee4243d50ae858855b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 140887 zcmeFZbyQUC*FH{)fD$SojUrvr(uj0Qi<C;|&>aFIDBTSzT?5iRC?(wtosvVt5Cg;f z4(juK-t~Uh`mOi*{Qb;YXXebAbDy~HeeZqk>)LZZJXe*wjYolphK6=qL0(z|4GkZR zhK9v_^9Hb`JnW4R8rm&Mt7p%iD?EEf_uL6&Ze?qRh9>_ZK^NzRW;bc3er&X)<!2?_ zX55Dw81lHEv7?Q!C?DVwDTd-cZ8st%F=%|z`X2m|*-et2`Hh9T?IamKUQfe|ayF2L zB-D4+d;MZ<9(CosCSfs<p??D{B91DCnQR=3PVEI<B>wH3C>4d=I4?8|m$x+KIFVFf z>GtpsAFzgQ4Eoo)3w{=?vQgzJ^*=%BjxHZC7@$Sp4;}TqV^7l_g4W7b|IiT~O`04e zlq>nDtsI@2TbXD%m?yokHGwA`+<Ng-9M_ROR3GiR`iet)3R;12fM|1c8ZQ6X1)Ur5 zA(j*x-v-ge>gwtdoodh|0V0$xag%q%f2jL1%^VTU7!oJ+j>I`8O)*;XVb7{EQlj?& z!oj60e8266KsCGLi*(z8a{F4RLweH}x0j7xduj2tPZRkQ?f<+Y%8qBZdhz8uE&rP0 zi*oaQam=ycF5HiAC+{y4y|&lNiB%gUdRPA7JsTFsoqOeX=>*<-JTSgPRdzhZJ`knZ zDFx$cXU~h37$ojp>3Jg-z4nMF0Z~?YW}7RXXOx~0eTy6WO|jHdjeG4|L?TK~%k=T$ z*<qD0)Puy|Sf#W3vxTmleIJqx7S~CBK=45mk+i08V?E4n;7)mV{F&ra9jW9m;xF)% z=m%*Bup7UV@!Z96J&XMs6Xd*f5EMw_+|_XYz1<^s07w1ltMoVQs>G|E%t%LkKl_64 z+P9M`8u!bn!k9=}u?vT~c%`wqaX*S4EZeGjgg>P9ArHl3C&Jp0#M2l4fiopJG@-wM zUv^LI!CTCBy1V-Uak1o7G3<N9-%UiBZ}>~K<HUY;3^+6rXMT4HeW2s$f<b;bFLfSD zwSR9}lnC8vsai{1@yR_7T+x8=McQGZa>v&#_2u0Nl$gX1Ka1q(4_Z7U-$Gp?o~pfh z%pH1P_SS>x71=u4-5|N|CTO<cCzz)c>0R#r-!-K~mWY>be2#PQj(r6_%V+O;t6D(& zr9=i1Ro!a$DNlqZ^g_zDARM+N+luQR<d|G15xz~Jm&gOs)83G<LfPGrn7y4#I~5!r z3-3lZZOoD@5-DgTEc{`OH1WMxpGP>J#y@uC>@z_)zW&X*eK2!UYMaiDKqT0(HS#t5 zZ6yEe+b`JM4Bm(6K<}P!QG}B+4Km?u-Mk_O?GN;@vN#qxZaSWS*<f2F>z*Heo4}RI zN@{9OGL*6()|y`#Y_PuMqD!Xhj%K<*)&Ya5lw39jY*hQLuOZRjf<{|gTZi`yJno`h zh%K%7{}i$4dYeU{jW$5Lq}UORN03B)bD!fgdKTZaS9t6J)ag-d`)J~HXjnlGM7Qrq z;w9g#91MgR+{$~q`HFxU-Ru?li`x@HDzDfkaZlc2exjkm%x~3mz%@V@Y&ER&YG}nD z!kG^kycrlx#47dd7BO!-g`>0q@r~sWEczIEs@TAs2PvUMF%L&$JmtwNF!Z8qpBBX+ ze-Qt?=Y(!0XHQQUL-UjW5VzwwzZMSvQ@bHzW=yj$#FiMs=>5`6*&Q1`F1QV(rBd$M zt{W`Jcf2rnq>Voz@3(4ki}g`Cf4_epKde*Z>n-K*{&vN$P9=E79TZExE$?05G*8N< zhmJZ3?_%jX+|LYkKIlGE_Tg@!S@-~5;yB~Kd@vv4PyZaZ6H6{oFhJ$L(vL^V58{b> zaA$Bsa4i-q$7DR@9^HY5)<~<JvF8z0ei#i6`#>4${ekJjPJ36o-<=Pp@=lM=W4|a+ zX`mOrtBR~j8<TEO+<NHyTwR-6(@`5&T|t|;s85?kcPiIOYPG0`H(}6JrGkGdjY?%L zCO<YQwl;>cJE`07=ioh#j~pFIv69_+eV<Em$210>@2bxi6cxQJ@G9&q!qc)S25Zmd zdl|t$Z^`l7@#F9(PJ8edPT#4fwq_c)FK+&<Q7ABmX3bjtw&C4N6)_cGN#rM0O?MrO z;-fK(Lfryrp|Q4q;nRuoiC5OjW6TrG&?g0sdfB>hC05zpBR?$%#!7}nzf66eDw7$d zC?qLO$gh53lQe-{YHt@?6X8}apyu6jqO&tP9c6XDKeqFWqt%h+jHQL8p|CWyK6Q<1 zjcUpOO@=^*xS^e);M}9RhjVyyD-9W5$(a!}`dqr8!g=)OG_R~<mE#nI4^jqEjnIWJ zP~PIb`FN2so?@H7?)83mwEcbCWRtqt^7;K1V*8)3?%D8lSY{8CXV1SDZu5EX7?MCB z@_0Lv+j{#Im4#@(N3mnEBjiJ&Lcxb0rWxH<aYmYAqiCbHquO~)%HDmJ=6>cFmD=Vi z=7XCJE{tMHEs66|d%S`IguF?qAZ`#Z%J#KQj3qbpO<9Mp(np3io;Ifhvr4n9gp~37 z#hW_(I_@Ri6SrXOF#d`4l7*Sc=|?l{<%PAIHT>mfGo&*Ulk=6?)8{iPHN_Q;_G(oY zlk1b9a>(*q;W`mhA+09{Zqn3ZRM?S}R5KxW-+SLCBPgSoU&>i)cVTrYa`f6LS*O|* zh1SoXuih*sE~HP4dLp=-7S^Z)?iEOCq_38HAC)dr;rP==7e^Ot6AXi^kEuV%Hjl8r z5AW$8j@YXi-bobc_YJ?mI5j^lqsgRs6ZJBRMvZNdZ%}cN;5`BT`>)B4^I?l58~nYn zWT+g7a1Ub-bKd{_$@$N-?ZZtPJCRfgubGXm7Sa}~1&)>sKNkoeq~9;&YVC>&#fp+c zPegBeyMsQMe9~vTX1~@enI!4BxYT;e4w|?%(M`wB%vZoy!0|%l3Acyueh^VubYFC8 zbj$P8A*;_!Z8u*NzwT`lo@BJ;Y)qXa+a(vLZ(@+*cqsnHw>?`vMBdSK<-UvZS#pBO zu8f`>ZuVC8Q$7(dvt#7_ndEM6Ig<}xxxbQpRa=qmREZKHQwz(w2VuK1lzR1j<Xz(j zw+|!jYmXvw4iv@}_TqwKbsxD$Qggd?PAaj;vINGz*V3@o*g6@za{RWU69uE#6K^rS z?8mLS`;pNg*^-ajefdCvBJN2XK_2l)?5JC=?~=50(G%q6_Hoa#IA>Vn`-?!bR{ajx z(#4_fewNvpdC8BBf#ZIIl&QGYEbYv3wpX|l_(o<SW}qIwth*VN^cHl2LhhhUMWdJV z#Gh*(%T30DQjOymIGye#gbFixs~L&at5H3e=Y0C;?xWk-!uxV9+SW|K94H?uEB~_J z7N5g{6IS){olL1FbslFbqU1@=S*_I>-)_~~=Gy4Gvj3AHA(LKZQ*%V$nqFe%+Td0y zwXpX(RZn`l5M<CbrSZ+~jA!=|#d=y_mKf@Q>oa65z91=7NtpDEeX@$knEAzIjpf)E z*RYm^Lq)E{`uKz-U%fs<p95$N1dMcjrO+8$lS`u{$Vtz6m$R{c*8hBf<R#ljT~Q+; zbF(>CMA3*gNLTHp@8{W4pHYe)ip|u4>KGf!3B6MNn%!PR30#mBgY86zLYK412NF=b zwAJ}GqB>1kAi)k%rB{AFkvPQRsx6?ss{cVBzgB!Mv1(@)6uwYYOKHb7bLAT1Ah$kH zm%%ZfIX>PP?#@yA)Fy{ttg&marGm*)?OCbjPGYl{ckfAn?}+c<Y5oqxlWtk^=xOJ- z>1Pumtc0TkNfQrTHDQRY$}JH}4T|ZgceGaa$5x^~W9J)@CTm@+kuVx#uRy0aS04iM zmh-OSzQi@>^Qej0XO8)t9#4)!_21}E4@jpA`3wBq-gNGBR$2EQq-|XEUHT!@*?~}^ zN!ofjr%#C5y}DQuh^lGm%}k;Mdl*{T($4uU7oo;Ap@HCx=7AQ!P1&)Cle7<lAlLA{ zji!#qabJ<slVXuzckzV`-zbN&<CZn#Ax<Y@(N#Ih1e7ngeh~7MP@lF#qTwR>;;iFt zcN~}DQI<wlq6DwE&fLzlZwXV*de4#LV53o}k<Tv2xh8C|C&`VA5J|GHYqK!f0@hvD z#VjL(aQjrQl+MhoT_s=P>>bYg`WYj>?Y9j?g-5N6Gx&R*c=nKuBvl*lKm`24&fGwB zpjVRZ#Lhn<824>0WpALBxuZ#{pufFwY7y`F5t%hh)RCzj+3NrOqu7^OQM7_!)ZW+T z{k=TQx4turC-_@7;W%{f@gxJuB;_I~tb@Gy&s#LirRxYMsgV3)T-2A8n%hGDKQ7jJ zqYE1XvIbEYW&=Lq=~@v$a)I{I?1h54iV7MluznK_3!MTD8(2dJ7IAdSzt?5anb9zR zZO1@E3$a4O`m>KJ@Ok};2A1nKzdtc!-=pCG-|hg5$0v-x_QnT)!u)F;iy8P0?WyK7 z1qI+!)5OWl%--1&<ies^cLr?0b&%I{MnfZIxL(i|G#>2($Dg#)dg1ayMOoMcWXJj1 z6!gZ7)5Ff;`Z#E!9>Tz?otevPIuAQrduL$}v4_9*5C+z-H*-Ct`?ZUUjo8B%D$nVj zft<|f1UMgaK7J^UM@L5|>SSsztRXG?r#bLV?4hNLi-Ry1m%F<=r#mkv$jO3>TS!QV z>oE@(4-W^h2Zyt#y~}G44tr<%-v{~YIMQa$CQeokE><9Wy6fY<egkrK5qtRX`b2-f zeqX1Vht+@1WbgdvwtySty8eZWoAWW(-^T`;ie7IOes1MqW~(P{We4;ba1L?q#{zt! zzZ(2MKmF&F|D)-P|1{+h5_<eUoBofV{&!PtXEUc~AUoisF5>?gus@Ce=Z}9HigI1w z`~T>R-yQvHE6~&8c%oc?kD551o*O2R=b}+rNvmlApMaTNzcB6rZ-A*>e*z0S2Su^> zSri(YB$|TsQ!Nkl?KzyLvG(cKrKE@bfq?~|m;>=Nq$Q>69;%>YKlLKcIc01<HOf^e z!w4Z5iHW4U$!s9~HIR;#`HkH3kAju6?D{<2yF&V^=j#`9ptzLoN1g&W?mQs#Sfma6 z_dqm^n{;pg`I}@YmfzVDj;;zhkEw$%BfW!|bcI&XKR?cOi^V&14jGi_=vYLOX#e<) zt^@-M9FmaKL;PPf22OX=V4dc_Ye{so7%c$ydNtSSzq)WVbPQ-c&VO^uz*T=|m0a|C z;aN!akNfLa55H}7-Tm)cq6L)W5!KP?7_rLz^Rc>a0SvYGf7ep--=Y354E5ik{)e;k z-&y@%_@sYl^*?l{|2pu0I4!ST-@mc?pGD4pWA#7luita{Us(3fV8*|&?4R}5FPr=q zmi<Hj|1T{27nc1Wd*ELO{?8oJzYhGL_1C`+9E(Uk8UqS8OxW?R?JWdB5|1F4%!T#} z421KjXf1X_1$z~Do4_VURf_*ZS`^U!@Dl=#x`ch%S<O+2n;2gJ>4z%+2@mpo0KLbQ zS{94{6zKNY$ksuKVf4$=qF$(lD#EY|?<MUV>^6)3mvmzq-2W4iNkkJ+tXrnoml}L{ zwRHNkw+vFTJ$fp3NIylB(2rkp&R7}ohsFG$^|lo*jp%UFyV>p@5JmtROI^<r^>k7P zK`W$<7We$GNXHAp+hc{btrb#u%vwVzggvAC42AXejj+r7%dZM%oq9Q@c}(fvN^WBh zAU$*<eVktSH8~g!pf0m7uP#8bjCK^=E>N*p?q{;y2`i4Cmoc-k?0OZGwC}|JVlxAz zA4*;b#`wf^n!P|0v>XkHtS*&IG0b2RDoyke_RNv*s)~|Pb+XmFoW~kK{N@gCT504b z-ebY=mwly&`{Cw0`PQwy%o9+kL*b3T#v-%}19f##cImpwwL@NXWiuW{-LpEf3JG@` zfh20*m`SqM>qgy0HMppU_f{IFg9pJX`K)f!3nz;Ydcy6D-5aVlHx@p&x$h5K$mK#q zcMqpNs0D#ni^cMT=0m9eIBB}q08g0t7$P`85rD<9-*)bNm{lZ$(eOUp-7NkT>Lb7M zn67k)bW1R=7N)W>G+}KxGqg*eqthWgkwRLiovIycBAX4hR|lCE3s&k8Nz(CQSW`e4 zU}U{T5p1g)2lSB%^GPg~TCDndsbkiR<a-Z2x`@O7BD15Ju`N-Imu1>W0;47YSsjoO z$<W>_I90apjQHW<QQ14u&<9I4^G$)I&8a;nV)`IPj}jFMJ{co+?QitK+E@?SfL^bd zr@H-jpG9FvTpkBq=J$+FYua2{D{W3x<O@g$kaLZ3<Wi>y51M$I^>5$)S~vm9dYFLM z6Ku?Rmg~*fB;?T$&!#ue+uaPt_@3og|8}yuV4==Ub8|ee%sb1nq`jt0|8{gDuN-Ih zk)=_0%nq?<&AxMI@7B~8M_k<9<gJbci(;|6up>qXu?d&$&|kJG8F-h@LF}8aS^8rP zT;o?e6XPRRLojsXA9=MkJ=ug3qz;~V?`7#~BF-nN#3ox^8*v2J4s-`R^baMBT4#UW zMaNpE+wUf$RVs^_lkvULSb*b$6d{OZT`fAeqfzb=|A*QQz~TaW;kAFTBRK=RG5B(% z(bw3M)p+S$1W{fklY^LYxfZ1b`ZUCmF$d!>G)Jd?^9eQ>qSf$4nd;13zHA(f(u7x2 z`L27&N0P}edZjFL{WyMI$sSc}H%MwVRh(j$net-(rdO6N7DUb&*UZ@cRHr$|v6Qio zU~NCRMZ7$tWT9r_bG&$3t84RZq2E*dkpRG*-<=Wcfzw5!VKor!n1*ZB@GSD~pb&J0 z#-&eNS6S{;1Uwg5%NSs$3V;<`fa84XGaZEa&H=H3&S8H8y>O-`?-|Sbwp+xsQb=b; z2EGQyUn4CqjZx))cQ>j2bcn`kvd}1<tJOZf;fZeMTQ;E6(fvI?oQ+wb7?-({l%D;S zUIyNi-wJ?zgtbl+a#)Pn%{8Xjdb)>|TE4+I&?PH2dQLm)oo;i_Z6-|ZOUi<e`%6Wr zAE&CW+M-wb*(CX|69-^10E~VhqTz`i)}5ACI1HNG8s=%2PjJ-S6Dy<CTcYml8+Ui^ zWEH{{min^axqfG2=8??dy-WpS{iwkJm>cyh<Ip#F2*Wc(_!?u@O*$g{fI(Fs*6NqK z_*)CMjcxl6V>1Xm|G3ELNi0j$7G(Hr;tDP{nw>=xW8H6jpb`l`5y5x6Pdy{_rrH0^ z>@e6+!fNQA=U1)Zc8@6oO;OTMNXjS(mFj<HMIfMnJj4v2a)D{XX$*GP7dU^6lHd#b z(SP7}Ff=@F)2r-#f4(PQq^}`F@u(U@<a8(6e2%hXvjW#3>MpGa{V(My`V+wHs696( zV+xb}PWC)Ql7upm5Fr)0cgCYGfTm5?7VFB*V|HaK<bG9>r3AZSm#aAn_S=k+4+U=A zn_xFlHp6(72k-<kop9>1ekM$GtaNO!VIwO^xPKW$7@q6AM5k!0SEpGa5ib@h#{BUw znd*@;V7>DT`abq3ugpye-K!X<vcwo(3&??T<Y>dzq)ugEVbZ9c{Y|2}fefN7pwBw< zK|#*fk?5C~TMhwkUr_eGv6yoyl<ZEW`R4+0(|m@nH^9ZP-Ql8IzS5c54|LtDgL*ug z@0*3Bz2`zEXk!{jEQZ$N0{&ctf)WsAa^Sws)i<RLU(Htt@tBUZej21VKh*FPM)5w~ zBU^BHI4m%zDGJ}Ykk#U(j}3sed4<QOl2~KAX5d6_C#8g0`~V|GW-rY8FF$Gd7B=ve z&MA-NDXgnnV54INlD(*TU3g3}%<+j@n{EIIn>Vy-ln=;E5#@U53Asdnd(3F;^l9$~ zn`~rp+y`aEY;L9L#?No))a=O%-!jr%6LiX|Zx_txJP!q~cAtYO-6jW_=Fq}6aPO~q z6B>Wr8957j<#WkTw}}P46U-BSPFl##%`_ymQ`LdNo_jr~t1aF<Fir`Dyr^_xW%EvD zsF6qj>?H-y@1P+cdmZe%%QC`b>AWx^8u_B#3AgbOSkwI0T+|fq1YVG{mS-`F*sI^U z$g1+m`&;3J5?7Kf=et!Wvwh*<XTmAPep%#uJ<MX$Pav+wO%AyAB5iYi7dWw>WOp=C zN{ON=Ir5ngqYaq>rIG+cx6CL3rR6()vXHMoI>#D^qn41L$-M2QE*<gwbbPbSw6#v$ zX=CHT41Gy@%ge)cw@k&6nmasyhh`5>$qVQO{>Q-`<0IwkaQU%C;|1rH>#*|Sg%%_z z|5nC>;6pJ6ARs$d%Sk+OlNY|d{Yoe<RWHD6*sE*Aok6d{)LbL>stVsNna}cL(~Bah z*psDYdM3c@I!#dF{?)Vc@<6vNoYhS8*)mb5xj5RTi~4HxOt6s*L6eCvpdqfbEGZ~j zAHSVxh|^UP?a!G?3+da^uTBjl(OoP@6sGyNiK1mRSfm%dG|uztRe#&A6g}W?EU5|s z6Tu*=yF4}PzVI19Q$&z;C$T}eOKVQGOs{h9=qS?B4Bs*!)H;e;CbSA89WZ`Ds%G^0 z*Bo2UZ=HYUo%1^1YdTKzn=p4=qC#fZAtK@YXgNy*1#hUKx92Mwm-edNS+C{#8=OhL zA-<W@oZqN34G~V+&j(G{Kb?l#l!`?osGD4R6t8g*gon)UTmkTr12xXx=T+kxbc9?E zR}NR{2VZQ7C7$sHw7O~r4$9vgx{7$m)AcuYzms`wfGxcI@>4Cfg!MNF)1I8(PI+SC zBWY|db<<#>!Xe+(pSf`XUXU~mt14y5@;UfwoSCw(vH6k5aWM_>n(u)F%-;CpBZ<O+ zZ{IA{Nr7njs2tViEBn}l+KyV=xP-G<U^6IvCoc-I-Xn21|G9a(p>j4!#>spzxBs;7 zIVddaYB$~@7V32+dVcMXjjUQIw>Y-NaNH^B6a81bc=aO;{HD*0d*WGEKjTy=O!Vok z4jgZqjm&;%Y4oyV8gGUBqma68I}4j;#C@G@dbEkQov8H0Ai0I4Ff!C3+sQ^-R5xY{ zZS?3uW~>$dLi~y`P0|ADY>uDAXtknDzJn*BNw#~Y9u!it(4s0XaZt!6ztk;?oilV- zO(8?kl|klXydNBPXDlchQ;y(AOPlv<d*R(qOQ=M>Y?EplW9h83J7c||CMk=?r_g(B zXSCg^t4|I2)zwhQ!G+bfDDk1pF3nE-+qDZ`jcmrq9k8E8Zt>-H9<Vp)3o@9&)Sv@B z=uSGq?M~x0P6zX+=QW<}#rmU~!Og10;psw7iizHPr@ajTX7%x1%ei#8!f`h$e3Lr| zi!s{dEy`!tBg+;mp)h<FQSn)StgWd><(U4Fm-q%B?8y;Q@Nm4atWTwEaK^q-1yACj zN;h@=Jzk=O%kih{K93A>`Vn*rGp;ybwY4q48QG)tQ=1pOYIHs6a(3Fycm5LTxK8b^ zm=}IpcXgZVWUNvnXOY(lO6`8W3!c+K<)vS)<p?aW(rXuK9n!A6(CM9`g}9HO4oaZv zDro&4H=6QZQLeb8chP!^F0!y^*zWbal>PMMiyGgIdH)-n#R6e6?ePr6?xH5Tzfr(u zZ!WX+LOxEozv^5G%9S`z?`?Sm)3u`mAlJxR8o9MVwpz=j^a-_>n)uan!KKGiQ#xw1 z$)Mh=1MaPb%+$$%b>NYb!N1qx6-2?DI`3=mw)&s;*3%AjyRQ%Yn4%qM$TTT1nRkQP zbuzo?{cLI6i|L9;(#>LduBFvDWq<5Z?1oHW)BV{<+mcYL(-#NCx8Y2uBVBz82nrss zvlht^mRKQ%Q)%!7C@tS|wL;C8e?}HV<b0ehQHYP_f2Bji_vjpuc~{?Ym40qAh5c|I z51Sg`b8{w)d3WvH+cvRv;Zhd}&-p>ZbcDI6Zy$W}@`k`^^@Zr`<J#uaC8P7j>}O6_ z-w6*#2g#)2GDYH?<FcOY7bb_cOX~)e-MOj>(_mKjOzZVMv1717vsZt{T7892+ncGw zUDClvWpF`+TKvq&+y?DQ&lyU#!n8ZrbU0Q=&(BKXGfL3FHz9w($L?6cf5)Qe&aSe5 zKJ+a<tG#)DjkfIU_as@Wb~3Q<;m<y{sP!LqeI>{oQO)uKGqC{1qGSsGuv1_LNK?ar zs1kkwmov%RtCB5|)u~_D&>VCEk$Sdx=&fW%%%P=ZuHWI*95gA9ydG$xI>0{!=&RoW z`~&l2^G@cJnpQ;r%9r4e3eCC{@|4+eFKYjS|2*EkyY4xzS!;=&tlY`OuvAwNyU3{> zbOJsQNG*%zOwj8?vPgDxBIS8h`k^MfL~qC7q`pGdqSD=Dr@IOJ=Xeji^kY|Al|y(P zSa{Ppu}Xm8=XVkQork5A8$s6n!mI-9Tv_Db)>?X_?b}vyP2y3C5ypr8PIl0;`R54t z`x~iuT3|y173RIHy-sEyA#^JjDo*@hcKl0;gQzh7A5E*yrr)<6-8z~M$HHiuXW((i zwfU!tQG}HEx)27;PWX6c&$JMwVE}k*H8PXFK}7tg7d!!);~WNd>augUgdO*XzAKz- z-t!h;JDF?&g?c@Z;eeJIC&?6({m^GyuMeE)=UH1%+ROZ?Nc7%o|Jd|gv<CIwE*TZM z(<@mNxjg=H+OL)^zf6F}dX~yg1G@nxTjqceG(b`pLoDV-Q1GW3ke8T-H6n&$TIo;R z15ZHHhC?PY*j)T2Q_P=|wAMJ&IG!;H;jnsYah;m=uzTw^@2P66Ke6p0(tX~%r<Gus z>YkJc*H;QNQZc*8EZz0>(e3kZ^BFCz8OOi1C39nL1g!48IaF84ndb64K*pA!?HFH_ ziHq0xqpsFL-I{CMGj|v2U7~ivs<%33XLD(*OP(RY#oC3DD_YBPQM8Gi&xG}Bh}qc$ z?r2lim<LbC0%U<9v0Vd#LyXs4_BkX#+ic6r2sEKkbbp7kn5?GE5W*;=WD2Dbyhon3 zC;ZxT`}r8PC3TfQT%*b#G(B|HZ1uWU_5!LJBKjx23az>haUC{V1uWX(e%%e7%s&G- zam5^vO$QjB!@8Lj&9j5<SuPRMAkvRow9%fT#cKSPGs&CTKli^9mR2A~w1_T#WUgS~ z-Pgse=ZDSzD2sf~=u^F3W#@W5i)MWja;-_B1dPuwH}CMcZ|ip|5%_oScRyn=@0Xg{ zdj|I;+~E5m_A0~fb(HeBT#Lut!%d02D7hl!R;3z=`)$V01Kg7gvw7b#30Hriq~FLH z0Qtms%kdSrN1D>c3SKCE)MCvM%W6n~b${Y~#&M**Z*=8U&vDe(eyT(LShl2PaJK4F zOXB-c^P<FRt=^L;?uyCODN4ZQtMMb8LeNX?g2bBL_NP3a*tn%Uc5n)$te@cY(2cmP z@u={-=SvIJrTg48Fs_B$K{wWS;|}#cq%<jCT^taT^V`*_MV{tqK40(3qK#uTq^z5# zJDDH#R1#CZKW>vA?~#||ymT}1X3okLbiqFXYE(wKuU80Z#=c96bFYbpMYGY{*!{J6 z7qXsqP!ak%Lpq|pQ2z@_9a#22=jnO_gUuR_*?NUogRMIvwD6f(A~9OF{(Cpt2i2_e z>A~4P$n^+fD5Q#==0hu-BrQ=P>a`(9)LwkDggZL9-6h9M6_(9<kOA#WPJM{9SH$+N zUM{qOyl(fHCy65FXumTG1MjeKs-b8Hb6_XTz0v&6fiSVaAGW2H{~~-hS9BWk@Zc*F zcFdDAcZ4>m-{OlBsBrA^g7S;&*VWl*3G=fhQGA`XSEtV^w1Qf6D%Y~*^=o2c+}aU- zxR~-@>yuy718TETRhR|UH0HT!!JgA}xt<@_+1)xnOvUCpm5Zbkt{vkn|0jHDnE|jW ze}4H#zcaYElI>WrZx$O6Vs5*adP*xF2X$lNGZTo_%@c)<0GRdLJh`miHTULB+&%E* zI)ATI4&tro;5P$ZUszBlVYCd0>v3ElLW_&(*mee5LpxEp$5Mc4jW>f0W(vMqkU!QY zE!28Wne-{}dwx_h1BVwM2|t=4T^K@;&U|F#ZI)ai@q%~c_7;T|03&^Se}P^BOWOTK zP|Rbvm|V>-Tg15LOdc~SK6oohh3$tpXY$Oe5NGz?981t(-qQpVJD19~>V%wbH<b%# z3<QZxM-mB?bTOM?RlL+`Y#4QGsx?eKwcMURG2FBXub6ASZ#Uzvu1ThCWyC%46lU(r zn(Q!_u7Zv#7d#DH%QxJ&m%44BOMT^r-qCZBJ3?ak?WXDpYn{(t4<^70)}E30qW1j? zp`UAvy~=STtodW+?Jlqz^(T8ou)%{=LV`M=pJ^b^nGBCdDm%Nrb3#R}@Q-G!3jGt~ z*W6db8}P9uHn<pvI6@ebCszB1A*en#?_919%h}s7TFYBlw;88YfDmBLZUP8C>#Jqz ziW2kg-D*8`E?RA7WJQg%0nN)BJh8##b$+WoB%U=diFbmSTmz3fIr#Dd*ui4D365*q z<Sg1pdUh%6ALHYNOxA3Ux_`rMx<b66tJ~XYQJ|d#_zbiMhI!2EUZ|ckw&$5I<BPJs z&bw4)=FXW^Goth6D6Hk0oXBpyl2K_!9v&swDHSb*m0nbpXV?wYU}P!74O`%yQf1@` zHw_>G(twebHQWWD$7S~CDZVV^$kjMnO!%K{nV3ZWV*sq$7|aH%DZ{OUG|C4&bUNSj zXTNB?<F)8T7gVz2e}UN@&mDKs5)OF4i?=9a^&Jq2Ws?nxV4>twjds>a;6d2x*KHw= z(=c-&BicYkXc^~fE+X0p&68?%o}6NO&p)y@?_ham;l-#2?qUd?t(1a;#uveDR6$UB zvn@~il3zxT#R()(i#qYrXaP@d77#aGXECKqqtwRr7~UDO8*`+UN)=|llNFhBdwWh1 z26ib{j}lp~0+j`2(JS>4;ae<96CIXeFCwPFvflo=Rvn-64kl<?fBXOvAa#1D4)c$o zMz6L63$muP=a|rEcDZ?DhZ!|nhYLxo=fVut^ki7x1Ug+eYa*W~xZ@f4Kbpy&j;YQV zcs=ro<?u}*USh{XJ8Snn{mf4>1sH|{$g`mEOmQjQ_6(ZRR@ZzS8|{cqIypE6Ok8cQ zN2F0eDXoRWW$Fu!C1+|~Uj{9(4}VXiup2WeNqHr|dD3j$0Zs2#x^1bO2K6ay&E6$r z1t2(IkT~Y21i)BdFQ+?phGyKI^=x6YJ`P_Jlf7W{wksYMms6!)OIWM_e3e#oHPiD~ zsqs9(uQiKp*DPyZxX+dhUo$&9n*yaX23}QLrTtNM?^9{&-Db9GDrd3uYxfF3Ef;F3 zf=?OyXx7uH^6kvB!~23;;gol({H;($A^|65=UhQZrX6E%TBkpmIb8*Y0zf~(DRUIk zR7p#c$%sxujj|1S_g?nrr43<1AtI+4@dfJ2uuq7f$QI{WHi=kY$xIx#)Y2c7C8G_g zuRYGkv=bfihVcy4@Q{$0?cyqc$K&F90+-p^{Va1g>AsyYGJHP2Ma-;SHhyS_#-%~1 zx8A->K6Ps#;5T~k1_;U>-ybq)0YtdRG*gC&VB<G>Y3m!!s`Oj8ws^F*bifvRgX6`z zxzB@{Y!@&jYT%j*X^a`GS4EoDi(%(g7cg0GH|uvrs!25w)4gqkqU^fi&Y~xq>f|D$ z;x8UY$2f3sJ;eLq_O0=n8jvcHy~wJ0^)Wn!d*pyplTdslM|EXVzAyFs=~R<LS`L7% zzYaK%Ip7&Y^&X7#9qXI8z0@JL0^-@iAy$MmzfIdBF4A?%XcM?)S|Cam2oAX_0U3f~ zZK9a#)b8<h=GkBeuQVv|b^`+3?jf+BZgwvY&s+vVUyF-Xh{G)asfubYFVaT}=E!zX z@L26i7u%Nc4FbkCJTeZaGH`D{S-91>CoR@AU5ogXBOfyYqni9HcPE~P;8r4MoTYX3 zT9($r5?W3x5VuSG@I$=xsy%~UD5r&T#*31C$CV&Dz1K9|cakiZOFR9?#t-Ue&iYRW z6N)=~Yb|61MKXoor}Fq2EZ`bZ&tQS|H*u0=F9e^IO~&UTZAt+sH4}VNR*R$mhejp| z$hlt!o_4z(KILRnRdQzt>f<rW?YUW9kr?E{a12~~%xCSlr#+h>Zy}iq!XKjhApc9o zL2a(B$xdB}l{T6O9nxSc_}v!Q1RvqjRk+S?e`0E!Yq;2O(Hq(EaSwl{MVoJTL~>rN zyjM_YVy0-Wfy2BOT7r|AATu&{RQLb*k{5WF7kiLH<yEQa))^ouC}zJ@F{}m<=pJ~{ zaqe?^9{FD~Q*C44eQr5LRDQVG)$mG4?n`{%X9lMe=3N0hW^*<CiLOPheeffJFvQY! z{gY<(mxZ<@*`w}F>zoH}bDSQ6H36`#H#ijEn)6%Erw<#rj1Z%4d=089jq<P?2$uPN zMK%HUvK=3P=Y1DX9uT^x&8k64i$hU;*uwg?P@^@>Q(?V=XGhntcV3{10z`@x-K3OH zyGiI7H+p-`Se1I>1p}qDiMCn=5mTy16S<y(R(g;KJ(V$W%-aTK0G}D}4Q`tCT+fiD zYd@_x9Wv6ZwhCVHEK9A|$^4P9P;9`bJbsHf%O743^}M`<VW0mDi)tPb6&wpH$?l5s zV6=i}lhn=o+ZyW~pXg5?;T?-#92cvtuF9@f!wW)8aVBWo)@QI4)_Gg@#yd~J`dUO0 z2=56Fq%rLycrpKe?|h3zan*+F3Pi#$MK%!co;u+M-Wto#RC`tA>2_O`M093b<J3UK zSJNImK=gPGVIB93Oy`;9LYAxN@4o)*)WBfI+KU5eA@Kz0-X?(6?j7)*@eQl5wYZhV zk(=c^-@H@5Y&!i}fyoM59<<K+d!*^!zI_1DdrLL`<Lr*sRX%e(IWrFGn#3VaxH&;W zfQpoB(@O)MnzC0(QdRYH)*uC^GxZ8`){2qLl^D#N*v2F_r&^tA4;sFAp?p9-VsXss zyp)t>df`1|x{>qL2x6$+=s#a4lgK4akos7%MPLBx{`CS`+*739!@CgI;VuZ-(aT;} z{P>gQa)drb@>j53utbeqMf`+_P~rcf93Rq0-@}9|k+7K@=TahXRA*k324919p6;$( zsKs8cI(b(U_h;O3T0{)1MaduSs#JZwy+vWur1k9mDV2#47{)u|E4lUQ855zv8~S73 zf#SWY1sEoPk4wPLO>R3yCG!=EH=<}?oi`Us6=fmjNG=$VJDY|V$*8>{IUGBSB4>(6 zir51A4Ga-_84LMace8|?B6WWSYZqfPI>2)EwBn{!BK$_E4%a&*Ru_kn=(o#rVKeiH zU6GNZ(|$cfQ_D{jsNm{334I*$+qTorG=H`P5>nxHq*(lAw+a`NNhx@QtFg8}K|U?D zYZZ{h0EzJl;I)H}zVKn(Y?oSWFD%P3@P)l{)Sd&f4{78Xe-djyYxmc|$ka5ytgIc; zC4x10zt-*UU($&isCamKykJMC_nnZ5oxcOh{fNnFrnL5a=Vg((quq+{<y!@fugVM% zU#;&OoAg!a!#&UHAH8ER0Hhn}E?;ap=u;h-6L1^r>}W;k#`_Ggm`iO7#2WU@>^N+z z*tk02c<;#%L-0)bKn?oz9{c12p^8<9Btcy*BIOWtJF}HYxOwctI-md{F>YPr1q(fh zj=ad#)K&c4q;{Us4MsNRSP7!gOa=o>DXS8IS?<NC;KQv=Xtz~Wxa4ZDO39jxJaM@~ z)%1Crd>#eA?%orv%P!%jdAUituxM#0l;3Jmy`bx@+UOqTve=NCyAs@xTZQr~{sgaf z7NDqf0mzN)`a@`Ln8wLC&-5#&d`a(0nxo1qn5!u&<f2cdOAW3uhN(vreXqMI$fZcT z-~pF}@l-Qpl%26?n-W<n=R|AQcawWAnOv&2sqsygY5l36p}^)JN393wG`O`$7~h>Z zFROY$BYx0an_<-AU+5x3K4Z~zEwuY}8UEHe`nl$mpF_xNIi^<0CdJsy-xnk~xQ1z_ z9flmO=Y%*Kj6W##<1KqEn|!p>H!*IP2RZ#_WHof4?L4}t89`bm8=-a+f5&(lvL&92 zEcPFsJ>5$_DJ#1<^A{~_j16QuUc*uyrr*vw4%eC#4m)l4KG`npS*`u3*C={nH4MMY z^bP<}tvYMoyp5bY4cxpJgzNtCnhSo^LugtJ=y5(7V{Sl7l_h*sEoxkwGycHPO6MEg z{B?d3NPi5(VVLGF7>08!<h1oTg`C$=|6~Y3@1%>kHSJRz-ae0_jA&8JZ*_&cbLR~{ zfxb1$T<HFmjT4?ygOpo7Wk1T~);wx<Ux1}N(ghoE-#BJ_bMY{At4u2haV~C*_z_B8 zWZh11&bhU~%Fp|z#X#-MT!Bv+sTzoN%`pVL1*AB_&<|1ozeN@hT)ftcW|CuBEYtke z+hav8_F+eadagG4IFZinH*;U`J+xP~UzN@2FK(&y`+{f>VRhOU)7UCoffs(~NHP|K zC57t7)i3yxI>CZ_(~kf?v}#tZ9?*j8%#3`L#>AqZuahScLwCTuvjH&7aJnNe+4B1O z4Wm5Em$miBmg6(*j19x5Hrgc|&l;m#5;Oz$g_ICDxoHVcBHs8++)rupsQzN^2vOtL z@TreQ#N;F0Zaxdt>}nq67sl=rztD0#xV06jmKOl{XLalwt`wY8v^~4feZn@~J6Z|W zSWa-p276qyMZ|ieY8?t$dm%W<LNv<)6ilFh*?k@2Ik&Y`;<f)-e}1Z3Z~EBob8jLN z<o_unbHF`clJ;d0Sy@qhu{G9c{!!_7Cw-*dz2d{FowiR=HB?D1R?QaJ_2#0VI8GE_ z@AzF4ZlfWaa{J|?<ad5!RQZPh2riuw#{G=cFYREf$rxR_Y2d!a?gJV}uNNHkorYuz z+ZFeI&JL+M-;(K@jaobCao>`!NnPqG#xPkyz+n}yyM$RRo5FoizTY5y$?+IaE7V7T zGCClWzBql4xnsQGuRbOFTd52<W4-q0{Vn(NQ`)p8PC_WfV}EwX^_noy)Li1-m0E0A z{eTngVVu`j!Ec_w+A62h7y=XAPiNpYn@oFFLa<4}?+d`bnxKqAy^;Bs+)yWf_?L!; z&R3-Q>$kt%_?4tv9%~DWg$V;V)=na4)HXP5!GJS3%VQc=qt2@bwbt%u>vw*3yQ$oB zNuW37QmdW;_9kbrAQNmj_9;;8<!KC%!Zy^DUK#T#mktE+{o=bU0C$`b!3-#+j`zy( z1GO}ttC0(j!fvWkCj&BAjKJ*~&z+TeZnLXL<bp~l(z70NkkVBbzQ^vvKN|Z0a88qn zg7ZX)w`%_0iald2OL}8AzJoo;>CIAi2t+B>rJ0;+r#k!VlF*&6AS0h?)$jmVi)O3f z_6xNWzw)lAxl_9%KNBydJ_?k2g%~C@%J(Ej%W6%+EJ3e+?-rJM!+Jhvv{6E#ar5Wn zy~jh99_`Q!45+K-L>kl9u~euRU7oq5d?I^_P12buh5TZJm!_}Q9(N+wQ1Xr8OIsT( z6Oce)g{zW0CKRCUM5Kis3{^4;@}7@zG;D779a>vAG4*=mpr?3lIUg=sX|((d%T$<# zOVYdqXVvOCt$G;fhj5x~>BpIE_YF7Leod9#TO2-(P`_(!b($jP)pAW}31r>+O&-Qp zpzSQMCmQ=pS)wLv4YdhqFNO{;wciwALe(<GS<GMwBQ%5drhLx%9E}Sx6TZiq9H&-L zV~Pn|TT*x50eu0v_sSI@-XfNT@xA8DdOZ0Fc0CR`pVK0~AGh<@#XnfJyG}!@{QD)M z2CYyH%$BHBcPyX|;g`Zsv~cY-8`hOa-MHL~-SVtKMh!j+ht1KwhsDZ$0kF~c{qgHz zN!3mE#qAEr8}<{;U$<v#=eDBn{6Ldd9kd>M_4d@XsjI@TxcxcKU*<NA4Nl`Kd*KUV zRq1$I;Bj+~2fW}>cq~Oywr$n9Jzx^9m+9JDFmIA;eZJLTtBa`9%QmdM()L;+afejc zhgc?W4Fv!1Z0!kNL7_hl7c#VlbRnix`<qe{iie^evDg%Nj@MnmQe5RK+>AZnqK;#= zP!|WiQq5j2tnJjyYZ78@s;@K*GDx`Q8Z!rs+u3*FR_=Wc5n8*h7!Ri9djen_u-X{T z{>FL+Ok4xw2EVUBTG-Ycwbv8N!uIn-A9Sj6GEF07R@dbHDoHoB{<Z3gg}vO$D2v0K z^_axMQI!o}aRsXbvPK`|yhEC$3|N`=5V9tD4n3-U2X^PfPC(}^a{TZV9v7to4Cwf+ z``7rFmT@Ei)<Aw|47V`m5hG#Bmbl=YV!w{|>asBzN2Q1$jJ4Fr#wKMwZR2>T!ZoO7 zq|i!tVGd(j{B%Xn1h2)S%B+;e=!AAgj<mVC)rpor_Qx*Co{;$cirEzD-$r$(9x$qR zb60k=xdAm9x%P9w@LWr}WAfS!GAf?4m)bDB=|10da~ch<{>Yt+_j<GN%`x)>)=SiF z_{9?_eUws5x|;kqCOn1tu0w^?F{_e~H{uh2d9#r#ZU1`!5q>o)d2yPDZS$8cL4Mhi z*A*bkU4XgF52j^C++DbOxw@$y4cOBawNunx8N|f$ScJC__T#Ywl+saBZ|P2Ukw!}@ zUum}guwlk}%3jMgkk-qnVC%O&wzYdrPd^p_yQaV?d=uP!_dBvUA8cX!%vIQ+IIX5t zBp$D9h_8gFWH-y{x>MweMXxh99DP-cdS5v18U0K3TV$79Y^Wq=06-)GhAQ<JYnKfJ zq05E#8vJeTd}ko3tY4`Ssj@IXC9PYZSvJQ8Xgfxraz#L}S-iitv-ysgz&1s21M$e% zn2vUuZlYSt*<^NJTCoaDAc>~*7*ssZPLAXUz$w0gyI1iO@InDaj9x?^RX@YM09aW~ z84K`~vLT~R?_z=<%_+Oit6fZ*`Tj>^L0^WOyR6FC#7&hm1=TwTX^O>2;1%z^tlF%E zu2z{e3J;W~-+0pWvRS`Jcm4q2I(tU!BcEl&RDq7>=3EU(a{P%{)ohJNt*JMizND)j ziIW$zi|J9<E}8k5e->WL@w=1P7En^L9GPYvosb8=%Yp(PaV;L}N1uQ$?NK#*3$g;c zFUp#ud@a^SNN+th8BWC>@mHp`+mw{0My)K<YJBoBvZL0a-cseov0^*wE>QoRS7meD zaE<@6sMb|5pv@7MC@*hmr?3-{WtA1`eP_(eKcqLEAMPV~D?H1;kxosQed9FO6exh% zet`hn=$IOkS8KkxWdJg0EG$S_8qHIHt-nbkiWL8311(b+xHGjwK)K$u*=AlZYfo79 z<v&iPlfeNQWD=6&W`o?=&<TLKPU|E+lOwbOWfKbeJ=z4RmGu(}&{F^s2mSw&I9`(^ z^ML}Dlc^DO{p4*HN}Cp0(9A|noc+v%3x*>x!Mw*P*ochh7h8Kf!`}STL=4YM%C$9u zx%aM#+G#q;Ku$rO>Jq!=s;{`ERcPqB&Xu0@Iu56|A!4g?`>$sK*x6k6q~fmND8~Y1 za7x+%BCZv_hyj&Tre>{tWBzVO1Ww(-W{Xo%_yn*E)og0}Uqw$YD*?(rJg#^;c&200 zl+&T69C~E*zOwK!nqs{<(vju_I5TCYNVC5OWDNFvhPT_M*>re>$7amH{bdd&6rcgP ztl#xGaC~dZSv(#(JThc@B!h?2lfV!=Z4Nn+Z&QU=b%+Ih5x+dcEFACg=EEAK4$7by zaW<a#gLSq6tRr*B>7sT$6bvZ3JUez2TV!W>eOZrI%p-<rxS|WYxKO@&hvHe9`XPJ0 zU(CM=nn%S5uB-3}t{jt7k^wf*ZT0_a1Fmqps@*x|nx@7MSlB_joj@TLxLEIpES$!M zl4~*eXC*N(@(WjIDtP2UM8VGF!!}h~{$I^oLvFhMrfg)5&461qF3?wO5qH+`rlp=i z=nZiNS+eo+e0go{F8=eubsXEkzG(oRi3W;?9uSpNo{MshLv)&Bj$w}_^*hugP>%++ zICJ_>Z7p|ZYo7rUuc_X|QK@4uhV1gS+@zbTGiKH}3Q}<|=Kiun?F0xV?y2J+@kHkz zgTiJrmh6kSJ(6S>y&M8y$j;-$WPbd%6S1O;?G2HxHXVOD61j&Ch7qh~vPrs1*Mn(5 zJ9B!w(4fYeE?k|h(sxkDYPh)Y<m|dUCEIIL&Q#ESw#g>DYNwbsrBSD2ny2bQzOoJg z7|mM!%T8<Cg1ajrUaPC?Y`v67rW;zNbI+6p3xzO?`o2U{c&?mJGgL!io?sL6aG<_q zzN$dxC!k@NEla{gd#UB6f1ygc54Zr_JP&4SKiLCvUb7Jc<X-J7*?TQ4pN%Uo=5kN) zWELB?3>GLVNB16$?X{WDQS>(AxSg+dFg|r%)a59nb?mKG1;$n;{TjP^c?bWR;(TH= z@v8hISFU#+(*9v1mcszm^P#>Qgue8G?-Zfv#_p+0(}a4U`%;R~K)13gUnOn;h8XKa zo|+{B#U;J;gklec!ck`ak3({l$uGVIe?Oo++m2dLI<S^O4v59$0_jBbOA;8Cff)dP z;-pX@J_IX0b={TixP|SCi4=7m&F#^*D0${z)wYX3VP*pv4keHgDFzt(p=9uMJ$T}^ z8(<pft0GA|jv)O~KG^&Vx7`{0gjEtBK?f;+z4AAiX7$iCm&BUeGx}?uj77sA#+g+< zzerTpd!V+;M)k4ip#~06U4#veN)<3LXHO-q*<%E37Np;#Um+IVzj{AQ`R=ntM2s4N zQbeM5)#-+_jABnZ_Yt7*ydUi&+o7mVB8U)4>!7EpGpbkk#f{B%5yBOHw(4}D0^SG8 z<l<(bCp=MmXBrxFv5MQW0B7v?^&W?ovOW&^pQPFhNUFbHoU8ye{OG{#5wNk%PH+L) z^YO3()n51YmN+)uvbq_zhC^PHys)jYagNV74Y-oaqS@0bdJc(_I8hAFiG8H}2ruSD zpl<%d)-j>!1BW^NZWf0OfMeBVuC+wHmyH9Q_OBYVUoLw&>3Sfo6S+u!dL5L7fw{VB zBT@iDUd+OsV114(vK797x-`UVN#;t=x4R`yG6&Qe4qfNFJ@PnUk%+f6Tdi=*<o9sG znCqu~1V{rx*Xtpm9rIy*;V6gCx#wtu_G-(Yd8&#e?Z%Yo(TwSAj%{8}4crCSX79`& z&i<BUUU=W)UD+Bh9%+(`dlIE1P~J=AS?%OoIgNFo@N4>wlBp&PkIGp>^!cTnQ?;zB z&Q-~4XJcO~l5%Wnv6q~GzVFzqSgGi=4LpYJXcl#X6U$=4sO#s$qjD<%X3>I@RUBsp zSL@Qh-P5Z#j%85F<H+WDuS~&wl-NI<NIqm$K305~Dk3pL4;I|IOx+f$oALEoNGN<} zamI*U?Yk#-+I_eF5T{D*3C&9c&Dmw}k;ZXCzE9lTjB{6u*`*--foF><u6yeGN3Ykm z7qN5KWd?($f1Iu4A`0-fK@gH<{fkrJ&kfOJ#@<i8A6R!X@gG)?zERBun`>UEEv*AJ zJXwiSo>@*2BWvgS)>H3uM{L^rPjW!bnY<n6Ybi4lMrZ0Do4@!UH2*0UHFxY))9wP2 z6;sqwv(w@a^j@BTM<@1BHHvZDJM!wl*r$B}243zFF!1w3c}SQqbodH>^6EFI7?1-z zu>=tTns*+vS&%fxfMQ<2ZpzUP0fo-1U+*C9-+;gITsJqw6nUyrXb>f8PZ_q<s9zxW zOMn^PC=Pd7^lFQQo9^Kt&{-DrT_tz>U9$Fe?!k5}lRFWnQ^bI)kYB#89zCFZ+bc(x zj)A$b!Gy2VPdM`cpd=aijf!$hQ6sBLv3tP=)0Z6}&ZJxJrtwa2CH=&13oI&dX8i=u zAnU<&b@5>$x6{Q?cTj1C-RKzQ)()j>yahfiNXCvW^bI3WeSvvfly=_3kD;1%LSSBz zc2n`?SF4oc`+5G)7lp9vEU0m<=2#q%J9;n%km`jx+V(D+*}*~0AcM*lCHmDtHy<|^ zn9VrfYHVU)in*S)Kl|fL&aHqHyx5lgUhH0<gbk6{V1Ne1LkN|hl-Gc-Yq+-}eE%+i zDcc4=6Y1XSXKNnmyUp+X&0o-e`F=VmF$!h@n9Iuy%|GnFj-Cd@8WZZ0gxPgk*+<Ao z)Lnt`7x(ZJun<gC;EK(~X-@*%p~t42Em!;IsvG}D&lWP8TA-?>vjnJWIkp~!CP026 z;QkB2qV>oY-g};FDFCjJfgpNpc*>m@2br(x52JkimU+(+eYh+BOfuL<sMEOY8cpG} z^*eo`y$aXf0Hl3H`k^-KTChXRr&sy=%1r(yCg`ky|A_dlc%N>c3Rf*buB-6sK{qj= zbN6c4mbNe}@Fh8oE1HyPMCVSLq%z2X+5g)eC^lZ?osBX`-*(B>7YFKUt!u&H-o#<w zPK5-g@utJZ+c5TGfzo}&quJ-&BG)7YZ#M`D=(C5l+?di0RkeuKb4Ka5u~Vww*Kt!B zD8X(`XAVZg_zJ}JZUgOnVY?kt=V@-kl<P`{10>UCh2f@Hh<nT`Ws-iP)o6K!Y15HC zIbRFJ?CNb~P);WC53(O$0)Hlz8e3m(7;yrnrH*usp%m3;2XfAhvX9twHYCt@?$Y8< zVc#eFRYN-=DE-D;%wiyu8^b1Re0RRkip*;6Q-;7o-;jz5KcEQ|BApB?lQ-tE>;cup z#frvY5kbZzk(5Lc?+V>%9I0)Sw0hqB8C0=)0+4?{mpj5Vijv{IWQig&bi*tTzTaEB zH9jBrPV+kAN$L2DT-QU*ONgg<iMKjued_7~YP956D-Iwlet*~l8jIcL=|6d``<Jdn zO|*#du&1ioxBM!Q_zD@U)m`fLVEcti?U%>aUcBEd`dJ-(|6uv7SV6&zz#~xNJ-te^ z1mYiAx-`CL+=D;gLfha&Bd1sd{~vqr9o1yky@8G>)=^Xh6h%-#lqw(~9Sa~uP<jVJ zKzfr-Ky(zP3j)#^s?<pD1Pi@{9$E;}A&^iLNJw%|EZ;ca%<o(4-gW=<4<jo?-}gOd zpIx5k**oVUrb`v&{$Et;&#mg=l1hM<wxPDVg04{ZqGuM|EMD>AcM<x_9&$PC$Vp`) z9_SJuk}AqaLl*n$YRd{x28ptsBlc$`Y$8Qr_X{ue)vkmw(XL2rYtyPFKhF!*9J4cm zt!XSi=gxbFO3(I__1S%S>dL(V0l3(gnZ@Y!ZcJE(!#<IlYu%QF6J=6pRb{Gt#RkwG zTf1ncKS}774~*aM;j<|~ot*r{Y}3-`^O&M(ypObJ-q5UTxLWJsaE&IXUafn$=je5- z*bNr5HdVSUYk=cceyehvyle8{)`*h&DABtT9e(iJ4D;_FI)rLL?`b;1wmjekX+yVI zs!~o70y27a&nz}>S)$|afDW8;yhhNa<V@pF$432P1Z_v1<AGL#hd<*+SaUNZS;tU@ zIeRqv0%&SDF4w37`o;~<3!RW-tDph=GbbEz<xk{~blst$efVq6H>PZ;K-<7NvHcqG ztQv;RMZ#KlHz05%z8xfB`WP5xg^okBSvA%><rR0M39VeJI5Et+B$V~4QoD?<2~|(O zOK5=>{We`@(|>2q7Zca1FzL|#hOmLlyPV<##y=SWB#XTY@YJ6%O`)E;%h9~}2`6Hg zfS$N%=j+$j;YnwqPZ7yaxlFi??_WCRr|Am;V*VboabB_?K0-bF6{j<D5#_uky)f~F zGFzFNr8brTRJ{V)f*noVk#!rB;YGhWcd&$hK2firvc+U)U^P~snd$LfBBlSxNou{C zZ&grCa_R)x0w1Jh(>3f=+IgCyw>Xb`{g)D$=!=44tw?k8FbbK+(v)w7S-M~K$V{p7 zj;&A)zDh1w<69$KAjg%-&9PTFr;j!i$FWj*1DvguFDvw`(w~1pcMc?sAqn!WMTPT{ zgbIwMZK*5L$uS11;3;5J*W)18(TkZS&X9?_W3d57ySEH*_7_1Beq7{oV_NsE+3+#U zRl0oAK>a|W2b(i8p{tU#WEc#1ggY~p$>gKD)|o`ZOcK$o$Mp0HDy?n!$MCas4iF{p zZeQ8pQYun(RavCGsg8eCaH0y+^e6Hl!pRKYV4nX>EN^hY%bVO>;H0y|eOJ2U(MV~$ zoms<0+ThmZUZ-m7x}lW}6}rDwHiKpdsQ@2Rv}iNAS%pzw*t(pz)MkFa4FWsIcGA{4 zbYg{|yb~p$-<`yARI`JgiyBbn8t(&hQv?(Jp2*qn0}=+_dPQ6hCXZPuulQkQ63p2o zxMBT`@d<rtx8#|Qtv@O{%k6Zt^kQ-xBwY(Vj&Zzi9Cw=hgIr?PB<0pm+5th(0{Pxe zH|jT?k5tWEfaZ-qbvi1WU!r3YwcfY8W|cek=zv~6$N1EX-iN6TPDb7}Y=O?<RYk|7 zR>S;D%m)2BHZ))5eG0f2ccJJ6&hLov_AJ*KF^e)@kqI@N(f90nIsR0+J0M8h&H0wf zqbiP$`qnQ(X{27Lo@46}<1Ll;%)h^w@gNVQ?w;zyA4K+T6z#E0TdUXMHEwfbf^KP5 zW|lxy#`B%uishAiHHpw<ARi1YFsq;Gugs0}8+iG93yg4ke|;~?W$qPThxLtb7*miG zQ_lSfZL{qpL8n)TFN-m4IqmMwx>3RNPL2-sd_V>%->LSow0~ALcsUL-Mi;qEXNk|p zQz^zo!TccRtUjXw6s9CM-`>=Ruo8jloWj6?`Y>Na6H#y3tLCYn&%p5T4X?gXJ}>NA z&VhJ;$<v2aa*E#E^j@L(+LyE1d{5~aKuUk{72vJ>`(-q%BOO+&eO~!LhKgmbMn5+e z?lY=(l|QzNhb&4}JzFUa3M#dVD-@i7p<|tlxTS+oerHDUP70)kBniJ4@0hUCOk0U1 z@P(9g3G8E)?|pl_CEAG^v?{*cj5!jdiE|R#)Q&=EU3}V8<lt|#HFDmAAL<{aLkznM zRO)B)p6<GLtfpjS+OMqxO{5QDtiKkKQGS!COG!Vlf;#KTxnhciseu-(^Y&lsYhY)) z3_zr!yHaK#fOs`X3)G?=C3HK0YqTfwYMCn9$4<#B->EoIQtdfNTSVygTAsL33g`=2 zRSx14tsd_+TU_b8dQg);el&jHZw#Inv0P7wy8~2>^`kqMsXTU}1+v<b@1lM7ug9%j ztREEe@4nvg_E70^MN@6q{AJJhM3dCEXs*h0E7HYyib4id1CNI`9M2h{d`~klTS#xm zP>iq|;C|-fXAGPMc0@+poGVRh;?oE>6*o^$i`+|H+G_%X6t2}3X6SdKOU)W?7ReZ) z^!|MG)+hNiC?f%qu;&PHqE{JcfgeA;XKN_K_`p)a$l1E5bnfnGnvkw_qyg^D9JJz^ z7@utN1g3mcDJ<5cI8>%;z%3P!?}i?>vVh&nU329_w2tIzX5q%0>Wi2k*l1+E_7z4l zIX%@zaxqORUk5<&*#U8t?E}I4y_74Oq;eh&Ax*y=GopKVO~C!cl9AhN>lJ;BuN)cl zE;Z~DR7T1Jf@ZgJCtqc0>JVtJ=`kL2GiPJLFP$0?kMpi5;1DKb`(|as4sc-g$$UF0 z7=Slck*%cq!D%9n;iaHv2*V+#>4Z7nVbaI@6E@r;aEm-_pW{O%cnO^0ip{<>BCP5Q zvAJ$uCIxNl(`K*lc3PmqDAob%xRto_v9zGt5%`659eaaI(l6XaLTqFQVe@pdZ;gc} z+$#m2w!&R!raw`uuGrbZPqre=se_M$LlFE?4H>@r7^b+-->5gIqwj)$l<8?Dc|G47 zyJ?M5o_;?i@?TF$F6nG1)@-mi`^XHAWVrCPT@OuqTE3*3-<y|*=!k3}=C0_Pdfx?Y z0~Y&4Gd*YpKiAc_u2tz|XhS?PlAt#<MfCVolLkYk(W~_+v3dHaE4MD)7Z@S~v5L6Y z=94;`(D&A2`5G;kj5F_k^3ch_#^K;G0UVx-iln}0cenizha9YZ-n%zdNUflT+g|}R zMch0|?KKVx`+dbUfL`MV%qc~q7KnBcU;5tO>8}ksY>6w!%|4br5T4zcvP5RN2j=f9 zs(oc%elkoZ&GC~&trcyB<b%N=yy~CkQAn>pO}03bDT|6D#ZI+Jk^w`mmCOvb?e8%e zya!bKwh9e(PTfu)>>%6Xsp<7%gEA5YTR)}?<dhjj`P2FH_7RrwD_5ejBGuK;dHUhB z2dbb<p{4~MTLu>$mfW4cX^?3cfxOm?`|!mla%nV6D=kk2r2jl>t`(N?>8$MGcW%8` z7NjCi)Q1P`ywbAW)kw?1a(mjNb4=K8jIEr@o75JGGMF&h$@IvoSx?PLDVKa$*M3B0 zvc`pOmN%ZA7jcFa*z$An`U~e1Lv+Kc!!|0bp4wTI2E4-^Dw~!OZQ;=_2<<#4-r|b5 zlQ@bb#&VsOGpAx9?CtsSN0*;!(o}K4E^;3<puI)ppZAyIf$R7-i-~iL)ik;@T-7Ys z=e*zF{M@xy`f_}&eZ}v|r)p2!U+xxo8}6&Eu5OXJ4buKG?Zs}*42}ka>IUCLhHvc0 z@m0|3gI+nYpaVKc^RQ)pB&M<t$Uu_}Yw<1D>2;fYK2yDjF5x5an!=&lYgos9w8R5V zFL(~#Onx?aq*-U)D*JhQspJK~V<A{NxmHkSWm7t_IHzS2Xf&M&|1krA%^!FR%r5Eb zR88gmYlJ5>N0)Ijn)8L-T{X)2h*1IquXd@I++O9Km4(POAXKd7!~xATUR#0P*%%T* zKNlLK<i?dZ7|Dm8cc?fwAkde6p_YyNCIIoG-U2*EY5B;!>xq|tG=9~Pg>aAF%Ds}E z{d;mQ=w%x&4~V<AGSmp_C+Wy1^=1;*`3-u;3P3mNdCvG|IV1FXczE`hZd?kGuzS}l zDIyFDYlPM~fxN8Jnf$TDYdttkU7PkW2d;6eBfvCvskxrM{mF<_#H>iRl__^6(#No8 z?$7ZLSDfozNAf^-N}e8z2GeE%cSsp%CSlDF5%vgH2Vx)Q0a7lF^3C$!(SpjhI`6Uy z9n%HFCO=oxm+jR2&d^ibf12fH<pN;Db~LRUV&~DZ11rq=o8KD@NVu@(TPm>Vt0$@R z7i`N0D4wHksh<+V+gK`*U*bPYd{6wnKtJJD<I7oFKYL0Z>H}z;?r(@>?aa!H!y~#U zF4K9dc3^5LkxF|lCxp;Uk0Bj^pzoeIx`A>Di^UjvY!rp{RXFjWX0AJl6^!T+%FwJ@ z(vlNn{9V4+j@h!JJ|4P21;((I9&U+1MUP7KyftYTD0Ki=5Qc;1>_>m|O>AGg4!aHZ z75Sc(&!>At!j-QRo-|s?ZOB-VhCr>H)pvwUy3-5#rN1dtf##5q)e(2QX?a0`fIx)} ziK_`yfU=PAyOKN=rga9hb&*Oh{q`L{Xkhle)*B@)(;S3&5<s8kI%vdcXfqirRfNZ# zgumHT-ax(uB0r!N{wOx$>N`>O9JFUm#d_<!`mR0Gw`>yU8+LoOvB%<mpz$g{07T@z zD>OFyrHRw{z5>JP$bC?zPTe~LsE;d^6By5=J6ynH<-guf4975Rx_jD8bTJG&cuK#% zR$hx=>F(thXmB6Tcl)7;N7@{CgZSQ!<1i8uVEm@9JVa8BG^hXSaOasJi_8HV%}>H3 zcF0AT<bj9=EKs1fM4<cL>Ci8Z)I@Nor;J1M9!ZyNN}gc95P5>%4rZha#Bz@1lHao= zw$%U-0RF7~3XxHTeqfwP(+&-&T6nU8V=4+2-6z5lMGWTnlN}7ZuV|Kk2*RIcAF+Qr zBepWmwnEs6XvSz&@>8hT`CCv%11a5O`Labm9}zGOrNVY2S?J`=Nr@FxQso`0x{i$L zncsJ|s>n8-l#k~)KuxQ`zH9=h3hmH&js|h})!EeE<-G|o^OWCHj*lOiov;pYv-@7Y zi_uFBrJn%#Zz`x7*#~vc0Aco)bP8zytvQB1HGkv6S%f1u(s0YwkCEYurn>|0S1bBJ z%`WQB_yVorGr8o7!P%bPTcaAbGti_FH=7NJux&*VcTfamA#r&^M^@Fhd=WiSVIU4V zD>d?B-LCpH*9x^a3?861iRuhK>emXwwO;vT57(?Mz59YW{%+XS<ed~=(X8}DOQt#% zfazfsn5v3gsr}D{@I`v`iB@eEh~)2Ye6*KnA*ejar(&@A(B#tl)F?jSXe>RsIAbti z$7yW1d2rz|yCqPmA9$S3_<YKurC#E$$kapl3FIv38|UjDl~E!|in>gmYGDfPG#Gf7 z1?Ku6TL|;ra>BJQ+RegY<o7IRCayRevl!mZ8Fn!v>KSK@QW{j_E$<*sXp}rsDz~Y= zz42!j_7I;*ZR-}zE2o-y+)_N6!?cJ?Dq8Qy0Ap5Q4Ki#l!=Vu?D5x65rVQaTDl^?< zTw+Uy7ge|bPZUlqdTCnYQ_*9grwec{C74vsAA491C?R#13>)MYYq2)dl~879GoZn1 z(BI>fkImPv7ws!kI|9bSR9Ro1yP?%c&h(zPY)-ea(zuuU%@L4!vPe7%*3!UGSj>~@ z!FY>N*idZd#jkpaCr|2uXU55J$2Gb;Wjw;~$&FvGrNf>Yn7o&wd#~)C;F(pz@wV4| zLDnS*pE3{{l{mij$tVa@eLkYdH2Qfy<7a|=$Oq#f(D4h+%cC*4_<*_R)xCpX3p;gC z*vXze3FvD6ud`t6{&<XhSDt6F(V88xcj*pD-1_Pc2|d7F2dcQz?`$CKF=}7V6B=!q zNH#B&&mb<x6vCY^3Y2t;iIy=eIb~)v9@CH0^f2~_D(|8@O7X&0{yif3`&9tj`fNfV zl0^y|_kso8HbIBmUddKCt;2paG32n7aiv38^&)wBFgwBp4toDg!y?5%M}TKTbzi!_ z-Qs9nZMDtL&Md;TwX>;zHW`^nlMpVW8{U#-9<QvC<)=+mOU1e?@CZgWZULFpYgU2X z%Yd$=&ELe|0qy9@h*;^T3A7*TFwTi{0&$XQ*mkK@IhkoYU~G6a0@{&neD-16=;;y4 z7nzn0YQM7<zoB+}jMx1>RLVqgo%J4(v1T@lhc5v~>vhSu5K(BPK6>rZd<`Wz+xM)j zSri)Elur|KbPM$!%qocW?wD^)c~;A(yv@ZBQ1eHfUd_`w`(5d5FxCk6M3FjmsLx5& zDdJ5}L38#r`J;fW(re}CWn5`w+)(F4+Sj)Fum0gSm|g?o0_cecGB5pl@0c}1m$>Od zsMWqD^D;_%>}9UDC{8zCtf3sO8~kPbML{Ql!|zIWsEtMr*#+^X1fAI`(%O6rK*cJt z$V=RJWFzJ4-;d<oIGbxwR5Oi5$t`JG%Ir*8=vP9*lCHboY#<Oio|rg?cWq?J!MWbL z0uG^fonHk=$qs-bjrh?f1P6^i4(6wP)AMjsmGdbAy8fW{cWC^Bihvw`1Gm#}@M~Ka z@?L%1QFn)PiQv#R?evv_K2<|r*zP&73DVz(v)QvH5ATNN7dn{aBXgEI^7ZB{EzGht z!r_86@m;MLy&9=4UQ4@9HRB3hA9F0_s3(xq+XpReBuV1<1f1Yubm1(nM^)$J=c;9D z;Ma3C_}`BlfdR}41TBq$&L?Ui%&i4C$`v$LvY3m5Lk?Ev_U}YTQ2U3>_yChvek2z> zgq$2r0A-`{_w%xHu>~QA-uc-d78f~guE|9N_#Z<VlzH4VX;;;sL=u}0fZeGXsBDKd zQ;_ZmV^@4K{!T@~h<%QK1a6Ra)Ws<!Ct+fxUqCj-37+f{R+8NG_5oXjAoqi!bJd}8 zFCSYK>jhle->+xt_ts+<l=fg=0%5ebt+HuZ2-6=l?f2L{NkAQPA@ytk9V(fmBV<{n zc)oo-QYwKx=kukjTyN=Xz-)${*3%EjiCFaxER1xCNr1wctC1b0m8;PP=0C-Pc_NYH z5wo$sFADG1bf~+MuQ2gwv@j6o_k;NO%dDbOKu_yy0GlC2lGk=p;T_LY1zM9mC$7NA zgnX&T0ixckfd^(k@X{#*l>Eia+`VL5F7;Gu-XE75KpGhoRZWygrj~)-wU}$8f6%Fr zjnv!uD#uSoO+zT3JYA<Yb=NzbWY1EFT~$LU4u`z5by*x{-$c7$z;D~?N1`kfZeNJv zi@t{YaC$`OkL=6)+hXlUJcENZaFDi>4+`RWEp%gV47JG&9gNAmaOgD!f(__S_Qgog zd*FRB;;0SE+YK2sQ3{m<r}Xy|CE>LXLpUw6M0w=L7{(YV5GilH;Y_7ygKBG;$L5qo zx_rI0bISoM0X$^-^fzGM{=S39+2o$cIUer3;?pcOTX;qOz^ks86u*tsskBX2f5HU= zK4N0}ypcn?dv9qGKe~Wbg}?Zrqng=?4DLVXVLRjkA^j=ZBaENXl$4WgV|(U1<LPB7 z>u$Td8}Qy5ceGxy`&hc%7K6pgAR-qxMs6nya_{Reaf`hKu3B{Xpb>m9Mv$LoAM^9Z zo^<-a7Nm5=xSl7n!P@BYT-NNmiZut_pV2`R0u*%9X|K@_Ss{Hz(VCyLrPe(RQuYNJ zF}_=RMl?zZDYj&~KDkXQNl_*@TVRJq7ehuA(=sp7j7)I6UMiKy8O|CdfGbiTcwMuR zEGCN>EU>*~MllqIHTR8@D~k)X=3GW8RvxcazJBQIs{B}<v_JRlD-wHCM?A^avp_q? zl-w|88RRb9Z86>y5Yi<!deK5ZyHgV`Er{`1?N*h4on$4t)1WIH+r|+YLUVq6ekyN( z{PvhEL{5sZnQ=yvu`1;?r?pTt>s^N`&A5W;#mW}4%UtiR(pVm_71WC!R&OL%t`3cd z@^FD$6whfk=^$6G6{V4!ri~@RER_Gg#a7cjf#{vu3M+*tax6#ca6|dM^F1bKK(Fcv z>(7C{M_ct`uWTfB1T}?u8l~o5u6ew~j1R9Cu!vWsslGh<0m5ITyb@uvImkXeY6$Hf zUOPdg3!b~n1lJq#XsRe~QG<YIDKHMC%ke+0uAZV-)Q^`Tdk(RxaK^BnCOSVhVBerC zZFj%)?AwA}KDdX7da7p?UGU-w$w{P#`2jr<HEw#1`ohl6@sGP_i#!R&qD8IVeezo% z_hD6U;@~jEYz`1x`5AN){2pTs)&*r#?Y(v08?N<STtJ1nsOaL^tAw6zMrjpE#S{mi z_D+Z1u+ENZI5boD<N#hAcDr1tQS$t@VUo<!8*Y60$o|Bp!Z<7cPq$+v`_pa)23?Xl z{p`g#lLyHq&&TYGjYH44-x{&E`F0l48fljUj_lGsS+r(&Pw7&Q)}_V4s$ogg7X^ho z|Go5Va%`NS%IEx&2p(Sh)LY7@-G!1{Im+d>)mj~8J{t|Gaj`|0UAkVP&1(xi-n|PJ zq7ml=ORmPJn0$1ubTL)FvcXWNIv^0OeJS7L*^*S1=crG4waB++^5!$6BqBQOTt$w* zrxflnVYjuVBoad`!HeC)KfayaA6EMH&NOIf5Sk|&4jpH^cQg<ZzH~O)G|?z5HpZ@x zq>h|!cWc%vDlypSdb{x%@tpokpVQ6Wi11*$W@G&<$wNlale%Htx?3MedwkEI%3&4H zRKO3s{r2Aj3nQX-jSYqK8^E4!tuPy^fbBF9q`t6bu>W&U?->9gg3VC`xW{+tjlQfT zNy51yo;=s5K9)|!8x^&mUq&l5_3_0O+0$<wocV*gZN<ND3BAP5L6};ONpfG3!Qalp zayi%cX^9l+#T>xbI--QTS^e%`bLcB;#t-iK=DH$NX>hQe>LM#`VQ_;9J0jUbV58~{ zs4p@KivQfLv@$?!_AwWITcH{E4;j$&XDD9lfwpD*sLYK~bkmdi1I_$nD>@DLjs+^P zfvsZK%QQGQ1%;8B@iMOJy%ajXt0oCT-mhh6PE?tMY=<o53DIE;D{Wbx9lL%BH|mak zG*mEh(ML=5+tp9ifED|ba_B0h1l+5o{JNwAC9xK52VU$f9{$T7wgayyz;ls$9BdE! z3F8yJmPZt)7MW6lJtqPrWm0gx7k#PLweY@usn=yt#5`k5Ec|YbBQyH(a#^bf_wiQC zz!{F_TC$5G2i;>~$gxp3xiZ5CS*r5u%+-woM*?06z!J{LlD92};&~gkr;7W6d93+# z@A=X=)#I(!A`Z9Xs#JRurA7;@lnac?j-7?Vlz5A`Pg>^Rq5tlnxK9Cdj8uHE=g=^P zvBx|vK9A&dCUU>pC`ELYrb_xpSozB|1Gx_#G=kE^=27h-uTlMofAEfMXT;71EP~aa z`|u3!;TeYszqKj04_O9fFTtMz+qk74SNrCS1C!{%d93CXWcwbp{`%j4Tu-k15C|y$ z^CSQKM_=EWi34V_YoK0w;h(1bKi~Ov$^UaVel6_(UYf6o*Ds6x7Z3PX_Vw#r{_;Lg ze)-t{?MQwdl3$19*CF|l1pdnKe&w<MS5EOOlKhG!`wsq!m%rlWuXy<@UIJhE%N6`` z1^?R>gqcnfcQ(@T7o;t^#Z2Q=vmFvW1iugtsPysw1yfL=oWJFrhZO<*{iUEh{^WoE z2H>A!f$^qYp?tm$2_CSaLW7nWqRHKk=zo6jKkr=r9q>EPi@U9q{(cqe4WyaS?khs@ zR3@JI&&U7!2UintGu;BO_x#JvJn{pK!C6$o8NvV4AEfb9uaH&rl~qtSn=3f8Hua`0 z%T>s<2<einmt&FB`8>-N<xeTxUX7+xp4B3i==WRoI&3&(G)3_#GY1fnm1_e>=vDg4 zU5dZ>oNZ}$&@Bop%0ntT5Osaon18-2HaKlIWX+~-6*@ni6;OYAnzg2O+@5Y^z9+qY zLA3b6d{~Blsnu*dQ_R}zvVes~Z^PE?6@li0k<wr?v7|~Vafs)3;*f*+bWx&0rK~+Y z&#5f8m5q*%rsnH*0-xQWyTVbBrBMBTi{?1t^g$<sCR-lQmpGpCh?rm;JkjaBC76m3 z?c0^Ip{)}<*Toot2^bYjxMmYouD19zLT2Z(%v*wgZveK5D38Jh?3(bF4`}py7jBt% zh1)e5unch%Jj2{Asx4!Akewp`wxA-%TM=F<%#n|jpUAN@9Iu+V!_@u$(D6q`G)EYD zUqZqsgiRpRF+h>Ny@#v$0g<J;)#sJ~^GQH;m9_eW<X!degcSSd6mbqt5EoFzZWIjX z;%HTs`L;;0MWrl4L3eN|gMT+)<apT6wi($%1FhR}HIl}(zug|Ek4doX$TDcIFtleS z;zwj_$c~@nE0mr>6^%uNEUx-xH`jZPva87~4?IHJ?x;3xCB!YOKw%KoaW90s{EeAX z9(SyaG@<zk>^vijE?O^_V97&T?|{3I%QGxG9T0E$lvS`BasDoZ`?t*3=0b92<L;_b z)<aHvH;J!BaIs!Cd?P45^Y+hTb*O)DnNok~tPmh${_FgJQ&J}<)4ZYOkF!0=7O4)5 zbi+oZ>-leiNeri5npUg6;LzM64ek4j&XuxF{Fnv6D!KWtin4YnW~nq?`T6lG6DBf4 zJonG+l-z|wyhc^;uU{wB*>xA<%7RjyJ(l}RO^0w!WMbIx{5xrj3H(OMP@7idz7~|v zB1`dvgs~6<LS-UIQ#N$^1uFxUBPqvt&Ix`>M`1|$1GvO$)a`Zben5<BJ*e$?VSGl? zp+DoJgwk)jcRi7(*^92%e)NeyEVfvql;qpKQ#{JeBE-<~^F94=9BEPDDR~ZMG4G)f z7XX~%d|c~qz%-U=B4jrno%0!eqO6`#5{N#K(i8BSE{(^n-;d4+%0p=%&b#%KTL`>) zyoy8~n+omC8S=`5cwp#nsh_@&nZf4v%Y>&Kz`t|t@v)UtS|$8}J|B8)MOWQ&aLjZ4 zYA*aieV#OKKn{Aw#n`@b@L2A{=cv@X6y9Uk8%+JARx%2<B%t*sMiQIB%Cv7x1#u-+ z3xthQoOb-vSDrFwSH{Yrs@m>9TiuTxZDj+`vNxC)aQs<-WKpQL-+%XsoW+Ul%?)*P z#9q(5zP(FeAMwoZ4eQo0Z<sY_+$MdD35e4s2YxQ&TK5LVms;J%Ln;gV8pOuRsDv9` zm@lt?tr5D6yL|1_imbyBpW}5(c9}m~Vy&qYo`HB`Y=KJ}DR<3(<tatKqH9q8v7sSV zR)BE6!ncn2MN%KVvJpp`+RWG=M+}t9-ruH#4-of?Q<c|lN0W0fF0<kmc>@ZT|5h@Q zxxnQA0LG$&#~+PT9Vnh~2C!b@ysOjdvW(KanB+C>$u=9O5fdkqk;W|tf+ItJc7TCJ zpdXm?Xh&iGg#KyA_Rx5<InU8Pn;wFpjCh2ru`dEXV~3WKh7eSnV<9>u#_CEGqjAy} zBsDaXbv+q_cA-quD>LO7%L-xI^fEp%+jzUjza@+e3~#JX=<h|0RoF0%ITjo5&WXWZ zoiXixxkiUH{^Hy%zB4}T76-E`EOq|!@w)43qZi=PONYb>Y18ICWJPN}0pqKbpq=8e zKuK%qYU!QIuxAvBt2zP_ZRDU-|H}k?M5XyS(n7`awFhLHR;)!Q(J?ue>&D-n<=dXg z%>v1UZnqL2%|2IC5PdMv*C&8_;tnmhk5|CfOF@_2k(P(Qn?UU+eGn`Gx<!ya4`lgJ zewki>c1V-eS^E@^c7qDof{Ea6!f^3eYAH^7uENfIK$~4C7UjNGUwmW!sBr9{wv{mv zF<7<rvt}c(nWJYT?fjX5_sA?wM3v9X+tWV1XKVro06WX=UpeRF@+sM3D(i|sjOtc) zh+`O^7}-Ke!d-AP{eqyQ-&}Mb#iPl%xQ&pQoyQme+a!dnA=n9-vsoueKKKaX2$opo zp$<Hk{0B6V6#*TmgA2%@ONMO{&r@AlluIXw9Opa;7uJ@}Lf_29G%SqNdm3HcPVdvV zLBvfsKQQu{=Ctw7qPJ$~f}k!wYxhs_!>wJP8F(h7gL>bgAZRrZRy;1$krfkgb&CXK z5O7zD$!jfxdPs}c<`9=faTBA89s9p6cw;*4I%kS?udw#B-va%ZfS>})WADH3;I)^H zd)3F8T9QOAPuJ5;5F@Y8zVBz(ob>KX6ADDbcy~F6Mrtgo$rOL&UaGHapVj5ekO=4{ z&Bmo#pk)F=&y;4z)ea`G3<U={`WKk6sv^n*s+GuM?A}Lr=jAC~Nh0jo@TBvlupMrL z3O$J-BZsa#=OhYBe6Bf7NEy2A{CtKVY0<QqI43*WJv_IeJ#TbYa@VHi`tIf)n?Q7~ zv{N7Mso;E5d8G-9(s4YMfiFrGM|Hot>$BTn<R9fSafV3&%Hj#v^T4v>KfkhOjVoTx zIzVA4jCF`9E+`U;mNutM<H9)JN$!mMtvM7oDHvMSAqQ+&TX4Rf9eP6z$UMQdZfl`f z?Nbj^uC-bv6_--1Ohj=}RWpPm!aCkw<I_#Van<M3nCdGpOr$t8?@PJCH+mhP#|Qx> z&kt|*Q=nq2m6Q3r@h~;{Bv9-q0VBUqSW$hF?VbP)Lw&Q9`0M2g6>O+gSHj?BXVMt9 zA?&CpH)|UCGb$x`P-pR8hPTeVKn;DWuh9M^Ke=XNiu?HLa3#Y3Ovlp2QkmV3aALJC z`AplZ4S3zQ)=0YRqMBX7S>NSxG2HkHwAZ|iakky^=dhVpe9%v3dLx}x(CM{H73G|) zxQ>0lsNJMb-u*sm#-+yD-{4e@pSzC!_`$|I_wyUYiSSsJa#is5JVX}i1y7FMLEVO6 z?`v9YTVcdp2n*5a0jba0lTW7RMH9MlxA5I|P|!#ka8<i-Msml6n2JZr05QUSC6mPv z`U5E^_u{1s9_wTk9Z2zTmRjqym|d!H=;u#iq1>$A)oLDqXE);lkP<Pf#PgnODD#al zAxgYb{^ftOWw``7i->up#L#_vXlVJ^oa@u#P&0>)zdI60ia?5`K&p2SJ!;##rPwTe z!a?_uuHTqFUAkrtT>DxDJUf69Nr;tAUWo~)Gr@=;8MdNx3xGLGul_+^+niB~uL;p* z^<j@S_8AGabbqbSO3roR3Ou0H=wE@T%2~1`I3n}hjl_KHA1{{qXPbNfY`v}Cfx$GN zHM#t(C0*);q9y0oAGl1jAmZ|CkUEl|Mc9jY;!Gvj4STk}$!FqsuC4a1oL%w(WkbuQ zTKmW~oG+$CH_2`NovS6?UB9dQkBZj;!!91oHf(X9N1nc$c&$s%<o=vht;6Zk`A&Xa z&c%)9BaZxWsqK32&2ALbt|a6>9L2w<?|xP9i%I-<#|!)bv#%69-TK}BJv7fRg2lV1 zGb#lZPmC(xIv$iFnl`RvZ6Q#{Nv>+s;hcV_G)Vy8f<s@sOqq{r&|sqYFJ(jp^|oD^ zbgJ9%Q6(*BY}<T*7vq-fS|sFiAkbQ?oGDGLhC{o*?v_9eLvm&XqR6*pge#2KH$>tt zA}By85IYYKfrJz6zDDz|`M_d2Mc2c%bU&xV%o&ZWzLX1Aoe3t63;C7U)AMu<SBt7= z+o=IbrJZNPVnu6~{QzBP5SMh4a;zV=vSp9p%tEPhUp)T{V^^JVKYZA1FnN3h?y|FL z*FHOcvpGVRbeby(V$Tt{5HAz=d%cv8eXX5A@5kpyEau6`>1VmvoLR0{GY9(dsBPz6 zk7`u&fz&$IRDYa}pSZ6=L7!uJdH}1Q<Y?i0jcU;>;h}Nc^1ikx^h3JDWy?~CmA{B^ z#d5h#^Aw4A{<QV)62esR+|SnW-7%CLxrXyfSr*p3`9$tdphgiWV-Pc?*|#LmChm@T z<EGB*J?z(cqow6Fku}Bxo3O74cHPo-8S)TSMvkIKM(bt;Y_=)7w7;Y0mZJ4DgMFHA zC4~nws#M%Tvnz4X)dv`?YF6*gbtT5_Yl`DP=RWxIw432v5E;n%U^moGf!yLD`I`eX zD%B)J$%RWo&$9v#shu$Jw|ay*tno=SUFa*g^!>#(9Zu(Z#1o_tS+7b6V&uLt>+<34 zMtfL3Xs30-z9qKLB0QUqKxJ6nU_?=WlE@U=$-1R%v40A40MqLA(OamZOLUmmv3e$4 z&%U^+?*-w`eKX1l`90)#8Kz^^*Aq@9m-&AXnP=Fd*i>`3!Pw7DINdw9i`dg!W>e}q z3!%d%Z=o1X0ybCl^5i>Q9il>&;OfE8)R$6=3*g-l^O@XvYu@O8seGTvc?9~soNtqn z<lJ}gR20Z9ZyGqbaZ)Q{e?A|AA2^#$c1|$HHlDtYhS*v#s6<_c=ylF@>4wdQ($h4j zIy0kX8RyCZx*8R+M<~a^D*NpiY6w)e)?M@=(``)CNs9$@+^bv8p~}MkKntgy)U9wd z&P4agp23PjD%-awVE_zpA*6li-SUJ99Fy~+Oyj4KbQ8fGm%og}rCy1*bzMgybt%%L z&@x-4lp{YI%r((KRoMcQfpenECHI%B2GxqY7{}yW1|_TtQyfrI%YV3;V^fHXutVrz zm$;TOFju%L0#(>A9=_oD&_!;`f!^`eh51yIBJ4m!LkGivn?gmkC9{I-$DKiTD38Py zNQ`S!E#0n)>J@@#P)X{;6o@A>e*wi=Bw(n;pS*7)HGm*rtPmv9*Z#uf8b@;&><XVi z%yl#Pv#Cw?4$50eagm1k8`454zonL~^k+DQN@eZR9o8WI5YhfE(NCMBsXhnS+!$F5 z{4KDVANZVNXxbV*ThAlf66wsM&NdgSa4yLU=VIfgetuV={IiD5hh7^7A&w|Ew4xK_ zBKnHWnS@MBB&jFarTry+|GLJj*E!+gGLqPM<+*XsySuPNyzwp>o-EwG2s~nVp(s+q zVuTc-{!+MGviy3F?4c#~r$orlH`Oyt8-7OO@_Da1pPaYQkq3q8a&x`Zw6Xtm)Nc#j zV};4Famq=MzSdzoM7?msA#|t33waol*s_90F<OVphzxS95DU2`LO2;sFY-Yf=tppw zSuL-3C%q%m<*@8T*5tJ?8&oIhlIW;6sqbzcBzYWNJa(z6L`A@`uLXsv71^KaX`ocI z+@v<P`ErjhUFe_==D6(c_N_Y7gyHMDm=)lW;tCC-&R1a{xJ)%_D*rp4^bQi(4Az#& z?870)h5$+R{AloL;7e)QBUdiCTY|Y9HNN)0!)7`SikA?z0ZHD=?`^kYlG1TQzD}`r ziAl@2U6g5_hn!#aF*IAoC2||jc;zCoB{W0A3wvwo&8PO%rMSGLG7?<OEnOvN^br4@ zEi_fi`XASJl=JF*B6NEDOG|TqG?%vTK`vGggL7qwCy)9vm0Va;;oDLbm{R#>vPK{} zC{>2-JcQ6&CV`|3NEb<2t3C&Z4!LDiFzzv1=CPIBTEWF99X_MA!*SgYWB`&<J8-V4 zNX=us$V>$Uan}-d+Glq)N@mValB;HGL}=P@NSRxE&3v`@RtDufQMWi|=|A`N(+^tm zX@V^qT9zqGpv1Tiz$&qBcVSzg^p;~AyS3E5ugLU!mdB$toQb|vyj%i%RXG6>riPyf z$OFwX=i2gH^4H_narwo>Mw2s%%iqI|Q0%O-)!khtUcKXJ$6BkQ+%l(1TTmS1dTqMg zEw*qh8Mk^D-3kr)Qp+1?{#r7Sb$bMwGLlasCS0|RuFlojs#^{pn!kOrAlc8VD}^DE zVmTcO$)W*sqFnq!0~Ro45>&`VA=TpsST8t*H2bK(=B`04LG9sKv=wtp_Za5nC-u`O zI`~SB4A!*8OdIZCXE?UD5F`k1_2;NHatl`5i`l1dr``giPW*H0nfe5)VygGUQHWhs z)%++-5#cWOPKjysXQ1sz&2}%4nrr2n*#;jx-bxQ(M(>iy>>E$yrs-R8ku{)1_))dY zUP_jVl%a%%nD?V7f6kQ8E3Gyr(~X;aLDwrLNpTZEYEw<-Zf`xs7MVwxyc_XZf<-bb zN_@wdp7QYKS5M?kELVmX0*_hT)NI(SlSsbeu{6Y<AY!MpSi$}Cy-`zHs_b>HDJNL% zW&j6yZDy}Fv&TBFh|4LUW;+OWgo!^C)CtlSM6F3esem*+krht~#N-D5$ZHt(7s2oY zEu37nIirJk&kAXq3KtI9;091y-wxq4e4{1)BYDg0FiE<;`sTVb{8@(g#vG@7bV}+e zt_Uf9D|ozdlrG_-jj%K7ld^SF+J&H&TlB1|9UZZEhOQopf)7z@>1E4UyI*jn{(N4( z*`*xcL24dgz}7<xU;*F~rCXR<SHTjS2HMl*^3YeMyxZOGk67E;gimk8_fJK`WnV!l z!nF$z!5G|KC*jm(NYik~R5}_CKR60q5J6fq))&>5q>i{X1b2CiUAEXdT5EsG(?ndx zLZ9DqXdqx*TCAxjBuL9nS*h{E;<N5h$3)M39pAs<fIX-8MX_qoU0-bU@>$PRro&e~ zQ-Ar1NXK17X_*LU-QHh(q2>25k_QS_)eWGBmjE&1Z@~2UZvlvb0%JkX`8GGd4}tS` z3y@^nB7##vYQI>pzdA+SPGJqgM6S6?-dGV6bQ|jQn#nydU3383DUrgFCUuBc9TLvs zqcu+1{zFS>#TROf59er<?Vn$>J|A{V{cL3JWsmO-A2yb45eFR;omG<Dzf6|xmb~J4 z1li1GYCiGoKrA1(T8`Q$xZdUHe=hfLfy_r{Zb_+!op=n|O1h5#74QzHkQ{IU+G*K= z{o3yF8(oOvHvV|i7UMy)4RDx6yX%*dLT^=V7j&4%t$1|k#j3=MY*Drlrqc_h;RWl5 zo@N~dDGhG>(~HEoZdV}2{*O-#tUF09TIF+~vIasqpNOny0JJ2c#^S9(ErpUR7Ei<- ztu483mzqIno<UrS<L7UXF{j*cS-!#brb8h4^{%tz&K}PZIDk1AD@_uluh-d=2x>6! z1*r?tY{V3wc6KMllWZw|+=q=|l;$7{qzIx*V%OexY9?qX0x2qJQL8SK$c&fr)2-XV zB(Ale6=<r%&NCp;eYtk_u-Ri@8zQF8^0)dN-p!~~4GqtC?;NiD*KF06Kay@uN<_D3 z?;e_Js+A&iTepFg8UibQ#p65W<8D@hEam3vrUJah?y0g?Qd|7qUNWG)CB*cIN%GpD zb)r3^_L{m{J<NT#%cU)hDMso&wEf&kXh3*?V60>HW9@|8zriyK?Ds^SrOW4;zoWSA zT^WxjH`hKzgdK{A4aflVVV4=^lce&D#`@0WL`aBZye4sd`b`MwT73AC{ia&{vT;Hs zBODe<U?^>`_sUjqkgrXz$!6bK{_g9WV&{Fi40x-xnoBr@$0BH=O6XE{ICczXtTA=f z#hV%`E#@J7I!2y-8_`pf(FAMzxJ3?pD1+tC&gaKQ-{TAPdo`asG$V3RF6}<Z<ejuS zEZ+mhFj~%GpBfkVqQa#)9`om-JPN4uzB;YZyWp#*0kBc9(Gpe}T+D=wDlV{}`#hf# zx`&JPo@niLNUKaxvr<q>LUBP}d|Q5Nw0iWVyLqE!8e4XsWZg5I=KSfYm44kip<}J@ z4HHNsQA!Ma>tng-5DtyNpR7-TEi$PgS>jOLrgJH6ku0dR1y{?Ssp#@Gs1fd{F{sPT zn@UO)o%fVqb6=z=(NsqNh6$FDrdv8eMXsy4gK&};)vj}F*kEV5DPkF-xSVKHUl~Ud zkxy`GU?YU4-SA<pwhK8~eh)r}fp)^0@W#Jm<OhVLS?;y+Ma&{Tw&NkMiV*Sqgd~-& zaGkaD(BnCO9`9AXD{t8e5TP1ut-}zjI~U{<g!nT^@t?&NquFABYl>wcctDlP0W!u& zoiU?B$6K!iChS+^`q0;A_aQY!D%Z;-#Ag;ahJ`WIvQ)V#Jj>gc#OigE66G7avb;8S zNj?adAJ<j%SR%e(yVB^qy3~dj6{?={NFLG?!yzHCVaqY3?bgHf1cL6h+!;k00Kzh| z1`zJoZ4zd@7TN+%Wyy98&kTHmzkEiPVEeC<?%Mu9D&^zDQlhdv?l6Dab$PX{aoIi) zU5N@XOcJcwrAwuv`nxOZwIMmiy@w_kRsy5f9-}-HLmwl;8m`nhOz>IgFLsWOrjiM0 zs0$KR6n0-X>@vo`5>r3*RG6PzJgaU~S`#sEEXiGT#uJP$#z98j^H}6K_?()r-@%Ga z2^_#i>7*!+OU{uO`fNzRdrBVqj55pF651b+U+fiANO3hWAjxz3lXvE?PyXS*w#x&z zEU2OO*JR!5p!{~h!ZQA+MCm_Up{0_>Kkc0-!bXh`k@5h$Vmv(pKKRZqFaa1hmk+DQ z(gm9jJq}}KJU&$c)@7zPgRZyK&%a@HHiCKKX06*}IL}avJCCtHb(>52;x%Oxna3#Z z=stZ;gtmBjedhh#5RTzm>=)n9Zz@fBVu-MgBG?!krfwbcXtZpEB4p>RO-~tcpm?AP zo1hi40jtk@-H}H%MrIqLp9F<@bf3HGygCu|`ASf-Nm1DEf6J}*2QHfWpMV@&VG*{v zD{$U%eR`UwyFo(7Fzc3!gyy8r`t;&6wPD%m^X@R|V=pf#rfQFidUp)teBD*Xh>T39 zNN|%{49h3=bNBgwlNzBMlfjnVp_%URAFVVLIS+$qP&<t5?;F%q;Brv@c!wmR^=&FF z(NB=wdEal|II)*npU5X0$pADl15%4?8vIe7Xx{B_i2iNo$N|$NP+g3FFK0B{%CM#3 zEQ~}PCN);~zBwo@EqWq9S@dnFTNMn|Xrs4@?sT_n8Fbq9`2RJwB^(*Blv3|f%sBwo zArk~Qb$sMS5a$XHsV2(sWs~s(tE$Q8op=4?OEM6to&tw2V)cvc!v=>!V|0wfQ+yt1 zoO^6O7u4FuGZ-N=IXvth1f!Cq7rlqc<N^G2QpXER=^ftc-su2eZi!v7cWL8cDwd%Q zJ`at2(Sup8WKZwe5rhK1&~w*RfdOjbwY4*hU#RD{ckke=Y+)50ulp#G`>|$SeJi!D zNXFX%vJ|~9m9+}-p%*lsYU#9juCu`mQG8S?hSNJYB-8k09p-P0Rr95*^JC(@m6sn$ zZ#5V6O=AK1vNoSiKkQWPX;epO=HZk->oIb<fkGzU@sCVibo($Fl5UNk;FPZ%_jRMD z3Q><2L;U6U47ISjYJe~Kj*`P13C7t_8w6_%+P3stjz&OyEhj?aw76^Dg^?CR1#((U zBbE~d7i6k+PwM&KxxY$C$S-8*N)QQQ_20da0$E#3j~-F9+Eg9oxG@=x^%be+-8m<_ zJN$NaVVim}YSa%}@qO5KXj|zXg%m-N-k%!a)7(E4UBQq%U{Zsfk=S{S0}#s{LsW@7 zriL&voVM}8vNY>osZRO8n$Y>&rUy&Gz|S`UK8bV#z)5l}heGQ&UR)=Z{8>Ikq(W>m z_c2XZqAjrLL8$VrGsD=`>uZtGOe4l8{YXJ6XD$7zlBJ|5^V>`ztQ7(!8N3V_ze(J6 z2d$>*(=O}p7HU<RKCI+o(CxC`J+eznCtQ#Yh-^pg_2i@pe9%$RkZ$zjSLKL>QyX(# zA(bd!p7RNtW<zPAYZjQ0ElGF2#ZMrW{mYp!KWhZoS_YsEOB+P;CEWB%KN+k|4hQv5 zl!;)8-o^6JyFwUwP5d34d7Jg<rn;s+INGc}vYuF#*M0&^nrDgq9PersDJ&{#>ClCg zwzpB<{7d~gim21x?cO$nQS_sg%k}yOCX2WG@PK^?e^}q}c1ORNRV!x+t6zqa$$5k_ zj)!+btr(LNP_k$fvBl{+Vg67PNsLK6X5io2xY816+*#n7T@P#&Kx<tASmx5c%hYx5 z+PB2*EOBN~q|%6C-q_8tYro!r#G%7)L55NMc}j)11iD6tV&O`5^K14|Wbslxr~SCk z_pVHv-8lR-#P+#y=2JXR!6phEfdBvinS(FgdDnTIQJ)GMF%Shx(hvW&Y9O}~44hG{ zZ%KvNkAp*0E?Od4RKq1|;56+@GJZ_jaRF&Rm4mV9*`wIhd%$X<KXxStcp@hlMQ7P# zvfHv2s^bGH&*>C9uFq7kp@?*}GUC+<-6Ugv821_AutO9bz0)-UNznQ&+uxb}yq@3K z*&D1DO?6&*Li^nR)c{|}8G{1a;!XmH5NS6-709rD8BiW_TeM$S=Dw@O=yOCet8PSX z`siL&HRIZq5=r($DMg(*03@0!#7}hUx%VYgLbXQ=LEcjm{{!CsxCByRA&=2)#pmuC z&)pXTP!{-RmrvnJ$UFnoD@w9%ScJQTN8W$eDLpi>-F3I5Yg3=8=Z%@pawaz;1K#ax zwairw|1*c>B%n<$TI{3?RgRd~R{4A`x8bVR9mp2N=fE2_RnNIJH!3H;OL0(bBd-OX zWevyikSJ~ImS{e`_iV=mY82feQDjHhI%c@4<C0~qnhF<%(FJlpa`#=nLM9L^oEycy zypEfk&ajEjwhYPq*9Ef-tO=jbo&B;D4o)nX^UO*u{|8>n8Shuid0`YEw3u6y1&c-m zuzI$>>a7`E&nhi7QUBK23=N%C`mOQpeX)nU`O0%tSH$W`x%K>RO8xxvN3}O=52G^= zqem&J0<vd635fGh6(>uvF=REh_FCsw&fctkLX2a^#h;I&tM=p<CAy|~rj+x5Hz-;^ zJ_aIL5wo|$V*?B1;jv55S{9GB$SU*)NuC1TiudXf4T3XoA5b|Y%aD?6JKq38o3&=h zQv_X@r>q|rTDp{hDgbcV{lL{c%7SRq$+|8pzK4<$gbn_}l&YP@wlmZ6CFi`054A0u z0VnI*t?OdmYdD+&61I;%OsVer%OyxhhN1ia(k%T%Zsp*BqLlhdsFnaY72Bx*wWa7+ zeT`;^2RtSpR|LUOT}1v%QvOb%_0uo5y+#%F=OdGPD(?3#+5ieyLUf_-!4MEsbi+Iz z5A`FVXCa!8$Lb8Tt+cy+ANGB6!NkmeE?Z2^F2CFCkGV3hO(@&Sbb)Mh<&a4k3z^%X z_xpH+4C&G6TVYOWAH3qS=R^C_Sr4_8j(r>vKd0F|k^5q0*WfKv*og77(J2A0uQwJz zU@D#-l9b2Gu4rYYt+zp?S;fZPAym4HVS@e8w#<9@jbhMluB#_t_`m_99_`DK6C;an zmz~BD9bW~7;2J^*o68fD$40EpVo-e|qz-|pUAyj(&o<2ki~3Hb58}e8MEqA`y<9DC zl1dx0;4nI0=;#fA-a#xWz%2{46uUGg_VfP#z&!_<-a)-mmLl${0AqXuIAD{B+sD8M znX}|vYeoy8)=i!I!sdj|MZt~yGrc929e335nD@#-kIKiL&NkIW1?|9L1S7xooq<Oa zb>3w*<>rq`NP@U5d1i2#LS8^_FQG@)y2hc?IjKyyd!ugGiaKOa;hc}_Jk72hyok#~ zjch-kmV5Mp4+E~PgxIV>Jx#)*gVh%KNlN~2cR&82n|G?fqz1C&1H?AKeV|^B$osyp z-k58K%2O)sJ1jG{oD3D`%WH1CrY86=*#;0uyWUMi+@gHoY`-<+#Kt^4g2AvdEM~0m z+)D6-5N-@r7T&~YSdjfmK*pQ&NnQ4`zouUQrMtlsHA9yqibjhU&hrc^Q$)_E9s99Z zKl@}VcN6%O9xRx%isQ%s{tZ~cB@?qbU15t3lf-P~W$}35bqF+jR_Q<K^gn+%^#U07 zM?LG(e<6<VK|$z2$BcFTU$^o%{m*w^>95aTlL7uY%&9B%du#qbJ^*_E<EU5A&_(^r zqa4;U1B-6HoGg~v)i7<#LajD3|35#=ztG}8YCI4=o>9x@t3G1iCvW`q4F6oPzr7>( z0CbBBgu07T4f$`s`RjoG+W`Q;>szqy`!hT({@?z?|FvPi9`e^t{$<twG{Ily`ae(U zuQU0-eAq9?^FNQ){~sY_N#E=X^oRvHVq+S}7+<FP6R!C0%+B9g_d9q^zd-4l!zeGK zso$EO>l$Z9(@DOIZ5uzAk8&1Z*oi}EzK*O#1*MSH+VkgMesu<?N*X;k=9`6`)IxP9 z0<KI@mJ7H2aPxxrAg3eopT{XY?>nP9n%fI?!^?FR&z&tSSF$gk5G{TjVz~{_x4j@% zyP2K%9dPHnC-hb~o^IoaPC$i>nuTX}W_HsW@RvX4tsN-ot<&F-fW@8tIUc5o?pqG9 zyH&hOcu(Q06A}AI*po9p=(oD#>q69=o+7t-BINOePR&4Lu`zbss1HidZPtDL@r!Kv zG#3zXoz9^jz5yYWCQ#fB77ns8(WkGO&5I86_PfSED(`f1bN6bN|7o)V&G!&Z1CHBr zgBewtH&%Ym{Juua=RD4f^%+w`N)|_$8awTH->X-Wrr()xA*xteK#n2}(wITFek$a_ zNz-=4&6G5s4<28g)0_XGmTsDNX_LhciWj@NwM1k4(;y$YWH57P=#0Y+4||GXWQSwl zfBT+5+arG4O}>K5v6@pzI4akJmHFhILyrx-k*7<r)y2)qb~z<}^lbO2nsW4^Q3p_x zs|T_&?Vc`eAPX@97E6}vk<QVJJr9d7_0CoDd_LI~_b8HBd*$bZqy6wOC6?iJ(NQpT zX!PE|fuDazJ?v~0S`*^x?De%#c5@6JYL4=->;1&OSbf)#>)1#h^JgNWMU}oaC-%^s z09elzDh44!8x=ig)tz$TQGJ4cf;MlYRGn$DyCD};w=?svIc>iM`cMZ!9hjkMcusQK zBhZlh_Zd0hKDaMLCY8yoM)^M-;(3szJV+Zgxn&oAetu>vuixfI+9-JH{JS|90PN0x zm6X3&JUO3wIQhda`W~CNgHP@3*x(nI$A4qH<c|zf_($)AB~aJBQ{C>rGb=fnBb%&9 z$qCYKvR_Jiwa9z9oESt{q8c-*F~ixH<~ch+_nn7ERe=Ug{)c4^nMc%IjMO&+M8fTS zOaH{`d;C8y;gTMhe-<9-Zh8@z*3B<Z{}FO0VKi|QlH88T*J2L@N-z!QjFDZmpwZ}E z8Aq^|;G3oc4ufsDCm%S&u-FeHqz4yjaovLidUQZfbtQ^G{SO%YO!=fSgTYLLa{ZYK z;BS$s{(mIak-9Xjeq`*h^`_~%t=gsNF`CMhYZr!$yjRB0XZD_G-Am=o-@nWT*CrkA zBzt&t63MlM`rKNEGP#bd*zlo_3jRlXqkkIQmPQ6s#?Yv`9_%AZqGSLKRhI6<+9y2c zx8dHFHVX9HKxz4&W91)@4pMt-l<0L#s-LIv#7)$`zI`4@`-tln*e^ETCjkV}0P^2a zu|akqD(8K_ig>;@(rM!@bxX5DJ5XE4p7k!HT~z)jVK8v-KQU=t70knfGnSpW+d>u- zW3>BG=p4;<&mR&5Oke*x$A3Qj;|KQ(tH|BzV&s)Yis*eNd@omE35|LZ(VQ1)9$zv7 z&g4KkxDkWT0}Oj;b_A|sgS-WavZk|?+g(@Z_Zd*Eb{<tf>_|Erx96v>r604r!XD^= z@m<Brz%5#^g#OnGoXTswC4gkIM!LHGsfMaMdvCG;qS&OCxIFZP-PsOY3y_!;#PIJ_ zYuf`=g37BJYb!OP`|>|$3H@YZBGds^7t-bi8NuJ8#)YD%s!Z~eB>uZCBk!^^&f(cK z@SI<@C${|W(N?~n&-l_oy%l}HG0z7NS#a&Et(lk+bo(tnz-40uB5$RI(<X=+N{#ZN zio&t!zCW$@!ClxB5>)okQBE_;*u8a10LsSHk6B<B{r|A{=J8Or|KD&)sicLX>@BE- zWQ{Cc6_unC(%4FtY-5sjFx1r|TV0Y6Dp!;>$!<(#t1Lss*vFD}n8BFMY|n9uOI^?X zJg?vL^!?rU>viA$I=!w-&hz*jpY?sbKc9o)H)^X?L%;J`Ph9s(%8}I0q0Xw?Ng7dr zKy2p!+P0B%Ko-4U*(Q1Va(#kM<_Jb9oay{E;1<LbaqIH_IzqkUe)-n(>x#j;Y*vil zM7BkL%ySYzI(F+S&gPzoNy}>;bFCK5%-JSkF|n1vCDQWxr{&FE=@3j<PJZ7x)jo4o z8Pw_zx|A4MrPD6W!7r$JC(EedjI|Lun%UdJh0fumf~OC^7<0`0mUT7%xf)*m;=cAB zur0r^ya9yRx^gy}C_xJ5oX)0Y9^nZW9C<rkiuihBK*Y@W;Bp?djiA(CamaYh#@&ZP zv5u$1Z($Vom4_uFZt_C`IqS`3<|?T>%YTY=XSM><1RZcCnqcnSZOd4Tp1_0PjML#% zATjKuz|+BV3ih1}h0$#0xRf=jsBcFnM8uCMb-7YwM?V#bB)v6!64>?%LgM1@5p>`e z&q^~|G;Ae(4|yVA5QA7DLiXGWJ~OShIns4px8K=v)xj4hDHw(Utm^5Cs4(L)5A#dx zXCeY&hyi%U>(dTvYokk)T1npu4w$w4D<LS@@tXZ9vARpDB0IL?`X5jC^3?4saXX~z za?LE^rBR6f7Qj%l*mTMpu*PA%U$=2lpU+;Be;jmVO;(3b!R^ulFlO?fit~&eSog}p z-h=AaO#CeD{w;oyn#Y|vVdR{HMI9Ov0Uz4)Ot8V0ofvRhcsV7N8sU$})?Jh{JuE1W zBE0ae3LsqnblwW?uhFfee%tn%V=M_*Xk0eObUZvf9?AwoPF<fIn*Q@7-%Omu(Uc@+ z)kjmTQlj-=x;`Uk=ekV3w4lLeSs$EOF#S2t@wAjDQ3CDZo#<ltw1y`}3DJRy+~f_e zIe@U^-rW$MXSsX>vF_e+TsoNTM|m}2v*>Zw!hm~!jl(>1f^%Ugc~2(0sCrwfN%oXL z%(Ag(auIi{6P1!%ae%sSh`otCf6!T{<%D3lZeJ;1>Jes(sU>CB+B5}foUAN5v<agB zBf2zt5Y|t;7?TkWu?>gVAKxS1CG$CD1^2FD4~%|_0i1ssa4=~0_M6N<k_A76c0hV4 zc5n7&4zS~~&sPnfUIXD#e)w-;;41E|JXB2Md$t`s;Kze#ix|e43y>}r^pvQ>z@At> zSZGV~IpaTiR(5&ELRZq4_OoK4Ur`-dur5!I@l|%K;pKiWlNxX+^l~a2!o;oA$3I_V z1DZ6U+4Wt{;B;mTM7K>fN~hGRO{m!w`3-dM0sl2&yC*v1@~_kI^o^mO%=2fM3D8`< zu;fBsVL+4*YBq2IM)MRJ?<lxdL;#vzxZ-J<%s-AVln2bTRA{<|eQ>pt+lu|;iU0fF zVhTwE_ql;s)+aSpR-lzmq4a&*U|$>DnKl@Edb~6Udx492-<m735`hN<o-fr`anTXT z1Fa*k4KCVP%LZH0jadHpiGhKuQf7XMh3*~IpuJK)D`qoCN}DDjXJM%n#kJR8`MgU? z<GJ0@(LaL^FBjW*bkpA_hQGk{1LVFfTXXCWk_6A1;{tKA$L<UAu6Qo8>F|9~d;N&O zEjMma>&8n<%G%?*+f5xC>-~o-a<s`$(w_{o<59_EhNnjb{m}(Pd@OQC8`V{U7#d8c zI1^^iF*Ym@e%;-ZSGB{)M-@FeO-3~TNw%MbeJv}Up(A^9?o{<?PjsecPdj8QSVZsn zYC~wzZCZ7Uuwu*CU%6ag*6y}4k#`OhU4DyjbtXcl2@~>x!ik&#2KsK_@-U|5W?j|8 z`u!DaMjkB!eP_-sAIAT+szSQchvR-qTD`}c?Ki~0&s=n4T#~OjCTA;Jo8o`Hi#dtT z&monOgY)Y9>#DbCSc#e(qEuy3GaJigE2d)Mjk7(JKDvrwH9f2a|B#bPhDVs0Unt0Q zt;Nd)v;A1K9@q?6>>GBPc1<2%6md3ziAPG6%2BlfXc>g__deH3`Ws|bjEO2cH$A1} zklcGx-2v_q2bpadZ23rVQu>Em*%VbLhvd`NIMr*fH_>d)c=Pd(@EWd_kKwvxC(meC zm7b(cc)a_~jOv+63^lvhZ9G<I)}kVd-t7>8KFjUhbf@Nv(LP)zJp1;+A^L@^!Y{af zAt~<BY{iaWs{#EXp0zj?IkmoJyqj}2EHkp;Sp$BM(^vU(S?KiI<6UtY)MITWRY9Ac z_ew7MnTJQ-F^a?yierjNC~|Z?pH#V&kw7kI6`9|pd{;$&jjw%fs=>@|tK7|pNF|lJ zXSX3|U!>Q_;H)+~N%(rWy`~zVJ?PmO^p?ylt}VJlULajE$iQv7yCK7-Y%n=-ph;+b zM!-avHdkXP(YGQ4_r~MURCzOEkP>H+RM^ouC@;YbxH;P@<{=~}?sF}gk+CJ#LhFgf z(1#1YbXvg7#OyWNO}(g~sP=pUAqx|`gO*NYOk|N_H^dgzC>0Dn*Xh`6yZ6{sokBri zu8+W?|ASWe^f-`6_~yvz?(h$k>FTa1G5M!}H{D~tQ~zo8%3Va*p%8^&rXWp0QoqRL zpQ7vOE#4<6*zODNewmFJ2A}sB!PLG%p~A-dS&H!!@svbVXu>Um_07Fr>#XgZaX3ko zT-P;kao-wcosQCq^J}x5P>FuHg3qvO`IX%*90}j1$88=l5*gN?Urbb61WO5#O7?h^ zl5aoew9Z7SsayKyYf@gSWaQBL<&|oDi@4@3uCYaJ_-mu?Iyu<|gJa4L9j?$K!TGiv zWNcKIT-9=5Zh1ZH@uS}E&sJi#Zs^+o%8-fgVZgG=ZAd&6CTiwP;3z*oZG^!8k6W7i zVRr(st>?%7wxXqaqOGP1J8SrNTfxgqwtybkOTgBI*V~A7l~x6b@iq%D>d4!>A!K+s zZ-z1aD?@|(j%p^|LlLH2gj?kf6J5}GffFqlA(52pmMNy=i0>W<?%ydhWBFwBvYNBZ zh_RMr-%2J4BNZcw(j*phshXZeHwXCngP)E46@+#Oj%>MIyCvPa#lGwAgR-g>(LH$J zG6p|?&5dfYu+FzYzPR*dU_*5yA5F2xmOyZ=eUWa}Y+o;x#@>D#&WMzBk5w|^&~Z~z zDV%7V3DL!GhVmNy2fzSC_J|IaiQFO#hL=canCtfi*rT(-2<LGA<6T95%U~?345SAh zi#|!8e~l)$@XYIuN%JI>n9w)4$83uqyWuKY7<T2^v4OYtjd#Q4tTxhg14&r~zGzKV zYS*O{OFVP-HC7o}1v(SDGwaqTlEhP~dN~$I+U;g1DG>|&bq88n6qc~wR-;kO3z18G zu6l^wm1`ffNz5Fbq*I8OU*8>cCE69&qqW^2$>)4@;peyts%4$`%yzjNbul>3K4;h_ zP+h(4YB#iI407bfWBVp?i3z}-%J-XXx2=^&`R(CQdYF0N>ow3y1#MsQX8J3&dP?!G zuUGw?8*h|uk_Mhu;6~voGw?LCR2BSq3W&w=p)4Tbt>Y1-Io!R5pzTK3cC{6&EdI+0 z*<Mzs$0qk|wkB6y)~29`ZtY45-JAnIa5>d)wj@2k*bFN!ViRQZBzW`{x<*mYt-5&d zOJ_iV$csLDestuNMr3AywlzGD6r49LOQZPEPnRDWd_ioGNYSuy`r78o47%KRtK!=f zJ9bZQ&B_66IE<9kmI)u!Q!OTprBprfYHT0q7%(x8a;LVxG3@O1UsY>LW2xH&P+n-V z9v<nBGtp}w2%ss&US<aQv?&cGyAp{Fy1&-$p|@89eP5k_%mqAim#4<-8$eF;hN{4F zp23axi<JzYgqIqHF`paM1T9ug&Uzr&P{XgvPkdw)DK}@Bw{_t^%PVmQ>BKQY^+j7- zZDID8=W1oSz-M2sDN_lsCIzU_%TOtshOzsqI1{Ag(UJW6b9FU6D*Oe*@kq~a0Yd&3 z&EB~u_?srjNhzRfk;n!&`M{X%RiDi8cAPOOo3Nzy9lB|!klC4p5RT&8UJ?7QZ36>F zwik+V#VNYiY{!GEa&$W6j}a}Bb#>J{?NcFtB8kfgdR31iKIXq%^5RjhkEPhJ=Ks+c zY_!K)*_<QbDRI1X<MHiJJ#|(lKLbaZCLqKHDl2`7k|Bm~7NGsH=nv5=pP)LmD~{ol z$nb$3FS<UQUzi1O{oNF^MuH}T3}^K7D{YdK(p5LV=rbs(!NQ{0Dvzv@pCsL_ZBJ{C ze{Tz@bid4Mu&ADLiX3wx(vjoKQ|*}zJ`n^DjCzzL@@TZHwQX_<mqGt7r`t{`*oeZ@ z$zplUiiDPZyn0t?52kL$!<BL=BrJ*n*huZ863m>I^oz*qsgUmVQCQZnQ-*aEj97AT zy-wd#N!(s;-}^%;Pvj&S)qhJ)1O-{zaaIoWxDrY9=(V$B!=U^B=ScDK91zE&gdx$F zz&nQC8#S@B16sj1^a7j!%ZLE2U~G8$sWYw^er?&lpW?((o<@w!TA$@bW30~Nr{xqJ zrs}=@deBa%_t>xb0a9{XnTO7fRRW^llxx4?(rXf6f#|V7>sth*gxU^b5WPn-sMXRw zskpbwh`G-SHBw_}Kf-ea8R0q<cNH~jKkoUiaBBKtTt<(<VC0~$NpUXzG8hj|9MyCg z@0+S8V_s=t$0K98&b>|XE2k<k73THIwNLVGiS*zW<AKZd?X~7;^`@E$jc(;A9nTs4 zJEM!AKDW1m!1bG)a<W?mS%ALk%1~Y4Eoq?xf<QQqVAUzsRWt*TH)x#eytpW>&kDUP z7?G}lq%~q9Bj52!1#gTyRlNT+X69QJf3)uRsy4@g39TB;L3RVZgN8Zhos;u{Qh8cS zQ?|$3G+H+?r^r7-=MA}GprEIi)5c}(7?L`GibgcdPJhW%s$S3MA(V?bm_4QdB}ZL; zeF}NspBtH>ZLS7#Uf`=o<B~*aCbK6Rbc3v4ZhmchecB;!Qbd^8GSP)_ss-I(&6#Lq z<zD$5L2@ILTdjg+rw!?r&f=Ac#I1#GU#xA}ztkpxj}3B9Y%K0O8@K}!-$w_YJu5fo zr1PpiSr&S24M>^fdyaKxR|Pfi=nq~DEpKx{IAqooZt)pnbw1h6rc7XJ{;riuV93rO z`)l>k_WBxU_k7`udT*$`&(IZ;3Z`G$S2DE2tk!o+j^1?D-)*B$8~>o?L=7}P-iWty zpy=X@Ijkc}*noT_uGo<kH1jE?I@vJl?lw~6%hJH=0YlKYTDLFHt%VR<lb*u#b~|vn zG66?GpRpt)c;$c_I4)My-uKGU+}T(%Jq|^OBST_>=@LPXS+lmbNfcT7XH>aoC1})B zPM2)YL|Igx*l7}`pwU)J$iyseEVMpp3lI@XrW4Agz>~eK<dpOaU=a^PZUZP+5u^v8 z-Oafywpi)n(XPc-(@^RbPmVjjt?)zR6I7zqi<8^QcPo9ufEiq%HHg3M+@6PvV*1Q* zhth1bXIGKV@`+1nyA_%mrij=wg31pJ4n|@5mEIoTS2)=(k&;{CZxx|SXp6#jbDcsN zA`ynNoN}b8sjjUTfv!n?o9dcly=RvciW+_S)+PDYaq@dUQf*C{$3`rg?$)9kTZARO zY4MnP@yvFtm^1dxV&EP@i9mTYoAW+a;~74HA)#C=?&YKA&0-H@INMCqjU!*dPlbIJ zM=?-eohr7WnDly7aGD_|M`<`(Tcfd@^)@G02N0PUrxD|vTADNY)~`6}cNyS7l`P4v zJ3(eCDHF7Mm&lpo<gUi!JiI4&OCxMQJQZ=S6Qr$NdmFZhU^EQ@&*3iFdjpPT6m4DJ zL3p`I1z!<N%1Uf^tqr>T42x2Z(qz20bnn*7Dr%iY5J&j65(>NFb&pw@pSqQN1ACC^ zK?CosH;lIQpr4)7wBGYHnT>am&sHZl^lQ}jbiF2#Fx=oLe#DWjdQ%w*u8L*fuOa$Q z#8nOJX}onz`Fu;8h*4OtnUnt!MUB#SFEJ8F!$}|5Cow{eI=VI8p4H<=v?@}(yGruj zcZivx&EW$u6x%54&HIyPWx3_7xPjaJvgYWt2fxbViUN>@>shkEQT@JI{kJnR`#pgg zt<X@pu`E;&q+4;C^p$NOxm<pu_ow9Y;Zab4><U#JQBOjJZ+hI$kJ})La2^M>2I33D zzW*LUk>DK@zVEFw>w5>ER>uZOFT1V)IBCalwtAb^X>tFPxq01UQCbdHtWdSy-HBSd z@bbnq#H_Lt)dhaE(iYEg9oKUq`F$6OaZxPlE7FLHaU{AEONMgV(+MB;h$L^0vq2)D zG+wH7s(EXvKIH@LRjx^-S`j@UnqzHi;FfO~(MA54lN-j(S%~kEN}t6@$nM9#CRWZy zV7czwS|7DP{6KlMCRWV-Ecg!!qMAiu7K<0}E@I+<N6YEuVMq+`PvJ*Y2IN({uM*A+ zd2+JT{<4L=IPO1r=q6I(Or|HBb3!jnC#oykg8NzAq?_CsMg6L{O-v+G4`Y$?S?m)W z&1$rn=oVWjf3L&G=&YL)w5TIKxtv0pO%k_zvhR+RPoSu(^O<Z#b|GeF%qWlWOuOdc z)!deGnE#R4DA>5T6tk_?g*qC1si@K?icgApm6m3iV(UkbO&|x(G#lnsiSlE|I@y(z zxk2)ZV#v8nRw|Hw*b1<er`j2MBCs5cSuUYlOqbrvzTiT?+@0tc#2K<Mk48A1eoX0x zDAiCtKCdqK(}7bt<lWGn0-l^dt>o^X2Ju%*FBL$Q@gY?~pLlFuPn_%$*(dFB=N}Ez z7vl$6Il>cW=L!N3|G5{tkulvGcS!<Vn+Pq>Q_)4YaC@yVM5^n+NS@(L=_fODE1BTy zJ;o9f8+565<34Rs=-Kn4#H306eX}o)W!hwRVYNE5%_toX_PO`^Y{hIS7il%0JDZiB zc-nO7jaK;ie>RecyRA@o-L0s7ect;b^?N1)obxndP;l_~uoHQ-X5{&x75)yr_ZhdV zR?1lm+S(B1gBn$s)z<6Vl_ir|z2glMc$7cxt1>BlC_7*RPGTvT+rSwaxK>bFS|K{j zbaKr0tv?iuv2>+4*lweDiX?|c`Y>#w50+MpS4?c^e=Ri=g~WNI{#E8b0`WTkn<`Ep zgsKhaPoa2L2%Ra`-LHfO;wyo~hgxRbQ&ANZv%=1QQSdT?o*ad<ulbA3hVeDL4xn9P zD~x6L6)J2b_|5N?VfFf+)4zaNO1H`OM4u}!Cv4ZQ-idkqjUemW7C5}MfQyirh%E|4 zAqjnE_;VGM+nrU9kC=>6%%?tgRz)AAe0;gN2Frm%^(f7nW>>H59M!Hwq1*V|(;p$- zB~92=3!RR#imc-LlZeWd8r9-n_3bU^$s~GW?!5u5s2O<{I51%+Nu*7n&qpU$>MQTQ zC-T?`5Z5xt234YU<kVTz8nfK)V0^a3t&tW*Vo}}lJbWWxW9dv+IJPF)aJ*;dD*k#* zZ#}oT_x6Pmvh9<WT*8rR)bP#1T+z;xDJlj@Tf~B#VseO_pTJ)TqO<~%9RBP&9SkKv zE508HeF#Ks$|HI@M85=>ZQT~q^9l3*3g=A-y+uXtumFB5$i8!}wil;C?<AZ(k==I9 zu{i)I_NgqlxKWf)RjPu2r`z{vYN{?h<%##z!9k2Tse<Woe(m#2wl@2*iv57MdXo3m z4+<8w2q$qB0+P0&6JBaBUe3If-xJJfjPUKBF{-Jdhw0$A_NWaN->j+fNgQvY6b#{0 zxYK!U9^7nKn_R2Mo>cFtmRL-yQ;G(MWe{i|W5yi!dY8jj`^0<mJ742BH5RxChesC* z2M8;oIjRN`T_uEb(U_aV+r)<>OZJ=FdN)W;fa-iV^`Ij*0S@NzJe0T3&P8R~*B{YC z?f21nJ1a(#CzJH@-vw2Y0`;JT?fgBqt>54+&8e>DskWGC>V<q}95Z<4%sv*0`|=sG z47(_&xx8WnaD@IZ6Y_1qlX7y@P&yu%{uK{{P0|3!3uDd!d5?;fj!Soei5m0&HdJu6 zCiju_$CW+L$?%HzjLFIk_Tf4j*$ngPU71Q1lY)rh;xc6Ur$A6~^%D}a&gD0izBxT; zJQj;GO+&X%6?JgWug#W14~D&DO_1;czE&cJ$p_wGJ>((}$rN@yCG}GS*5a5}e}AqP zTCaU@FFY}qmB?vLu`h_MuzG>xs+pY<VfXKl5Kux>ud#w15&pJZSv>X1x_FX`gta8f z<(gYQ_EC_xR8zr^`W+3&EjNCt^3MY1ei$(XzD-YGad<BXjutjCLaT2Pf_cFbw}ymN zv6sER-e9HQuj_SA=qH^#17UKjB8F}Q0Swi#nhm9@-^+<_E$T)ZIr3Yh`WmJN2P?j~ zR;VcHDKtv?dgM|=RmUNaldH&wQ~O@pnH!l}E7|OXC$9WnpfzM&Y-33y@{tl4t0+?6 zN^dPMNYNO29uNOyn_K%BKp9)pZCI6(?k50vH3CHxs053@Qmp*1J$wMR?5Nu+hh1PH zJCA?e_!88P=v5JpV86}_-7n5fd!Bt3pa+qE6+(R21&7~=30~2#*^|>?%e}dwCmK`& zsvc<H%VX82buYWQO`)?h8ci*S`3#AY8x94aS_o{l915ji;CYPV=#(t2qNa=TE-8=n z5<NR6I2=_W-3oJM>oQfJgPVI-q}yCOX?2KmLznad6JF4B+r~eivn?)37z&S=JuWu6 z<%66KFZf-DIIA2-DUpRQ`tfP{lEB8vXVjd~;7N%660Ls#=;o)c2mpfo4VEDz5O}yy z0bzSzzgS>#H8LB47d?ERzx){RqASy<TS@j<&An+DwV-$5P?0|HAx0_eQn*6*a2tj@ zKvJH~I*S-c(wJ$7Q^R3d7>5WgyX*BjxUPKL3<jH`XB-aqptdC7<V7S)7{ht*oJ7X+ zO;Xq5<ZrPxIP}|^<S$=WiQn8#76@BmD1LyfC))M~`+*a1kCA!Ft>AqtqKSB?29y}4 z6K~WPzY$vB{Y<b~R{q94(cl>zVkLVYDsfNhSArN32Vz7?zxCU05MmW}5Pu4>%(WoI z!rfq(oS@q7+>6&NP6~S$XE+(w|GqZ3gVayR$~ztC+d#RTq}D2S<}Bm8t&Xrom!Cff zm~ufmV1Nxi%(}2m%^WuB8br<Xo=WB;MM>FsUOp91y%?<+TXcszIcdra>I`r=BYBHp zAFZXMZtY8!q<y0dN^x0+U4FF`L^UOjQcB9vwMNNv-@j9YO$fu>UYpfZK9!UvFj-8< zE4BijHv(t7Ny)_jl_UOMb6lLQ47J;zd*Nz(tSi;)r$$al5x+xeDf|+d22ypjk_2w` z-%yI))3gqN9>)RbarC`nuel77f9vL=qYEjD{!?QJ^iX}R@J3-6KeTcTP3sBYgL)#! zWsWNiycXI(MC^=xK9qu={T_0!1$k<cqy*vk*z(Eir?_=9e#n~6spfiaqVHt6@^`&E zpowIkRIK+G4ih-BfjnDXQE=g_j4o#?x|T%_QZ%=6y!Du*$5EO1RuavHP7(AfbPG~L z*pZ{~>shpt?J40W-_H(<Y!ywR)8z#5m4;G5o8g?n2ETy@oPCGbS4ZIyjO0ze4N=F0 zFRBW2K73H<i}53``pKmLFw|%bfWBUN7IZi4wSt%3?>O@wLPzJ4tkBCKJr~i|oe1_7 zq)kX(VlJN1pAiSxSAW%WAa+(K>!k&9fDUCm-#dfbS&3f1Ob>(**G>peW^NP-xVaX` zFCnRcgV2Re>AQvC{|$gxD(2+=q6e54+W76y8DTW2sC9E3vrT~G(@j5w++&3`#dZDT zMCMHv1^yOO=$eiR6ivnw67~Q5;EjQrLe}Z)_XjV#Q0k~CjYv88YRyj>)64AuuUKK= zbp(L=VjsGOcYr_f9r3TqW#dOnU>=R{(nzM@nQ}-`@HnJ2YGAm~#@#W%UgDoym!pBb zD9IA<E~cbqkiEEGf!W(OVj7~qM)*2a6^rGJAa;faUv0hs&!!;XG8S>PdENR5VQ-Qd zLs=H!i9R>G>9{O-Rea)EppNH(I{qE1^sx}Aqu}bI#kpNic_FN6^|jEV5f-esw^;30 za|gm*0X9=(S-YSb$YpSUBSQ6Mfv{9AsjgYh<FE-pSVn(`mzabxFV|FC{{(Crg3a}b za3PF;Vib8_9z6Y%;C|%m{s0)k9%*?(0YFKv#YsJ$PvAXrUMlyNh5iMdDXC7bY%i<| zI?U6jzUW^c>Vs|MJ*+q>oP^>R7cT;>U_V6!u$j&wz%8wkhCNRM&jeM)jUPeD7$6%V zyIue3J_vyhyRJ;zgRreHE|y<h>BMeODMg)iPtrNaEj*po^$S}Jdb{**rx~GOK;+^& z<~3Ac<C)@~Wmk;8Z9Y?M6H~L2Cms^YpUu0#jvWI#w(o)BkS;(?w~6>JQum%pKn_GX zBRgMi7~dKetNkzJ3HT)ld);8>)e0R-+3aDj-Uagb<obr)06N$Upo1^(M6PrK=wQvC zor|^886L1>g1jZ~VAnYco}OL*LNdXb*K)A$Vj*r}3E=7arC48D2v|$}{seFe_+~ZO zF_|ybUFlUp$^-piu;#)aewzvmP}`uuoBEyAsg-vv{a>cHEc6iA_u*$QjRBB;$-j^a zy#oGzOv$!mA^Io4QJRq`{e~N?zd}0HQ*80yo|Fd0cSBmlFT4@6a(xtR(XGrEgR1&q z-&fpMI1AaGv8c%ED?5NtgbZQ;u}l~whHs9RK4v~FR&uoJ0wkyTKOE`<@(K6amxYbV z;Gj;<ocI%W@5e9QzXSB@(D??-T<{EdF|+eNbi#2!BNdqQD`3tiR@mNmH|i4Ewb5h# zK26AXtpP5=XybPtcr&Bu-Ft-V;yQi^Ds&atclfe5GA@uYoEYE<Jq3i~^uZa-Cj7hu z?AW8@B3>ziwCafPGC<Cl*ZHO>V7e1qTUy&=$Z$dxnDw_H^abfp1N}<AlW~x+1?X4q z!(iUOfTuPYa@-8i=Pkf=ulDl4aSvnO;Qipf@K3-xLcxx$xnb3-pv4u|)bxn_mx)O? zEd%>5pkH{@7(CUkT1npz1Hw49)ffy-vxKS|!O_b^)BcLb2!zGrN;}R1)0If=&_Z70 zxF=mGF<lV)g7kksZ{p#r&0cgt^TH6721c;_<D-{*z*9DopZCBfAe1$!C%{U>!H!w( zu&xgR4p>Ez2$5NTe6YSY<bdtZ!<<YSG1)@$z9$!iz99W2XS?KV=hS7%*#gq}lCuTF zivG`Xwl%!NNf?YoOfXsh5aWZ;)CZ~LuvcHxYdX1fn7we($!9Bdb#Ud%*FQF0=;yDi ziHo}+V{Yq_P5;)FqBx^dcJLSX@2mtu$;1|{d1vGYih1~Brn?&W581-XG{Cqt1zAAi zXWQ9o8KMzaXxV}X1{ujE5C8>I3(sr;smtEo+tk^?V`N14k%0l~Ck+S9%vT-Oc9kva z(b>2zMPaGZ^JVZB&SGW)BHq(wAl2aC_@^NL#s`WZAHG!#Bnjy4^}--qv<8u^Q|7Pi zk_}<Z)`-NiMFwHhJ@DG>tp(H)r(rznvVWsOdX?ISs5<y^l_K=J!b;zJZ1rvJROZ{m zzpgO{M9HDqxFGLa1PAWQHdGIIIayd;Z#j><j5Gpo-se`*0H`BYAG`K1#Gz%OkDw#4 z_NIdBNNuj8RE-Fi9vG4A>32wj(mpPyVL!@yTfkb&z@1ikVVrFA$b+Ole{b=8JdXsy zu4k)L=6O(=+H@b256yxS3z@5uAQjvW5{rBLMSNleX}1I-#THYjCLgdscablTI_{I< zg?+Ov;h4Kel-jj>IJNjUNo{T~gy}O@<eFDyDN^zZ)Yx;{ZHNB(VCaYM;K)ix$|Ap2 z4oFj14!wF8$Ul<LH)?{H?g20T{945ED|qReTVg*QT~VQzO7;H%Z(?YaO`h+0TT$EI zDSr_g_3czQ@7`*H2uZIe5my#Z|Fto_9ZH#Nn)fY~5BdwR3-Hb=Ky%pZ62Lq0r-ED; z2^#>ZAj9X73N5aq$ODO54^gafkb2pghNE`sp_h)tMP;r})P!Z`E~YP#-bg2b9lRcF zkPfBOjaX+MHb|#PCLaM8c=dGvRFQ7?*;j&Bik<fOY3pBr9aL~1;m<=1<J~@ZE@+G6 zf&7glN9;QLw5x*?BgRwj82$RP1*U*+AyAWiimF&>J4leRiv8L^#It}=LRwM#q6@su zORn&ryaoKg753y%Or6uf3Io`96SlRRHkCOlcVI=R*2<iN<E<A0NB;7rw*D)tn*miR z8ZKo)hIT49w;e2uCI&L7cguho@P)i^9RkHud`!aq#aD%jf(=u%Q`G>+Y#KV`H8@G3 zB9n?e?4yp-XVTx@&UBYFgsY@A_LTmbl7&`)WzZ%DTq+>-r>JQILl0g7*8X3FEz|6S zhe`0-jyt&t9)hWBc5N)?-ifYFAL3lU@!<Ai77Jy6p~>JC$N+ll1@MkYO5sHYQ2scj zA)iwr`fEWAu-Wf+n(+N%6JzI}$3KpN8X#NfWRVp}+P~)YP^#P!U4K3pl~qpA=LsDs z)Rt+5Ii&LL6j)T<Igg|REo){nI}f~5jNo1{4MeD9TR-u5S?FdUyFHWZj1>TekkAA{ zwDY;?>Ti2M86)@5qXgiwY4$pO9rpdeb-hTeat0Jjbt`wWW<a9y8UxXP)%pRTOdyT+ z!R*01HCj7e27yA_R6Onkx4`TH`(@g@E#V@-e&<kzg;#BSX$lhX)4RK`Z32Ae+R}kG z<AIV_*t8$-g1ktMmfPk378wR3zq?FP^*E1yY}lOh2ueS7djFS=zj#RLCa735*?<?F zXH&{U=Xemkat+`U0pamro4x^AzFK3v7z9G}lYkz2|1tU~9H_jOd06+Hh2C#79(Gdw z#VuW5>sQei*K}$*RwLmReBuCb97;zXPCQr^S`UP#lB#-^2Pg%v^Uqpz|2R<YO^JuM z0p?!X&4<t*y{@%<?j4Kn1c+EaJ!Duccum9R$-vb3*;d7y;KL6bZI=W?(N2M#P&p|i z15DN=#BudcpA-f(Oek;L!w1lZA7A?LIb~Rq_>!vp?`WLc1pEP}J}{LoOVjTBD_n7l zk%CqhJ~-c({&hyL`lcRAI(X{az|_!;h7*UIqE%Y2_dQ>kd!I2NFP|e?i77P?^ckY< z;!Y>)ebN!QQtxVN8)>UUzEZA5CK=_TH!ERT+`~7A?#c*T?ht17_vej>h(vCA#K@(0 zIq7QLze@QBB^-gy05^!oT@wLuz;It)Vk36sI*(x4C@W$y$AzNjY2YYXf(d(i83rnp zZ0l7Zh&tSTqXRB<g18ib)q7GJ_~c~@H@B0}G#j@vt~qobJ?>k0(@<O#NqlXBVV7#1 zLJ_ct&$r}itI9maT*k<xG=jV2PTHXzx9e%~x=e5DdL$>udwN#eq2pok$QZeQRy!xt zHS8YuG>j<fg;n#lW_$kwdA)=vf}J(7EP#N1-DWhNMpn^E3aRp#V|e@%=s;d?`i?As z`O^_N{AQ5&#T^D=W!rPNXx`OVWWgtIdl4oG082UjI=hc2hNHvebIP)E;#Zw3<|N{E zBsz&jX7zr)2G(A_-lB)Fy~RUM2=;*VN^;4V)gUo46%#HEm}x|YD?3SP_iht;jC!x> zTfZ{z%NzL(ep$=^3GQ6EUKSH2tgB`NB1lrn+(&+0a~%5SgvBHZL<6mrhwzg|AYmI8 zYxq{=vkl5owtuD`57Bpoa;~!p0FbK+;(7b<2r=<GoYnhrlQ;Gpyl+<C)Fvv@7lwLw zj9{&CZjTLy@<!Xf@0ReW%h(k7PUOBvm2nFk*#7uC^%}c~$)2cs4H6iPx_=>$JL3aN zr#D=lgfi4H{yJw)3r@>PLrUTNOKctik*!kcSv*fNT-6lc;Q)$Vo7=lPL8odwpffrd zTz$?9zIIL1xpn^TW0`q<iUfg|#pOL8DD$KhyL)Bf7wilA5Y<;L%#wD_$?@#KJZ<10 z#f^|%5d1`Y_Qp5z;{~Ygj5+je1{GVGJ{)5azTc0JYwobfw%iB>T9<0FKSu-Y__BNg zI^z#0un-;FV}+WE6&Wb&L}iZAPY&G1_h_1LE&QXFVyj0Z6tgQ}u2i%W%fvqKmIwi% zGbi7vxDC22>DF#yCq$>6ec}(%3UT-+fG_bGG{IHKfaABpVCLvQ$x0Ipm|juXqZ%li z$ba<P*~BByk9p}*LO2$-#mvz5`j;1o$u)fE6>M_TJ=y>F-lN)56V~h$orEqLX>O%B zFpYg9>^F#agEMh>51Dph-+&TL_qxfr_YBd@N#X$h{#NAoZF#d9u2oMD-|V*~vE_rW z6AF}v#_cnyxvG+=k8AQKYC3V{M4p9|M&BK5SciJfccordV&jF&8c8Y5qrAI(zB0D` z+>^eXCkbc~T}BmHX{6&AtZW@fnd>$>Ai<1$6_A0|?>2r2j@@*ZU%av1w+jUm_w1@L zGb9!^5E1lb!|Ho@YuU4XvoI7=-rSOl$!H83Yj&!V#dl|o6)f<yGjPepWR!h{|1hEW z5shUAm#(Z4!iq^)*r5Ddw!zE#YxC*_>!_tZoMNwm`Z^Ori_KMtNZ$Wv<>{ONcWRAZ zg|b>&Ba5ri6R4AhR#r&kQn1Q?M3d@%+lz;UZT-S7(D}owYV|l%?ROZb=QY+D2G(CF z;BMqr&(N@bJh$+q)B1CA_{T3mKQsZKdfWC76=3<!X%z)^5JbK1Y@s0!?*ULoVk5pB zT>~;wp{D|#AH#FYx8l1wFf+q!h#uMD<#G4m@Dmk8-|U|-i0G@CHpYVZds~60%D_36 zYp2>bxQIk+JD+kZ`m|mmZaNv$F>6_brhg{c_k$_p#=bopzv2^ml-aTb58-r4Z-bT> zv-jGkE6pQdis<aI;+z`86qcn`z(lq3=+vFk1Q(73AUNkSXoV5k>a(QB$efa&PQshk zgXDW+V|8oyO)|W+?Zj`pWU=1+N$a~5A$q+{>fV)uv<QrzFn#=n1n#d21iDqM8fU}S zMogWEqg*UPR!SNYg(2ED)B&RnFK`tvz`l&a`eX5~Q{rMhC@wp``um1j2^wCKkLcqW zc<H^;dF$-lqWavtj>+Qwx0xAHOhPB<eckCBWvk1+(n1JQTOSWUZl&&HwNKCORz+#a z!U?1ERj{{@*4x(M%fUi7p8QWN^mAtg%JchOh!Ch_5T$MZN1#`1i&KG&f)#%Fu@1PP zg_Nvjg1aN3jz4HHfwIEkGEjyoK~`2lUt`fZ+1i2Kn(+E+iHW)8^8D2G6SL6ulj}<2 zfS$QBJgW99R;Pu~p)X3@>g#*H=L1n{-e`Y0O}w7>*YEozyN}Rub)ViLRboJIV^lNW zc!1}4{?}j~$CDR}D?Z(g%n&w`I@I6P=5;41AWNAU2QH^kf>{3TiB1(1<Xmr$RtGs5 zgX>@EtjQU-w`Um-Wb}NptW6RV%R-;cbsd0L?2XdPBr8zRimmOhp{_>(5#tEPP*QH= zrhnjui$=iN7WB7x_!3x&iqC&Pv<a?fjHE{Qe<mn}+XBwv**(F@piydWSvIa+BS%h0 zxU2Ul?uGN{p;-P?op>F99vm=I5JSQ-CiN<`-ePL&8s)LKsR3PvgLU&wMun+RC{gJ7 ze%lGfb#p5wm#UHaorL6ShlgyR;wS}$Ryt6tjs=s4thCi|0(lCs98uNvDf*yMkAJF* zloAFJ_8<<jBSi}M?Bdbq+^}1^+-ggA-364P!x#vndq>AbCIZA!rQeUE2L)b%Mm=2O zS1uzLRrAJG$&wPa(2DcCBk^6dj@@-POnVffdjeO}Y#;qnixbaGAvt5cI3~gUF=jXq z^8qnb@J{s(2Q<`Nlu_jQk+^e!uag8+R9mGdjC;|y-DV>8w!8^O3t|yw;5;y-R?XRO zJD*P*a9~Op&;fL<B!jKGaN^l<3@p)lM|%Pj<g_9h|A{4@I9IGop}fi)61L3^F#LaO zuF<afevsR=@;0?N*D*Mrnyy!~p3hATq7aR_?}MmY#5J^I1(%t7zNS@Ogazl16?Yf5 z>#WD;(_j;2S{5}N45ngB-3HjR%unF1sX1^+zY{utba#ebwr|9Ye%ST<NqYMB6Kg?g z!f_0oVA{T#ThN`P>z=2|32Bl+>eg^Fi1wpF!Z~_WJ<Q0~m|+%qLp-K7NR3culv}Y` zTfEq~oHO(`qiDZh@_WR<t)ePW!kAHJktDQ;5z2Kc1GhIa_BpM<g6p#m7C3qV^fTJ` zJ>!SkR7~?R{PWQ-)CXXp?zoSIH5DL%uRihn8N%r!e{Ie|(fjV?eNPBKk4^<GL59hX zH)~70wT{;PrwxW5mx3A@j8gV*)C1krlC+ixC8)cxjr?(w@<8`Cv$)*N+1!zGG5dI# zN7(`BOhmp;g%rdqwzg%m%igj0YjBw9$O}O&YM0S{gj%R!khE1|0mjQ#1niNqO>PrR zU!Z(?{~_hGDT#&pLo?uOrM%xw<yAUrdv{HQ=dyHd;=fqlt(g2Hyy`N)Z-oSFEU~s4 zTzWAv@|BI+d#ok;6wBYxw1@0#ThyGATjCukj_hbwi_6}e9j(VS%=_?qiz{^$T=p@P z*-y_m@D7q_!heRfMR(BJwsH#kR*0rA&`h7U0C)<faCyQT19Y%|&woS*`Ef<_-6m+F z3n_^jxx!cHZ>X1bfpH2&^33eF7p=S+1te1DqJV(2AD2xVsPZzbrH5IxSh=^$#D(*l zl<v~ymON8qUdK#AJXTKagpeL*RH8ePZ^r*aJFeQog3}RFGqP|V;w%FKxcNPEW_KnC z;E#VlfY)6;{^bMZW-Xx4_Gstftlc5rWdBFgfC68*=Gemr2(|>y8?;T7PiZCAjMhGB zpE?M+`ed(PTSs!<{%9TPXaMX@HRk!UNvOjyC*IHdNzh<Cq@FFMGwtU7l}xOxu4IZR z>y<9EWZ?+J*$i4#q!qHO2rNqTKeecZRuG)ES<lvB!k{nJh1BzUDu+<3g5{<mO>2jx z+%Uf4Z<v?nw@6J?mo-@#C4Y03Kt^*acPP##XpfK1_1eb=CJGXgqI+m<ZHVxs0edOt zg$7+V=&-9b31B_*o5o@8$|VOJN=8n#b8gp{60-^Uy94J7`HnNdrwiQd_ycwgnAN`j zh*@1M=JeWcsPVWnae<QaQ_IF?L~effMYCLMb!rA=1`;@sH;)Oh%JR-F3-0@q+|M~d ztFAM(8FQXXhPxfLt5)GVob}<dsQH$SZ-GLyzcg%gpL^~q6*$SMzs%69ssjj(2`k`Q z7v-xIZeOzA<=j+z99Qyzv&78>FLDgPF$w{Hb}%`RYqa=}xJKa|)MOvu)I|0j3xz*> zphnsHXL6JPBpsr8);<AO1o$!w&_4N1H*6!AImrD(TU8Bf3(AK{-Yd8sFE=CMCAguf zZ*Y`ZLEXK&tj!u$o~BSMs9CNrTpUF<L$EAUOEoC;@{W_4)$z5-A}JEn?UEBK*wd+C z$PGYM_MO%qinxFnjdfX{yZJkcfs{XwuIN-RtL;Ndno$xA7NT7y)PWPg3uAJUz}iiI zf9)T(o2D6uS#a2`lP;#?fs^Z766|BVHNqn+@)5bN;XzlY=&a$}K5tEm2gfvt5%vL; zSE_E9(I~_u@lHd4HUmmRzA9}K8<4Xx1N|v{kzo_+qy9?%SlbDMU`}1_<uZ0rj?Y!| z&Oh^W!*g?31(x{CAin`Dyh=mevY_LIr8|`M)>|4b55$G2Rg^mJ%ddK4ZL7_(QRT2S z-P2rgn3GcsMHopKSj?D90)<B6(vj=%(_+f(?9u#=Qr+`4X7_OHg)`ZoS#*ht=KqOY zcAIbI7Q}~=S~>A<WZRlMN#d~@85W!d0DJ`o#h7VvsKoa6bN(>_gLu%<<*k06fqze4 z7i9?d-?0}}#;rqgh_2p26@5_z;PMQ7g0g36e?LgCdL%Yu-DP9a7tr#F={)XX>yKpb z5lPw{XN^3OyJ-tKw=po0`Nl{=%1BXtpJV;wO_(4~JT%&TphP4E414&pf9gvo)PMO? z4`9<6C^54o-?vsk5Y<%7?^h_E$CGanVqse3>974P#IS66S>q)pvD|hXQ}^C%_#=|j zE`px#_W~`C14tUrw4_LB(5Es__N};-XlTGd(UusLmCvWa{?<CuDc#Tq3kl8*pvl6A z^xcDzAZGIX2f)?7`{n#NJ<$C|fB&GY7EG~78Cj2;o$Ww0gQiQ+>1630*h9uvj972z z+Qy9PaHS4O;;|AzhoSaPoeoFtU%q_-4(QI{YewR#vmxzql1;SFFe1Fz^YXrek=M&3 zzsAplkxeHdF>lVMmRSHXE64sP#M}eK+&RnC<4C4rMYm}5_*aGyuZ2^JBF^$fM3oFD zlwlE|cnzAuaa9#6O4=>TG$^yIj}3a!D=)Pq>|3^MU*U<PhtKPl+2vA+0+E0knMKRD zJtSz~sH+&v{mkDTmrEe;;8zfJiqO)2BvtswPgwEW3~;EHX^^8s>;zbhxC{hB{T3D@ zBmS3n-|RvW+ENzJ%B=0bC#Dq5lw<!rM#-rGx$o)mH`O_L9VcxGE*S&>s`++T>1^nc z&@1n)f+Ar~S`;R>C}yEm;f2>PtqSKE=ltc_(M}b6I5~X8yGW^uA{?b%46N(&IJ#0d zuT`Tp>fovdWu^muflTcVFy>C5?e4Dx4sz4@<9;7w78X4K5{{{Qev{f63}}6HDwmLI zU}60Euwq9zUdQ*9RZXAWv&@97L}JL(sGtF~#vT)>YLhg3GLSVO70noCT7d#}tuh|P za!P<<gT|ry0D~MYp_5NBF%|4MI*v)IHk3NHPm{}&2PM`{dgKy%v<cUcNW1h8j`#p% zo<FARK&jgA<NRPy(Zb(e777hl2y0vS89LdO`1`kIrg~%{f&(9Ld>24I?>0P*fVcyP ze?J{t`tbk3Nyw7K|3_z8ORBP@D*we}&m~n^Qk5lD`Tu_uzZ8I$0?>bvwXvis|68ew z(f;rMk4+CZ;@4f(2sOc~!PiL~P<=%I@MdaI#yqSg4koXwMoaH>Poyr~n>hlS{V7N0 zINDl4zYlD#*gDO8g{}485E<PmhgS_bTl(nN#)tMj<MUG}omBu}tj_No?p_PZ;&;DK zQD`~%{R!-Jn5(3?g%iAc^^9;EKu7ZMvOAHsO$)4L=d<9Y!U9%_U4@{Y)BpXlh~V@w z92KZ*V4`^_IdRraPsdue3G?ptGo#v<6Tw@<gMvSCv<VA6&bx~N67YV-x1pk8a1Q+L z_p!6tt<#wzs7-fl-uRZYqcN1`IvFr2=a-g<P=RlW^Cjn*{b2DoR{njZXaKyv_xE4# zvhG#+)u8D7B8#^+m4;qgp(m$itC`WXA<Fd#gLzW2F=zsCzqagT1w>Ym`oWO<E%KS@ z5g+lgt5Da8sD+x)ai#rkBVQ0LmxR8WTv-@Zr5^)|-b7w_`+Bh8>w3Juuh(X`pLo^n zK{VVP$gpZYDh&&DzeC#0bU$#Q^F+fxyo~!`cW*vbFf{|G1$&LUBL&O6O-CQ!1Z!UZ zGHr3JGt`9(=r+4AHSxc7z-p}BysqdAH1DYMK$be_G`0ccHUCcuMUsGj6!37CNM^pm z02>L$kWS|aemv8gQ0Wm_eMd3AFCuM$ji_n>-l*JY{t6ub!Mb1c0OmPp2(CK<_&%Wb zdf{Xm0tcQ30>0BjrJ-XTdBbAI?%hUSH3VGBiLGYH9s1H5;Ei3*##VO#$xq@q`})E8 zCIHIUItQNiHEzWnLx>r5`!|VArxrlQRe(OGzPjUp8K6OCzWLU@I}fH28+CB?@$sP$ zmy<u!_8y)A6f411XnhzpFjfD(r@TAV1*=f$VhXx+K+{o5T$VopH1X#ezVb=&<#2qm zg6J6q+QolJ%Y`ygbiY6sqRFso+0_NWi2==x8E|Qx1zSz<%4jjVT=Xb{Y5?A>{ztp$ z{P)d4EzT&w9_`SpIo9(GtT`$Q>B<MKdE+tUT;G-;?ML5M7<2FL70#uEu+IMCH9PtC z|6#$?zFNGfdrw}^UNHxic*_NFMF32ha^LvYAp;zYn>)NJJA1jvW3PnQ#`|pz%Ui&` zHw$D3(AE14Fezo$1%wukGYmp+^qF@+6G~vWThhQ?F-Cw_HaAocv>`>Go6wcBLZ0f< zWu$dTSZ%)_v(U97o8E4`aP9p^Lx2;_H8X*2PBG245iBdX@>b&y{mVt0KDYivIt|fh z@&mppM_F-ty$*i&wd-9lZK$pL&hCyC%XtI<u?xPn;8q48o?0B<rvEj{UL$fiO;<Z) zQ>oxb1G~1S7XulUvX1}#OU6r<T!j2q3$b@?=o4P3OAmqH6XTW8{dPgY0>So@5o)eM z04k##Y#EcugBckhcLFM(*fY1EhSu<|ev1LZo{$>YL24pL?l7!H7eJj4K6jt%0G8W+ zpxU?3!d~73s=ejma@B)v>z5hf>Q{=pfnnm{4jbro_w#}M&dtXF)?|_#tfa-I8vT6) zYHdH*pYRJ)Rz?ssSIo^+0phhg({&C8T5RB%o*nDJbtz!X$gqMNrvmWim|sS@2{+xj z1_9sYdhHTZvg3cKZ|J|5HOTt0EHJ-gX<19V1r2FgQo<$M`d{cWNL*}=#DM;~5Bkek z+}vEfl}G;G_lILgi1=0u{y#%wl~#Vo|K+5$n|nI!)atb#F^8>wxsl%UK?pfAJRzvp zJP9t|w%N!<rnfI3(+Bd8SC@;>u3n<ZgGX#*;HlS1V(|}zHoAEg?w!vVw=HLA3}lQY zgs{qnR1;*lQC9--+oie0Gt><~CloUxo)yiQS~*e^qvlh0Ug3Hk$sFGi$lJd*epk&_ zchL*j=!xKlO!vpwF?s5~VOR2YVoz+D&%-|OOvATcV4TzEDEOz{oF+Z|_~+9QmROb9 zFMeKRjV8YMWjmio3!^5~IPY(B@Qxyv^ac<tggp@{!sZx3PlWAgeJyWi$%AbNm%mqc z%ACI)>i(wtD3zRyoHZXYuBTHir%A_aXc9crr1cKmUm`VX`LGxp892Q@3I3!@!j&Km z1a(IdiqoCER$cH^YAPR))P~(kx@!uRUY}xH>Az+_Ie++vB&7FAb8jzg#?odiS;mrO zEV-$rn6VTymIC-vHno&ZEv2?gb;eSiu~hUeRp9^UR^Z$XIMlLb%edSJS1#2r+-5f3 zg_>lyTZ>SA)e}>U4#<if7hay;yv+Fg=FID9v4t*o_KR+1naOVDRbKs4=G3|i#|88* zpE<fq;5mhF)lHV*vmJ%~$0Y=`ZpI$SF1W6B<;0sYM;Rm4OFP@G|8gX$<B=69Bs3mP zJA`FEp$#V+Ivzmk(t2<QNn}kDDtY(`LwEFu6bIjrf&h2VGrdWqfZF%ipyN;Pypt>M zwV2sDbh(!PR*4&}<l`Y*7!;b~H(&ziZr7QI==Gm&ypQ5v*FXg5je#Q9F*rj7fJJ1} zUwita`P=Z5vJsnKW0cR~+f-H`ntZpS?5ZxU1l!G~olwldz8)U}GcB@1ZZa<(^j6>% z?{d0#!+j`S3&2n+dM!VW9`sSm8AX_qO@MJv>Uo|f!)<-}>Z*)1xA3oTi`PPdgM%Sq z@EY1|bTXP9*(`6zG7|Abu)kQevA|H$y}i8^d(s;lH+y=bWZPm>_zNxcS?y-}uO~jc z@Be||JPi$DbuK$A(gIeb)}3>P)4)3K1b!Y${{@KRl@xi6=`-HC#|~xxcSW8PK(>Nz zo2i2~{e(4|#c+dHscV<>(Y81r3$ugSIs1rgbSsMyo1d()8bEa$&-m|><{AWsvl1Dv z8hO9t>rK>toO;YpggB&VhpJ56f9@Tp=6H}jR=T%;c3q}h6d0%}<mrizIA^$pXFXBA z>7L;G!go#MiqdU9avIV<{=!B8|80LdeO#XtNw>1X($C)k-@p3hlSQI4_&qSkkA_~o zbLBWZ;+qC_9S^h%pP^k)0pH!Q+xz@>AS<5i-J9Izw)cj+@3j^42ba$PF2}Ps8hrF! zY&9jq#CtaYy+S@O90bm7mz`X1AU=HQ^OvL#e8ZCRESc^PCl0xwCC{_uIG4iKQq)~a z8kSPFrHpeSDPKyMm&%o;YHO+1T`JX=kcK6k=O51YQn9#HEG`v`OU2?+vA9$$E)|PQ z#p2T0*3!}3(#hh|x$e@j^b#pxiPNw|$yg%9%r~$7HvuK@)I|J-F~POdrrc?{INv}z zhE_rD8FbQPakYtl)6>tnBgg7;S%2CVluruT6tg-+xTS*(ERv&^@~T!2x+<z*<r7k_ zLKe4kO&h*ADT=iEJK!1(-wu%0x7z`zMnVeIRq12By0Fdf)ybwwYgQM=r?|{hu`qy& zh3~qHT+Z-?pZ)q5%Jti~w}SUkuoGi<<}zU`rBEgj180Pz*qx6^%m*Z$(f79O&$xZI zlSquSf6dnv#1|6#F7&%YLVy*9Pk>*op=uPq6-^!#Txn&6sWMZO<KZ<2j4F(1c9h9R zKnvsf;vT%Ct!GuOMJE61!b~Btg$rCjXP^42mTpTxKeMzdVqM^%Z&mS004i5h+wC87 zAH+7%8>Rfn(*p3o?sgOMjpzy||F0T>%xQw{^eZRaUTCL6>i};I!Eh}f+}43_n;!Ek zGy>EywxD^l`09-#vVe0XRuH;=wPvwi$h8{78xz|u1b2WAL-*4eM}7#!&H+fn!LA!` zuFpX$V1<JBaCNsokjeratem@<u5-Wi`fw<Tt&K;QQxiF5V{&E8gT6{AQdzK@e`9`% zY4@E!px-J!wVWvySCWHsFsEwd)SdKiB<uwwGjHpISmTg)8^P8bX%m>`@@p1jZC3t& z-y1jEUMbF3uDAc7MbIVI(-a1{7h}-amOFva)A}I@K_CcAf`7N8J!+sd`|&W^11N@V z(H3$!@9K41JU#UuqGsPqbUEdQ*v)a|csu{$$TivOb0**(RcO$XTJ-YjCybhl%SF)G znZ1khV~$Rt!laQD>&4&;45>&#4~a(?1vQBT&gj&Y1rs@)oS{4=ZY!65q%OUjZkjUb z8$3t4Bd>15KX%G4(llj?!K}7mub+TE=!;7N|8gEMhPv1`scl)&CZHo?PlPH2-AweS zV_JB5J6oK+=LgAW0ZLyEZ)Rr<pvJ{Kj^Z5W=$sq~Uk@OCzAH!Orci_mtY)!MGpTiP zXlnQd-A~=v5+oxnm-A>)gPUezODrCU8tgNaw5b?|qiA!A^d7HA?ZdJ2a7rn(_FT<b zv=W4k+UbJLBWE)nIl7_jjWYWz70<q_rWje7dF{}*Qv-XK**r$)1y4EY?jgAw6YidY z1*IW&Wm<X5e?Y6DHQ=l4&vYjN24!-!cU|-#9(Fvi=`4`zUH9E{TD1R6uSswxP6;)^ z<cz(SqxX^MDRCm05t*Vu8o1mzZ4k?DfmaNYYjx>eap*vQwngy3ds5)s{CTgnK$KqH zMF(7gC@t4u5w9Roj(tuL0ip!jZF|csU2ZMV@=aqOvAmr-_v;dO01D$J_4zw6o6hKS z-0!~1%m?<B|H)(TeH6TZ($EPoKvjzS=2q}dKePzVc6ubmYL>bX+)E1PPWK`uyL3Jc zm(TbrpoUy42767>6BBTl?!XqzV6QJ`C`QkL@eISN<in8X=&SV*d9%?Q|2b;{?&ctK zsP@I$;^N>}@~$?#tFw0W_6Bb;PfFh1+sirna;Kh<Tl716ad?Pb=dE_>c}D6H@IBkp zavFv2qp-4NuYf^@RZ1m+Pj2kJ{bM}0*hasZX2ZB5PClxf^)Z^AG$_yM&5VmK8y;A% z+k)(2CJ$<GsJ6b7WhUIW+-r;|IH&O@o-s!h9fJ6>!=-SCR^YPU2qq7lgIw0PST!Z! zvVa<5)@GhIftaj2zCB1;Q)p-x*mVprSwFbV-<<js@H9I&np!yVf5bS&bf1^z_Ehyn zH-de4@~oV@csJB_*Q63Bh3mz5Vo@>PE5v<f5!gPRpv1}epZg8oz+p~50<QDD9H%um z=<Bq0Fu_TJ)p!Sz^ZxV@cH}`^kbHh2CNDO2QalcFQ!jF|{5MIzz6U77W*58zo`a{* zvnbZ7*E&1{1B!rgDB^f?e(s9^*pS<gOSNG79G%4RG#&;`c0KAk*fZ^$Vo^Whh)`$> zCqBi-hI&g$XZ9`kJ0I$@KgST$J3_0N^HmJqad>^v5G)~&9jTCg+o|N}auJ*o!mJ@| zS!fHOE9V+y4hVr~O33xZl+}er@26VEcy3ILRjrzj&Z1xsA`g4}J_=zWg>edck5KUK zQ-=+K{_HG#_Cuf5Hu?2-Z`hYY;nVf<pVZkP8Q8B!t+p)){3H+d##a~WlMnR=eBJso zD(rCy_>im{PQ_QC5ApYIh+Gyr1PH;2F=LbKfZ03QNzh-}A(vGfb^{OP@om-oWFk@E zBqOE0O@Y;$=XLM?3k5&pybe&f`JRgXH75$U3%wi^6aEN`Y8jq$oO~(M10=?ugQOsw z-i2i9w{!_s&yRh{1^eQU3+7iT!sOjDn_L&S5A2IfZcfQ2=_adp?}NO~NhAXjF=t)Y z3cW3E8=lf%CIah;?9iXLp8t!z_l|2aTi1q1L{S+<MMXskBPt34Dgx3H#)e9X4l2?? z1Vl<edI^DHkg)(NN)@7mfQl69QWBJrB0><54xz`;0t5&l?Yo0cIs1Fh+1u?o=li|C zk3aT)*jwdU&sul6?&~f~3ch~b6X)k(EM@Y=k5AE7uL@fN-t{~8qYH(`L1C*pS>b-s zB;L(v1={>8mI$)fj8x*ljK}qcMxp(CrT<(?0W9TJ3vPOJ40GFjNxwx=;H{Bp!kZy& zxFH^PLr>MiGX*3>@pT5<G^<pgkCXutV(1+F<@)K>u!QBm7Ci--NTYg&@Qc0GL}6OM zm(uf|p9Hy7G&gckhdBSB<|qyKspq~#aV}SrJgc%`5QT`0&-L(762ASvza+d+Fo-x= zK+Y~1kGJr1mx(Yl#?^=u?U;FZ_FipVzwm;!hH7#3|Ne4oQ|lnO55k=piJ6}tltaxW zXxmNRL$g2DSa_FP7$awyMD{0)|L1XP5~3~yn&vlu{u)n3<j2n?=6w9PM4;td51B|K zD_C%)xf4_UJa)Ra-9$Edwyj2-FpO3pdCx0`b1TFaq^d^>%GyyodWlk?vA55t4C#X3 zA?3CYoetF6*VBXC-Snd*Jjz%u&%Pd1WJHve(h9wpzqPQrg9H}YXZo=POWUG=%_Y-A zY9bS-pCl0{mo3!b?*To%zqP#-9@Cptx*hqe$nP7{3DnvNukSP12ELE<-7-Kl9=j!5 zp>5*J7$PoMXPleVIGC)`<9a&W^s@$yr@<BOdWKjyWfx7s05;BVWGd8x6&e-V&8vSc zMBu|aGWR+$mwnxWgWvW=Kec*dKr5^dB56NZ;*vtM57@$l4D8TH6)yq7x=r9Vlw*Is z4kklU_um3{a<~!o=uB2K%60lj)L09yeTrhvaH^^7<Q(`@CS7!)d*Q$qJY#JZ@x#%! z&5FW>g-H5vJX}+62Y3n<ulwUsP|qzXUw;+=@XCr27r8kkrd|QI$`Jb@Em|G#PjSK- z1Td*QD(+>;+;N4ys0RMMwLv%F9CS<O#AAv$VXOE3tX+78Z#I2>^uoUjYKA>9xiw7N z1}mXl>6$lrJ$peEKMk!XEF<w73r2D66)EU5ac$B`2xOa3X79={8B*)H>PNJ=+aEjY zE*wm~MbG_PBNXJv5#p41Gzr^QLz1KW@&Bms3_vxpqO6w7#(=Y{kNCWIkOf1TXQn2i zA6T{m-YV4;##{$a#NDT?OiL3=%JVQVloe+cz8Fdr7)pHJUFVH}f|OJ#qD;m%Iiiys zENRon&v*5;rhvigFcm`|hmY0a1M5?1KAfRTxc>R{SZRVeAADubOu-PZ@8RT8-b;2P zx?f{w1^B_%u&!1X^tgPP^}+$k;|`tZ1b^jsD=vy{l9ZD20C2?VpHK^{vFw1~h4I5` zYzH}){}Il2*NeWHgkVnCNU>)k+jj*ZD9z?-gY}YG{7(`$`t=l|{53Xz;_Lf)TJ!lI zWsX2=^FIni$aQ6!H<n@vAC-|HO_bn&RD{PA40v-qR`!~6V$-FDN*5QrT*<#9O<5Ih zXE5_|sA6_qG<Vzy-Py?Z4O%L2c3n?byKBLRF4=%scV$mcpYKs!vvra#OpqG|Tv&m| zQ6Wa#@YnC;m$)*<1b%hr2+DVMR^>!y;G}{XAIMu+(WSH=Jp-SW@=#6P06s6)(l9{2 z)qKgMo#B$#`LYGCzu@@gU@?{+Gdl+d_R+og2*3F%0i67wLG+i`c$=1Sud7%+0h4fd zz9R8Qc*&LvZQE9^y~2+<w<L$l0DLigv~kzZPC<{`%m7E~J8LaSwwC0EJGi4-gLk3p zXHxDTedCv3FF8>d$XShfa}xGTY9>rG7>-WyMYigC$$DLY0*a@%p9j>RsU>?_utVTz zPBF-LLTBI?`TpEK1aLs5&&^e{-(dSf|I)S3Z~yXwRv7%l*zs2j85ojsnRvKjxE9u} zlGJoI1omoG?r+Dk<n|vbhRD6Edi6piBDmrg*r63qHGnvfLW;HggLC=3gzth90_j+V z#pN&V{-W?Ks_vrYS%5u@hI7$vEqdMmVbCCzLoEMe7QiC@S%hzk;2CBYEMWh_W&s4d zUMQM35yZ=4_mvY1HQ*xI8U;>6QjIm9i3n0pbg!HmkS4#<n0baaOXN1C0vXh<7*&a+ zqxdfTuzZg{uP_YL3@NvlgqQ1v_yoxKj0%M1pC>@X;8h^cgm+!_wPCPJf}n)Y^xeTy z8jdzq&%*VpY3AE)-{;bzh`Q=CtB5bv7_3YEK45)0MAeq@0>IzDlF!k2zzH+D(5|7q z5`vf0-GzcHOm!ecZn@SbKlB*#TsnLCM35rPEd6tS#<bWS94lVjRRN~0%3U>{!i2@e zU4U>#_7sOtZkKti^CqzZ0({6vX~pspBdRKUCSDXL=4nujNL@XVK09F-Zf}GvLNjjm z!x-j0cX<r29l~Zv$KoJ*KMi5!aXCs}{Ow?tV>Lg$_Elt5Yf+gs@7;Vcud6s(TLSAn z5;4ro2r4re_aB}rv6$81&Ce@cl#PIq5-5lYIWNN8(;)gZ(epp@C>b!OoYKtkz_Pc% z0}<zTbx#Yz4%A3}^$tWWLrzo)Md4XYT(SSeQAFpIwQs(|ZkNi)@r%g$SBM;qvAyVG zTlBaqFl<@jumBI!8Q(Z~Ha=wEgpnh+M)Ix=uUH%N6Kux)8ba(T^+HnA5k4bg2OROA zQ$Iez5zMNn^qFgMyG)vL2gqBMl+uAVzHo2;2R_4;zDi?OAssc*Q*3HX9u_d8Q+!9K z=IOM6Ae;{;E;9*o;g7=Lt;*rP&ma|jmXdO_jGq(3%M5yfsLo{t2K!~cJ_+&=OZaO3 z!enD1uZy7&p)o&k0YP#z3tTvaYb5&ktH9V80bptl!U^!Ho-*zU_@pvX5JB#-BLpB= zwfJ*B_!C5cmy{CYk6Vdd$hY!&Gx&$|z$<WS!wNwV8Jm>km<1>F^kjjLdhjHGPe54S z5}^5zNeVQ-PgoQGLnL=A0aDS3QH@*TOs8=C11kKL9u0Um#fRx(0#7H+nG-$FVbcwd zvtdR|PzSIA#MR-M7sNmh*9m{E9%d;8f#h9&#p~kPIz$Zm$Y1P9q+my<?yKXky;eM= z1oGc$4*bd(*1tG|fA-iJ{j#?nH2#;>@w5HQqwvc7SKs|=69BIkBC~q<+Hdvc)fsjH zP1hJXG(yjoxUKL8X9eJ@W|0IlLs&u)!B@JcugHtPt9JbS{?ZzBJU_X9@~c_k)_Y|_ zsOi^(7Md>PD2eJE-5nFIB;MR$xOCTnYDc|Dc!LT|+$n4;x4`lV=o#g9e-JfN!H}(P zpw<HaZLpR9>J|UatB(@r>Zf{$evHqrc3~*^bzf^~q{Us%PdNog8V|4R9PP~{fZ3b( z6?Z}c)k-mOQ{1g32|JGH*&DVD=KK8e)qJ4ish-yiWTD11*P^vS$|vfS;JELQwwxD= zHGaz0Ar6$N&<OUqR)RY$3xs!O!G|GHu=fNd6>9-O0Z^<Zi47Nli#oCA>y3=yIF7-o zk%~Z;Bx_IYi(J7s@A3={TH^owo4FsCGUWJvuweRRqRslxPLF@w|MRaNNRmsh@F(p+ z)GkO)SwEa;$;$uZ$RKfpg^@v)*3bXx$RNyJeqt>)kU#inft+_*Ri)iw8f}z#8X0Gk zTpRgaP#U}_<K|p3Kl7W|UJ=KyZP;Si^>F3dr<<}4oYzau3JMH+_Wjwfw$gGdtF%pw z!<dQ3k#U&X+L-TxDnVMkMNdGv!scg~(Jg|erg*CdwRgV@vK3HHi90*|^hjC7xn;&@ zceHNW9M=UC%f|<nf0<Z*1PDvPa}q$duEXR0fwydZ66l{>Z4*26cp%9>s{Po?wS_wX zjI8<<RSd8C^v)@3B_#9lE+6z_zYFqRB5p9_DdPr@*vDUQ{MX-!fQlx%<}Lq2<hb|) zO9cLL+}7W?Qb%2-1+Fyg*+1t>d40YWscN6eUp`Hmb!rLoN@UH!+N0Iljr;O$te12- zUdH5ZT#NGrjPc<?hovagPau1Li-(EpGq8QbW9Nq8aq#F@Cz$egN+O)>m4^H_F|T}9 z;QwOWR%^`Fg>hTr2LF8A7WW_KRar&~OnulLZ4(!IFCxY0s^~7Qo|}Pmj;thiR0fO) z(CoW6fDw6fT*XE}aCn)c?#GU2@8Mx{{ok-(TFCZ>tM3{6rat&PI9CX+_%B9$WgYmq zFyc!&MdqK5_+sB0*@7R2f|=Dz9`Lg`WxJORB<ac591_`u(93DseLApg;}QyMVbU4+ ze@J`lK^9~Fms`&71XnZ(x5hj#Fo=c6yYK%EcK~9Z{4s`RG?2t>?!3Kn?SGMH0OPQb zXK>U#|4;J_%zq&2gY>{i#G6LhOMsEEeWrK+yC7M}^kxRm<?umeM(6VQGhifEign!w zBOwMx0+V_W9uKyBy<^`6Vy=^RfSgmMO;3NdMfAVSZB(3F$ZfzT{?ptBT_MBH|2fnQ zfz{CG!+`h=cOsNu?Q7Jy?|-QFr}#Zf1Plcq(kvEIJS=guSF4pD15y%|FWM_Ac}4-Y zuUp?n-qKn|al==`G#wE0s0-EvySouX3%<C=70_7sFL%Mxo){MQTMkaz?xKy`POV3= zU|MbOt36)}n;3NA{)+r%*KNHZ<)r><$(JPcXHnCJIOKPV#XroKQ{D@jd(9}tXAkpA zT(a_60l|zFhsnf+l8afDqHE%gN8nY`rtSv2wH}#+Z7ofBX9^G7f5$8T&p0R&u`qG+ z*&0ZMu05j)Fm8q8PoTL2*}C*IQy&iLX!`#q!tN}N!p-8?zl$O(r3n*}QE1;@X;d$b z9jGPK<B!^S4iBY-C#Rd%dtZXD#kW@Ri}qZOmQ^$@)VP36bpJ-1h&a%%E#Y&SS)#zh zMDaKl>;cg=ykSFvEwh2dlS0p_qa^Qur#ZRoqcj}xdd)n+#EYOH(#li(a*<9rT#w)* z)`5ICYW9t8RQvthY8+QQiZ<6xyw*?;G0pD-+N+`d;*eI;@jTGZt7;*F`fx7?XXkDd z)FA<mP)_0F2r#4b-)Kg={VI4p={OwoUS_~xb$L-?z`+NI>+r=#_z6o+*stgVLDFEs zIV*3l`6}lJKGsh-oy&JiZr^wWG}V4D?ugeu1=dphD}be;`V!4=XCj}m_e0^{I{tvf zQOq&_n=3W=T8ut9AOudd73^%g$(0$tX^Q>&aOXdmCZEhLxSn2-eME*1jxwddBlAY0 zexUgdnQ1HJ6c+{vM~hs5N`RN^UpS-r6#?rb5$m1;15z5N55-D~t6F2?Tt5~l+KC&q z8Gm*5I_n=-LL4V?88zfNJa_(eFSk#eJ2f@jLLBZH^bl(ixyN)_2IfZg(eTt|cuE^E ze+ZHR#Yk)2uugJ+8VI40VSqS2|G<Ov;BUk~jF`y?cB_a&=6pNK*X1c{Zc4$NI~N^L zJGB=-c?s6CikTG+f??iJp}G^ovpY9QD{>)DC{0LC1QtLGR8;U*N$=kFn0K4Bzfmz- z5+^&lx%q>N=94CC*3h33hg9~FIVgYLHA;dPi0YZDP)&Y()}-8g9=3%a5<lV-68s=J zuq*^brD$SV_uMq-iSW*UBWU@%d2}Gn2^0Dlawsl|>|3+nOOu{4s7;=-t`SBKC3blM zE;9Mmza9dp6Hpx5#orkyl3aU59?Bh{jt`(-HK>fdk4FJY1q5d6%CC;p|AW9V+Z6*k zI&iiNYCDy~=<;hkgW*QejJb63Ts+2qsBaz&<&U7p%U_FoN<(eiEiFqkkeHU4k5D`$ zw%gKr_|2y2IK9e>bNQCvYD26mL`r#&2P1gTTK4g3Q0(I3Uh|pbndaOoF?NB<>ao6g zy=~mhl29!CUC|F1u<V6DKK{{iBP_?z>f_!e0?{(CoSzJ5ec#u>a`eA-ISv(w`TEc( z;w%<t!+geLrAA>m?J9C;LVxxD`eFzVwiFi<9z2L)sQy=lrw}SF6rPG3{JVvx7sT#F zT#!~Sa}pd#>gbufdY-?#gCChIK?d{}3hhTRFHWRc3#h{%l&%0%dYJD{7uq8LKAe#G zM*T2}eR*rd#OQ_(zMNZ8;y?_E?I5?lk@3E+116Jm&#<Kv#s>rVdan9x4T<kPmp2FD zSLF+x@o&<DVwn?8lHgW>?Ro@dG94>^QaKOkXk$m%4klxHNX2*1vH_yQhfkIsfG#~Q zE$uv^g)HI|UmGF3IB%YS2l46`+=(*M0}kVAuG;P_FiKsw_|&4JprdX=&|qpU-(f!a zM(J8S(8UA&_e#V6zt+%FTYoZ#A%`K-x|7PWy=f?3j~AZaK+f}T#&O!9Cq)+bHuG~y zauf-x_2>flvh;^Vo4|k=?->p*a|Oc@(J^-&P+-9daIFj7TcA;`->6aD4iy+Kmv+&d zmy+qA-1%qyXZ%d66ui4+>Hjcc#v(Pooq>`~4K!E|AM=1N{)d~OOV<_h`y|e<!vQDI zqYU2LIk-RVBj1Mn{_S5ZpTW4Nte*}*YGGjeC?@wzlTc7Uu*`^p$<=)BdmY$F;Ts}q zfHU7^eIv0@(sJ9(l(x(1p)tTDNY@yH55DsFsfa)&kZ369esO+Cvjd#)ru7K-4FIjJ zz8zW@5DK~J7Oa#(FLpC&cxJ95mR|$<85@#YC;Gf|Z}A)Vc0_&H35|QZz%xYWx~^H- z8l(2)+PvM*W7G=)ZRXIphj|V&;TKXRX5a<PEK13cLMkmKiDG4W$h&uFEkLlfG<{Uz z2b#Bm$~#dl&N#4yS>*?T(hHCuTQjE9`wnh}a`S5g(+og|(Z%_;01sEeiC)n9ILOCk z`YCF2M_<oM-V#xV{>g+#t3Nlv!KnB<xOhH?sl`nPdSZaQP<qM!qx7==M=j)VpyRJe zKn|SUY5@^yFT7CRKS%|s$r2a0kc{ve<F_2m>Rf@vO~b~1H0Zc!2qI**QI&>yJ*%NW zbe5HD&7Wx!=Pw^9LIO18L8dhvvPP9o`pgqt&EwUWW+=UEx%W|F;S+)L0K&lpIkg5_ zd-&YVXXsCS8RjvuA(~$qJ(!Um3#~mrLs+o!jE7G)JR_g2;hxFTtm-S*hoqeJm2&W; z{BW)oGH?rA#T^`f6h5<HVH7^^k5M`9Ywtv;Ns9M94fJ=v&+ia0g(}rBVT+zHG>KbL zAy(CI<_A_Gle3;b+g}qtY-RzAiyIgouVa~HX_9I(J65g8UjXFrH=r#H(tsHl$6xxP zPsKIffN&RwK06hk2g}I2JhN~uv=oqUP+=PtQL<EU#d{zTt(zZ^D@vfl%zC@V+d+PZ z*((PJTliIymeyC7oij_^2ZDR~c`#~&qe%8#RwR_hu591GATs=6{LF#{(rkdtCv3S3 z#jRDuKZ;wo16o>Bb^Wl6Bw~9qzx6;2ZVl*vBj(-Tm_vLyxopb2#T=r32jVZlv}>Uu z_|?n{^EeN{>HJ+fcatNDJ~fgLJIXf~pLcWwPHse8ytfb<l{$y+V+P0u_t~~`Ai8o! z9DZ9)*+~E}aO1(C6hDiW!|(I+7%Vt|VTpHK6hgbc0ux{{-X6DQiGVCvS>q+=qopB` zQHve!5n0v|ybn3L6Z-sqR)%{=`JY&?3(XA+>@BkZG6#46xJ%;8WdmA5OPEiWX8#Zl z?4Pzh^qKIZB68tER5Xfl^bgYL&f{P2(5~<pz*W5gAVIW~wb@3nJ?95Xh|4<YCG4Vf z*wJPK%;y~~n}?H|uQ+j7UXmwKpgJhdWNg?)G}6)Hn(161^8e0$V0*Sc!#Fal)E8B7 zQ3V%Oa8U&pz2Kr3{J-7{mhmdPv4N+4dcazW{AJm>pN03S2Cop<vFhi(<Hz2wlKJiD zz(+e|RHgR(*l<(e-p&=Kf=iZ6ny=e<_m-;5gC~#5j^Eo^eYToi9L3W$tetkOZ89`4 zQRmcQ$8%EChSQ!0phY5xiWXkN`7^$1U(n!w=w_tNi5I`Af@hU-QkxVn{6zJEerP@p zkA6X4KohqC586H<IMisH174a=3!DiHNML$uU`E8pVuZX7xGp=Cf$n@Y^M1wJ%|c6r z4f+Fz?<Z}_+OKr=d7{v&d`%-q$CH4vSG3#kF*dLz?SRGF_ar?18Cvz0$Qkb74ekG4 z9IBVZrM-USLyjw2TF9&4we32=2cl1227ec{Qr;44{WIF72A?<+7KOgk5l@AelBNwl z@iE1dz>sIV^UNHc!t?PCYrlQ1;k7j%W&jl~e(2(fE{g7=j{dh9qh}tKjU;4(bLmKw zI|aqNJMK4)r%qNPbkLggbLlNE*|7$e8LC64>F*UW##aa-jq8OKVx!FbUmc=6QMId+ zR*dY*n+c0`e6zcC%$OWwDLPEe)z|9x9ZC6pHOyqN1@q~Ym~aD!*_3k0ZC`5z`&=aq zhlu%e+>#Fjn?7!aqtJe7c1uQzS#UEht_aL&(awa0c}wk3m6K<$JWn$pNSaKbvk(N7 znP1~a>bwc}AVKWf?8;qP@2;gKe>60e(u>uwIii`FueoJJ+$sJpw+JyJ&QT&KI?)BT zw#2J=9TyO^y#(t~oE*>#G%47>G5*+VWth4E&G4|SFTKdM5R2Bv7hf{e;*J@g+fq$D z-_+Cu&ZBu3RbwG|+zrrFFXsb}u>%oCe#jdhws?|;DZIv(?|Cn3ZW;EV`%bfoHD@JH z!>&m*mvA6yz)VFni_q(#W$6zVJ|Z(SM^tkEYr%cGdRZ$IsuROQp0)n6GXDVT;{hnb z{<HM=3J^9<-JKhYfzogJ?cGUk`fn*I@~~I(rzi`xzsgWm<`fskEf!-w<*MR(KCl{- z4NdQj<0c66=<(ZF>HRiAAr0y+0ba%~NIb!uF<?S|YSP54*a41bpK<uE{hmw0Myyo~ zehkz?2w#jD?yttYHsEiH;J0ncM|Fx<mzUjqZzk?eV!+TdU-YOWR&GCKn_IF7yRyQ5 zG9>DgLue7cWG1ZWcmighK(k84Z-W;Fti|dJk)Dbh!6Cl{w}(t*Z_@A(Hh6hzt)q3o zb88?B{+h&80{M=TSF)Z^E|GEk!Ckh=aXmdV*eSDnbqg$%6L4DXhr~Hv3R7pXZi(22 zd4{~YMWZkFN!@+Xs<;+ZcCR*e%C~N(S}tMT#&d{uS%h^T40Ilug5bWoE5#4U_gGq8 zz4OL$`HG7%Hy;as+xZYwQIv+9298L#A$euOjk2{tdxiJiSH@AIj^_JbJYP0{;=HPd z$BK*Or$;}iuDHGLkt_DHxF3HNx@B*g_OBP;x*==-#_~<ywh@|c+_W^@-3&tSJ74{w zeS#@cSrw--Cr8p>a^1tT9CG0xPRI@xPOi-&9FTZkcj}SsBiB*~t7Cv~egdsYKDK7F zHariQew%!}rLei7G&IHZnl1ZSotEW&fq0@{+98{%`)4J_vhE7Lz}((x?I#MWi96#N z7O<(lu$z=adj(3vw<XrQ4bCKicx+Cw6sH?(hk*$o9A8R=hCm#KYRz8CO7cAAp5KNE zFITS)0}VKG&s|~xg+&JXH>I?FBKqBp=yMBb+ZdfPf??`>Sw-5u0^@d<F|Y8cE1_XK zK}mLQ>4=BD`0z`%!*oty#Y{jVmXz>a&;d}Et%|WfZ(X)e_wD{=V9JJL0{M&ia{cHb zVC$4I<2QkMEpRw%vy*q>tbsu^0K@1fbUs?tiU<Box%LFF>z4n9czP-xtClcb+HWm= zR`1czWfWxg(Kxq;hZ|)cd=CP#`OXCGHvd4Rn#^+Wf`>8~jgEd9&|VHN4_TQYDQRgX z6WFqM<F`qMV;!b&3`0#N@?1xEK3{omt#?a&cl-ro+rd>2PA`WO`Tpdb*9llo%(~%> zHZ)jMmVLgap@85UflVoIe$ijM7R0k`<o>E}Qj<N(lxQ<o`yy@>@pt{-;<T0posTk+ zb1Ixwmpv!5g}(`sz2q%Fz{#9=T5OA2dBv2L+BR#ta_tH@kyWB<KWde|Irm+rfy}o# znbwDRH>wepD@41Ma7-y^m4Lyfi+h~7b#njPtMKn#NquIQc=Pir+-eUPwmV0T{V%Tq z;WmEvrRdY$YuC2=<a90lcsd>9z54yhB_raBKaulTKZ3zTz0amVzl{^f!qp`A-&Vm> z=FuR`T-bKqsvpdD|A73Lp$LMYppuWK?!ZuoK6=I2X8iCY`0-0&Vmib|#;S5yH-o9O zj?a}cD)~s^5Iope6yHf(Vx_=4YQ=&k9{hm?Dt|{bYyX4t{f?$^?6*y`!kEv(ArS$N z^fT$IMF)pKAIfNj2`0sx&YytPxRR0w6i$1bQd_vYH}@0-WsQ#0<kBkbm#Qs=5`247 z504zeI{uH~6F{jS>B1+*-r>M6ty+08=AG4MWqDA#6Y}3GWe%nh{1sn;hY|T#{*M>= zkKJc>gW1)s3IK~W04QOQA4!6GL7U7sxk1OezisIY%<uf&U3iG@fsZ!deHjJh_%%4Y zYn!i!fkNH9`)y7Sw0UdG!ixEiW|d2PAHMKdc^^JnK{Oi=RK6;hbk*<6EBLmw>RWB8 zTiD0~jN(5=#20>CcnH1&If`D{h6Yde5PZjiTml>&;6n-zm_!4j^QH4B4uW<-if=Oj zVB|eLcn_EM^w~4u>C<%@eE6~cBRzqsv+=e`yz%46o3*bhW)`eJ|1lG`@FV|mRlo*J zWX&(5{7xr*kj^#bS+0D~dF>lL=a6Z?@4|}VL7G3o{}HeQz@ngofaPqC%mRj$lm_Vx zwQ4QoJIPny<|Hq<Ww>hcb?*^8?D+b(sD_Jb_|G8Cq8k2RrG^_OM|gkC0$B8ni=Gi6 zz@lgTXW`?bXIuoR{9=-?3-%X1<NsBjanTztdc#F;_%D#Di{5b28!mdof6NZPU8HLk zSyK>-uSw-DvZjly=|YllF?sR#9R6bRVljEaue1D`4F4i1T?D0zpmY(GE~fGqQ~8Uj z{C~+CF0{AuEylm}gXB&q3-~=XW}!#*U;H=x{@K5kWAN?sSNzK#pvvI~<lC?MH~hc+ zx00H#cpLo76;P=nGP(Clamc^E)W6kZEPV05{4cdmLbVahkNV1u5nS=NnwqcMo+D7I zaufzeeci@DCCp!SQU98_{IL;h*F%j}pTU>gOTXf^e-+hyox}y*fs(Pc_)A|WYp{!d zD~$S=4{%{=zd*fF=-h@c73Kf>dHk)o?O)QsKekkBQ5FBwO<q*RKc(dV^~0hn{--u! zQ5FAl3$*AJ|5Nw;|1+=HBtOyAh)x!>@OLeuBpkB;DGr<8mg=Y5R@X6JqSWQtGG^aw zWZ=t@aY%V(f%G!E!A}VM4K6%Vh9(K+)6!oq3g-Wr<!%_tW;?agoh5prhm7W^n=x=W z{RmC2x3T}-w~eQ3(lx9S<E4*$HfHa)0erCw_qi4gzc}WMfXO()$hYsAbSmrNCW|YR z?(8Mf`ivOrSjikkEV+^3b+z}76a%RySI{&S;w16+r2xA$3B4kfI?X<R!$L7_(5w;b zRMAO0w!wS$m0hDWgMDnCp@?3MELAczQ5<8u;s_zc|M*1}CR+XT-ooloOD$^N^PFfN zD?c~srR|@X^ZuZYu_bq}{f*;MCu`(h`5U`E$onZj9gjPavtP-Fz-*+3CY+6>8nB!b zBm$-%AK`R#&*kSfN_)vU(*L8~J|PXA%yoNl_u26+`~O0w`(0tBFG<P%hoFawBej3V zBs3Xr(LFr<3ngx3(4IUts}s4q_CTLcz)YpZlO)pNtF7EdHC!~FNN4zbzGf4)U^gHa zmB*u71y|@^3Bq%RQmOHj#QnpR80@{dx`ZLBp<GViQ-|H{L3;aiv6oXtk|}r`L91Xs z%@8d#?(Sk0&!0E?*8%TWgBdRF`pl#Oa>@0-KyA<sDM>^aR(>$)W&E>Gh@e_8&Xrz+ z`ZOe6F(vWAP`n*sT@7!<PAo-{$Lc7bMR49)HyqU2r|ww1`pd5mnp_LrV*D<pKrM@v zw>_+6%441K+ZtRgxVFab?|#GDC{0`@x_yYH-jGh*?0tBQjOWpKPwW!0o-QWSqAen7 z&OG<p)h*vADu_GgRHrtc5O*|f9a{1q6#KS5+_jouz}>@C$+Y?luUhu4sM5B^>oBj3 z+RDspKYCU%1JW)x<~ygqtW|Zl-f7A<kAOI*=C)NGntyD??P$QDllMH#;c|-;k6PX| zP3g$vvjZ8UZlvT;1n#ir+(dA>MwF3%oBK4O^81eNO!1V&YgdZ;+GUsH19DwC;eO*M zQxkBfiw47qXm!dv*^GOhoPlwwja#YX#Mb44D>f{Xz)elMQ!xssh{Rzp>MI`a#62ZL zoOS!9r1YCYzR<??`Qz%Np)W$SQJ&Y_|7oA#4)`C(&~l&u=nnK0&XlYfNb=p5U^66k z<+-b7<a{@pghmyy`=&eHNGY!<0ms{~^`|$+TkxV{5g!yK>qDWcf8M1*(?&q_7)ZZh z>`65Aw{MZIcd#0sE+?Igq$J=bQn(-Nf9q!bs(Bq-+G1p(u3pQT@>w1+Liw>v*ns0m zeI@!-#$|x2{e*1Fj6cF$rIUV#kk2^#cO9Khg&rzWNeyx4u$pktuh2LG9ebZY>M|7N zYi2HGK9$h6i)Aj8@SfvNzY$jW-fu?%8Ec|8`sAo^hl(F>$eyD@NY8TWd3~QLb{bT& zrtBzhJv3u3dx6oNSUEerLe~-^Oi}M^nsA9n+`7%RZ_as2i<w?CSEGd5i8&AVB#7vx zWfU4)b<_(*Xii@A{d709!5Xa*H$*-3N};5F2X)Y`)c=!JQozH~{;41#8ahIQdD7UD zu&iPIt$ne!2_DU7TPPy_lW)ZuOGP^4vn5Mg@4syjh!lHIlW6p|y%^&=_cFAhltGTS z#XHUBHeJ$5Ri(tZ>5>kp&oL=@bes4V?o1Q0q2ZQ7r1@mF;ce!&Rkrd$cIx;GE|q2l zsdjjrvRrBF!N+J$CY_<;x4N57DptolWqKLAKdz%;C`2Jd8}(?Td6G}>mJfwozVYrD zFOt)H7tpfFLQO9Y`x7@xCJf;}7c@;0;>8X<JY;8Vd&8#DF^5@He)`VM&&nM{fJ@5< z$uHRgRYdC#ahogO5v}psOfS%fg}<O>A~+z$ys|IzIQ*A!c&%_}H`A|8K6LCp`MzhX zf93S72l;HPa+1VlG7{IK>6Kzwr>-hTnCN>DKa@iV4pqb5!u336H8!GA37cZGPa0nd z3bAWyyw-&jgD$XHF__l?hC{NR#_vSqZ0uo*O0M0sdF)LSEn{A9zTIAXLq-8Ts`$xF zOWUI?Ill>7DmQ^Uk}ce5LoD8>$^LY-e3o>;{wH4&u0+9UzB?l9`6M&E7=v#<%wstO zpkC}%QfGpQ=w_Nb4^CH$I45j+sA1DObKQCFEkYj6<K)m%)oH{u7s4nZ%GjNnTe};t zpJ`<E;IU03cALE^YINXegNF*+r-(xGl)&|zvgb03@V}sPdq^tg@hNM3D(9OtiBZ{) z*kKq%m;IB{yffLI>uZuVkvVWq?|uszJBqk`>#UuH`-S=1^Tg8()U=irqtmT)NBZ>h z2aR^9d1jr){SL94npe`xB6$G^69QiEr4EZ{Y@b!vz))82k=<8($*!(IM_8foB-QOH zox!+))4a=fMUA_N%u~KYZFa&fbz5KMv|OFd0o#q}Ea+^gNljO?Sa*1WKA6PN!x8(3 zf<nu=*pCNgQ-VE_m)mD$GkMlUYz8mVnORv&{ZJ_ppFj4A6F#Cr>lM~(d(-@klJGF2 zcv6A-5~pc?m<B?965~4--*5rZ4uW+^+lpKktP{ZOcOSyx4@~T@RUUDqzK|lpt2jgM z?+YSM^X4cC!L01p2NP7*jb_g{H+F0N)Y|)}NiO>p272i5{8X3T!}$?)2gu#>qCRPC zS)TqtO9AD?f+a|imPAx*bCHKj$!l4|>wi6zVTY4apJ-J`rC5C85@w4sS2wrq#|~NJ z$MVuB`J*MNSlaz*29AwwC5wvzV3-anaJt`}*X`%qyr#>0a0Yjp!uwQAP3gdFEpliN zJtKF(W-_<pxkE~=I{qN4fxIGgW4qR)p95!BkBDzHS3V7t(F&l9diL&u)}P(`+e4n! zsOx>IOu#xW8LKxFK{WSpa0OlW*QoYBvI>h2dBfvLIvnD(OK7LqL>p(cjN5kvxU?}E zx9~hFer3_sv+3C0{*Khu-6d85^q#{GzMP7J2wg*Sufd-DyUS^~5muwL3bQE}EsdbY zuxC&9&r?L^bSaT!S`9vfGG`L{P`Rsujp7d8w6e(atdb8+meN=C|D9R&eocqRRb$sS zLq>^hv%CLXwNP}oMJ$#S+0lAB$T4WJ${^yC|2?KbV=2+G4mHz@PGw~9xa%U)K$b8# zp2Pgq0oH2e)Clhnb>XA3%Te=*5e$vXtfi><@o|58*-2E}&rOp;4`agB>7U%08&L?) z)|EmJ?{6CMf8)ocFbsLl?KqR`%8itW>WWBpj0UsMj-hAd#;(V^$#eCKdZa00%6Q~V zhdql^Anq!kt=j4|(zW1pjwJjAF-oDIuv;FaLVgNEYpFceEyH_96lKngy;>M72cbXn zX~7-;>3g^Qn^-eeBdyDJgpC-QvRH@m=w9R5_=1vNIeP|r?7Zhi-`)_h1}~g{T^6x& zM#j{JdA?yB-=?USSilH<XM?|jt$)c%9pMFFGpDRNQWz?4tb{tWEN_GFgMQ`7UHvWs zf>!~Om@oa!0Oq%l1(M8S;^x!K8#cDEt=7sq#Awie&1tNm_%eI)S?U(pymCXN%4kA$ zu+a48l569JDUoZCyJ}S{P0MG^Cn=~W&!iE`j(%;|6AIep(uuS4A?4ZV?PO-pSSa?Y zYwx2UM3hY_+wEC$UfZ(s80GoO8FoJuwxo7b@NX|AWXh;>10zGRGi~JwnTj2=I(Uim zNfTvEPqJ;qbd*wyxc4V=tZq+HJojL_s{g)%(zho}QuMm>s9E>aa5w~({b8EPDHv-j z<7C(2)P0rOF!Wxhh7%ZGeHLr0UjIBxt#&T;A#N(G-LbsBVg_Z-AX2kLEqsN%NIy%6 zqgCgc>QpxDJJ;Hn=BmlM@QcZKNwQ04y-*wO7AeNpHm;a{YHr~gug@9_-O}NsQgCik zM77;R5=@ok(Xfsw+VB2YzY7as&+-sVimW<4b=UvUR9boB%T>A0TsgUP^eHjkRNq-t z$fyY!t@f$u(8!CNHEF8c7uJnDc5k*-M~(8sYZLZ_w<X_Ka}PjoC3ub$vD?9LQ->aK ztWNj!j|FNGS4|to%)hG(ed1$&0OQMjMQK<XfxHvIskBHTo6mFze2`Xd-+>)4L&!PS zAlLbjlV<linz-dDHExwHMasRbx<};$Gm5EubJp%gH12Z8G`9fz=uEWcT*Fz5ZL{)& zBF=NY({YBSyX6kkex^3-AEks9bjJitR@Ml`4_XxWIW#U&53soy$i|bo`?lliTtgi8 z<+L8OZ)Er%k}E0OK}9gSW9!(jx0J_~L~#2K*vIaQl53jrU2AVj`gshcrp&~TPS)FY z%HRtV6BHfHag{~1lbPOaoz!;mj8leYD7GktI32=i6u)4kh!ducq+&f<OYCSDaUE(H zw3}DmOhKJ`6>0dr-R$|{r6@Pj%uV;<UOV^hU-whWcJu0X3bdtu@9)V&rwSngvRo=N zO?<+phg#C(ch@5QinzQCbTp^eE`(#$?>^hUZT`km4=xuJTwhO&!~X4!M^42w@8U>$ zV+O0NoRPXz4PTMS+c`6u=iPjlvgvBJkew&tb?-c{*x7~U<V9ASBS$z|&z=YulJLpO za+>I=lOz`=md>JbXv);P(h}8(gJVS5OcVPsdwl0ahb%5qo>_}t#cJ`c9ckotATG8x zgUqR0hM$UT*qT=I13;k$>QJ4b*z7!qQ?HJCnBVR_@`kDL2P$8?83r=GBeta-;cn*^ zf54awkfUu)xnl)}1j=V<c~b!7?tbZxfuufKi1v)|v8^qb!olnJIbklJNO}e2bFGKs z(5A!@yq5<ukVX$F8pYf&!Co1$Fa55KJRjgf#jPIEo14}~Hd3uypPpe15b?Suqc4rD zOlE&Hi7&Bh%$>THNuA#NzL^={&C_F<%+u7!?HUt}X~avGc-+c!FqvZVUffKWOs0gA z;EH1~K}37mM^VXohCu12l$6kWAsKSY-7=*G^c%mI1~|X!r%`pZaR)T%{*;lIo*IYK zr=Fh|4&^W_K+Y-yUS&Cz>eHziCN%TbD9yQ6`S=ZS+7@K{ZuXSen$4c3v-a-|(8!&d zGt-UKUrPyzyY=FdT;4a5G#=!)kBEl2+0+&qLC^rm*?ScJ-vlIM&`DVDsqDVIBlGRu z+N&iL8zdVFyGI`K=*2Y+lXeJ`*7M1(n#H_legSf|%`2;xqU?ARFQ!X%<QHNxIaJ~K z`3ZI8!NM_G!tyrF@K&b@SLN0s(N2}|5?4uHWa$<w%2CfMOk8trF_TrW$C3VsBOvCr zwHz&;J(u&gA^~lHvzu&e;$3URH*zcepH2MIjO7&BN1$nDtruEo7|#YxVv$J^Gdy01 zgvYLD-nC#?^z=JUq@lPwwqs9Bdd)N))kh2KnrHD%lm*h4?A}$|bz(M%V^lt;PG)E1 zo};XmcdPx#Iq_@W1wZRJhq`D93vcQ&sygbh!=uaXL<{oB(_D6g?1p^Kv?1!0bb{9< ztB%LT5{c$HZ)Acye3dfnGTIEqhj{89A1yL<h43!*mS)OT-5M^h50DkM86GZM`UeRa zWG^t4$&N2wJ7B^4@eb}*CXgeade<$)W}n2CFi#@inU74JC9*3>g_JG!rU+^V>J>%0 z!J6mm@4~pUwI!6Rx!=JU#jI~!MV7}WWVIC3<0uZ4HC(6t>O&PKn3F7jY^LMkImHH{ z85^7%Y9QX?6lR=Vi|L4PQt>WX5~Kz#;TtxlV>W1N?D+e<ntCAtYqOYlTL~&yhPLqP z=%cX`{?o6g=R_k?Z8llhe#)@NYn8M)$8U<O%0VMhzQjwBp0nfidY){{#{tzL(I>TW z)WRd8p4>p7ieB3F+h(GFMm&2wGo@cX=X!K&1rf&<@l900je#el<#ZA3$67KXi68GP zeZu*-TvD9sXldI&mNlp!T)EomH|$_%rfw>l<B1NHLZfzYQs~bbmZD_kOE1k|rK}bV zfPm;R{!SwHC!T#M*N&u$ntqb`1jQLd2c!232T`#7Z(dh|m9mxI-Y&>{riV;GX1%Tm zZmC&LLNXlZKh?!=$RGP)KW|<@ms`@v+ER4p!S;Bp*?h0xwhzpq8$Kz9YA+rKl;$;$ zl&rLiuu^Kn#picB+<LR4U7O5$ncK!f9`GKmC%<y6JSBvcl`H9a6I}WI_r6&1BW>a) z%dc|2#H?#S*}?H1W&wJUILa$1WSjUW=>D!fh4H#DNX`iPa_TC8U=H_BebAAZIy>pn zd)D;P>adYEG8I{Jbdq*c-RYGM#iT-I^Q>SF#-|%Ry}TpJpOKmJAj-tY#UxXN=}W_v zs5r4ENvkCy5x1Ml@1QDTv^K)@1~Vo2kFXR!%!L6fw{y~9*i6u}ilY5WS}!M;BXmtL zQ_q4zt>+n!`e)HCoi4SOwfu-Eb({ZuPk+#BKFlm)1hJM&=(AK<mwQ_XzB9Q;D57D} z{CKGIUQO!TO~x-=-os70-1&R%eO^S>LRIz3Ee51*{tpqIu8i&W#5(B;3Xb5@Q*HlY z@2w$~)#3$tuAHaX@KZJNuST<2UDp`34WejG)k19T$7Wp?)|vOZ@+eu4FuW=)rL;h{ z==*K#Bn$J01i#5wOGON3*UaREk0K>Ht?IAB`Rmt@7&HkbYGrz1@Yn$_Vf^Vcd~;Cy zIJ3p8gu0z7Y%+S-MZJ;B(WW%=ytYnJd7-1%TC*_NJsUhErmvc!e96{+gh|^gWA&_d z_EP*wnh~w^s7U;*_wcFEjRbR-v6l46*)~t|YwJ+?<#y`akzZ~tXHbMtyz)V6YtBvl zVO*=TLF2m`;%;2bpUhX;S~&9+J9vbFA9$NA#5fZtxnp(NFlu1`X9kqBTN1I+&J^ie zeH1A}kk_~=ocq|7lgOf-vN)VIXHU@{WzOV&I#SBx5@(PmeDW<|q8i8Tw>U{-M|g=K z1E$F66uH?}$BD@@NtQTs<bqKWy?{BGGIQWExn9F3v3hASN9U0zUjTmkKM8=ZoGuod znbc51f3GrTgkok&H*RNiCf4yFbTnghoxjtHjn`~2X*SK|$JK0MQYzUK8N_Na<Q~+V zs1K&z)kf$?@QPU;X3R&?+fhID&u<Y+Obl>g;_O0o8Pcp=ipq<kp<J|r`Lz$p*SGr( z`=mZWaEAM=I<$))x1>giq$jj(Z9&nKof?8gP>1IZU02S);Ck#DyBK8kLfngBw+J=j z!~=WlbT9uL0g@)qwB0>bL`;7Eu~)!hr@YDL5T~Ug8FO?*LsO)PIG^`>mVn-ao5_}4 z?VL8kP*LwqwCIxS@~^B7rql&cNXV&@LoCM}5oh%=wn4+&1eLP`xD#}>ZS95JY#4e* z*@g|^9@4Btqv+=-@xsV<H}Vx_d1afs?r(xpzn4&ps+b{m`*X5XT1QJhI@@#dS`Zre zzamx_U_T41>*c0sXYNi_CRJ}Av~xXX0AZ|0aMKh`GUW5${G<a)?%zI`bS>$zc9r(g z;~LIK`{xn7YsdM_l`Oi#uf@-zeb)MI1;b3FYj2k)!zr2akTFPiN49as>?6G}#`Ail zWNA14bPIkU`YJm32e@oW3)r~twY*LV2=0#3$_#ndD1wsGOZ4Kt5ycKuQycPG&IWV$ z$B%C^-IL&5|16}R>tCKnt&b$!OZ1s-doxSb+zdcFM9lmoYkQtoQE#(&s-bynLHUq+ zN<k(o{7rr^(dSIj@Vu;WN5xc+aJ&<~dos#Pui)};N*OrEXuXO<p+{Zy)jr+No%s3I z_g$l|I?H^q4P_}b=9P9f$7H&sA7#d<bpLciNNjejXvWzdOe&&t!D+v-FrB>R{ytWF zb)vfX<=T;vlJQ#V$C?r~&R7&Nh3wwFFOp|u^=xB9%dC9!07K+<C<H(jQl7x}uNjq? zB&)08CkXUAXm7-1kA(X50L9lA0k@URT+b!7Fk~Kj)QC7gK(fOlHst&KndLeIBk^xZ zluA?+OV6bc^|%QHbjR#FVi#)k9ns0pqX@C!rR!e=`kxz@Lp%^)t$75wRi@8Xd~1sW zmnqU{Pi9>htlm5?_pa>*-#O;i7<%rR*4L#8w5hzoflf67gRNLvJ}R~UDySdK+TvVt zm0TjwzXPUynO;nQ%LN101BZ3a_MXxc9#sJ+GvUvhLp(9EjkRgd-{(Ef{b(j^k?>%w z>gU!HH&XP_!|8@pDKeS)gvL(Ih-fNI{79JVyG1p`v2BVb?$H}jTpzpqXVa8V3KAwO zuw-_fE7gw3YqK=-MAhuef8W!e{n{4Ms!?k7ATPzQ^_YVxZ!{W#%31o|cQVUP9sBVd z>cKef6Il9(*i|i;gcXo;g^pQdF@nZut%!}%Ea`AiD&D7vPVJ4~<ZKz~h281&^1}tb z_W5EDzWxLMEDhCV6@-(?qsQ8O^pT8s7%|1M?`2DAR>ifWH9naDF?JJEM)mOjSTQ5b zojKNijZ`a~YHB$pKz-mw%+7LpA{1$%s=AP6@rp=x<y<%6MiIORx`XrDEGyBY_m#cr z*Z)ouZvaN%H|<Ptqo=1ZnT8d=x9#SIb3}?Ep!DJR?Oq5dB^4wQgG#;;gHp_=4B7JL zmn*=?YO+%1GVuY&dh?@!gg?~rB4<;6*mO@=T4n+g$PJN>1ndME#??jesucnrO8E5M zwPJ-jS2c^*sZDj(dDw{;)mRHyZ^|NAVXfaQY+VMIT*|;4p9}iZQd*A?%PO+>d%Q8K zOs?V9JAFqgsf+Qw5Qj-*rhJ%b$75H~O}ewp<CMhM*8>FRr?<Ay-KM4$(aG@`qH0$_ z%NP~7Ni?pMS(I2i8jtG{wQuyoIc81w;Vqhc#5@?0y*Vk(bb@Pd4_9ruXu2y$R(e}Z z26yCq@F9M*=3cf&Xf;wf#(N<-)@!h&*<>W$z9rp@#yo~&-$Eo~%v`Qnkqe0P33cM< zh9J;IJ)LL8qbft9&F>KC!8ud$*TW3Wos)CBDv5WZ%>84nSiVkA+aca`_@pB0e>D=F zD&m;snptz-SzM>zch)u$vP;=6b-`Y$htlVte_Yk^SHQIg#ylR6cpfo8etvDZeTO8E za7Z+X^ZU%`6=s24ra9xs1yW#G^iIU^%li?-uI^DcWb3b2%)H37q#X6yCyBErTWfO) zem~N?LPWx^?}K}zlS;Ds_$#cjLOGu2=|r-fm6~Py`T}hk?y@O<<!!yP`3fC#7%@0o z<a!8hhy`{ov+{!x+@4__^m3o>YNv_!tyB0yLivw)G>k25c{J3X;Dw;k+c?V+BHyo3 z;|99dk~1W{V^UfQxwwSDg3h06BIfG)O?a*j?M}mW!IXE)9WwH5dn0dWay!mbWh)UE z4-bLPH8Ri5+6Z?nm(ZBZtZYPNaIx(!U0!P(WjJ2H$>}Dz_EM=r30}X7urJfT<B>8U z?e);QE=Vr~m*eBjuC8owFf=aB>I#n4wxmhTo>b`yXzW?7@Q3F?)`?`NP+$1?_Sij9 z32vHX#hdl>Ep@7e%hjTR0#xj*)9PFM3eP~wbXOzXAboI59FdJ#dhycTT@AyT0cL#W zPo%T?DR=ukIq#)l#~*$rxW$O`F)ekemzwv)Wc_PeCbuh>H@;8J=ELLSpE4_E3`AI@ z+%kXR@6~62&q1Gz=4Q-M>CzrZtAO5RViKM&{z%U)5m=ahcVPcLoI0i|8Yg7F220Om zvl`CT5V7=o47i#YVsN+{f_dfeE&rW})wuJm>o;Bjx93JeMJsnv%R^GLXF~J!#W5E< zm0h%E_3ES9!<F{IDo%&{$Q3!(&P8Jt)5A7~Tla|1(vC;-Bsr9j)Q$O=VyY*#bcC3h zRl8K+Jfw9>cG_W+hGkD~Um~y$QakJ5N=i8T6}LUDo*O>hHjn!5a{!f)LJPu=MuM7` z&gGeqh!g(pz9hL-_)!f&Fd?Wh|DvhmcFDWM%y*12LYDd}+t@yy5G}^;;}p~FN0*{T z2mxKWm8lF6lkAX`bVl1%<MNTRiWhqszm`Vm0~;f|Tv?CB;D(Ot_<<{!o=6f&4(rZ$ zZ6A`t%a0<cufh7#K92J8RS0rpg&*Y`w}#Nyw7m>X^qx3G#^!gZq;uLmri#Pt)(6qJ za}V9SBM2si9e;9zPNiIv`Vmp)$ULW3p3A{w6%uC1c5AvTmsJL!YLOkI!4Xoyzzx?g zj~+#Oc3+Fm<*JE)Uz{u~ud0whFV8WU-3?iB&E2b=%xNDB2_HI?huJEh6hqv)JSku) z%5Pzs`N>r^@{Y;Ub>tp{!$E&gutCU$M9}CUDo&-ihA7HHZGd=&uI_Gz@y(Iv>7y%= z3vP428{DNab$0gMg9zI?GC7u-;m0v^4XyRk#aJ-5KfqViC;svW8LQMqAG<cOhC1s< z+pMvh)zN-7L8Qp!>WAdz1vbHUct7Rso((@VB7ea!cSWN^$CW)U(tVp(;I~I&{d}6E zv-+ddy~1PDToOlLcQLvW@=>4MLdN{Cw)p83rEY$TH|RN#w&wS82xn(DJ_-L0GKm#4 zza|>PI^6~N@OKo8K)Vqg$L}kJpysNMQoDs{rwmQ$p;qPyA4UpQ6w!Lkg+<UFf57#u zSsfga^V+#%^lq~eHi6GTCKI`wdH3F3Vow|}CHXG8%~h{XQB2#U9}%q{D5Z{lMw!GC z?}@^F8U$WC;*)>e)XjuUlCMV`cDAiS54b^j&R8<_`AFtA4S8|LVyc}w?OXuPsza;s zN`Uq3-VfrKxySmFX1>Nr^LZ-fjVm9wY}neV62J;=XhwAJwus91^Bbtv7VhA(TO3yD zvD)eiUN8|M)<bdFjOn%rOf;X<$>NM65+96Xd%EA`tJ7X7LZL=_h*MjtDoK%465?cn zn==T;SFtOJrXN;Thi;lh<UzWuZC5BRn{7wP)$^U`6%vZrUbO$g*e4~tQ8}-;+0P>G zpwH=|nGBN#3+F=RuKL(eHi>}e{bcO$QDsPF3rSFbgk<T?HzD!+N}la39`j0Pjpxp8 z9eJWET^pN0WEO!?6HVr}SBEo)f~3W^z8v9lwim_RmlH@YdUMr&{1xR?jyo6AV>f#$ zC$*ZctFI%M?+$ECukf)pSEJxLO$J%<%)N|P<Imr?)p3g_D(Y}K*SRGuIoheTlB*xz z4B=2Sw7U39`6o#lEJrK9<|<ywNAi7hZgzC4xH?a-q=>#+l!-~I;>LST2UKqCzRjnv z)$M213!1NvJOPxvv&)B;K-DG=bG=-9erqdrDrln!Nz6z8QH*LSxP)oQtu@~|MO#6X z#O4}tvG&=6WTfv0zvm0M<56fJm^vE;Vdm3Hg;2e`RX`@@VAx!)4O!e1)uyry#beWh zJN#_oA4HqIs#kF7Pgi+@RYRDoOjFE82(sz9Y?%bQW;EBoF^?KIk}1QkMLC$`izqG2 zQD;V;rQ%NmP)8RH)|o6vFO-z{HT$8Bt*C~{h8|qrhe||zbEOLxT}Pe%u2aR^RQWJ= zxFD4ymy`8jslZ0QtrTn-Auef?z>zcO-uwi&Jw1hDkcgYrKf>xa8}W-4s<gcrm+|)k z`hv)(jgB(QNIoc4V1)YIge1b}XtI<58{dH&+SkV{eD5YVs1W?wxnn}xab>w)2SwDM zUOqaDI!)w!%n4OWx9y!#WH~p|iyGAjJGu)4+NqJn8`rc!#J?&mBP3y$h(2Ty4%u)+ zR%v6ofu!$+(?gb~(0gL^=?=|L;yF!Sg^5bl<K7cuDQ+{}#lG0nUNk8ur}(lj`QNvg zQ+tXl!&K%h+kGR^8`q?<7>q*22>ob_N%|Y5cM>m(-aNOzCE~0!Dm$N#n#iG!vu<HF z>{U0+C$_|2%pQyO=+<z1qpHC1lJ(Pw!;cc<$*qjm+~0TTtdZ#EXEkV}91+d^-tSKa z8;GcC%I)`geb=*N;d*WUNl)+qg;I$d1}vCf^O<*U<Qa0e`ydC&cb!K0qnn{N2qsU) zaUI`jPkorS_+d8u0<fk5Z}W&~1GK7tZnVs{f(kg;Su(fmO@3yAni40YaT*zg=Z9+B zS;?3+0sFi9(a1#4`FFF{<Em8|Z6o3@Dz7Po@2b~<d4LKJqVtW&o7*ZR1O)paL*9SA z4ty<M_?%J9JHk~v{MhWsAv5{2^)+$jUUTm}ll59dY21&Xe|365^yocCk_)-~$Q~0w zX+5BnW?TD6dvtOp7l4mgo%%!%J^`(S>N7DArUgp0wv3QxnvE4~^jdT3)Eun5h>&qb z&yy0+mAv-72HIUuiDM}p9^Iu}!q3q{iqU1sM8eL5Yp<(A&@!8;TO6nJ!x|w!Tr{yf zYP7Wohr<-pn{yd0jLoyvLsDIa0W4(4SbTGU6GOCV#MzWYi0uQ=dtaY8k;}Hx-e!l- zC-B0Bl7a1d<)RDlM0CyJy<_fGvyBP2%*p%D9)9_UpIRai3{kh_+pHkS7q9U<JN<e& zvw*QPVmj1}Sx^w2zM!9$wj1GghD251S+_DjT9Ga;Ylo-`YdpV~ta*UL#%VLGjCABn zj;URVKH4zgLs8He<CpGizsCMC2~Oc=4)>-MaW3o-OuF7wH~5)MoxYF@{`Bny0l{J5 z*~}x7av(ccDG<#Y&vx%@tBhI&>O`3C(@9nF+^R*UH#1To-b=QtO^QIZ+xKWFafTy^ z+wyTi)xuBA)bQqn;j{}Q{x>H$)V>;p(?r5>3$<4p5$EYhaK%14%vzP@hV%F2$K=|M z-S3e$oTcK98e^h&DL!vCmL_ZRYPnUj<uyEYGw*7~Dhv7ply^w-g_8&@xw6qXjc|bI z<HD}iL>;&^*7KtjFqVDuYlN`KE)TMq)Hz)oT}4DgRSuw_y^xLGkkB^P;E~s7S=%C` z%&xV)$sc`PKh$MzvFz}f`yq1%B-Ad>{5y;A*25*u4&<2p&MRrT1Hb94zR&d9y=jiJ zl&4#nzzlOQcT$;Vm)2~$noqu#YK6B&PVa4*Va1m&#{q-$#4W&kwqEBxvaY-S+W1|{ z>VWlc<b`N=oQaI^hN8zpw>LNng%?w}ehn+_wyXbU)SN|lttOsQt2}?q_ic^&fiuc; z7?25`TJLY}{6s$aFsI52BN6MDVfdsppLNOD>21rJ#PWrJgI~2`$-kt_1;H%1vV*rt zbH4X3TiriukledFf1#i)=*AvN#A^I`=F=-X5F0f$p6|ZH;fx9kIUDZj_JG{9uS02G zR(ebDmvpEIH80J*qdn6^dMsy*pczp-k1<R(AgSB`j=56jyH$DEzkgb&JO9J4n(Nwb z<e-bVy@RNeDxJR8q>;<w(j*mJ`)FxAB>tyotq_9Bm+o#Cl;Mwq4!6Zx7v7MA=u#S@ z%m1gnqm4@P3M-+J&dITqj@HJ>V=FDSEUl(VE6dW%9s6LZMXj7>giXz*si2wLOj}!4 zHpTKIwmHEwP%%QJn&nbaIUA)3eq_W9O)0Pt-d#UfZ*6qo?C0RuJumlp?|r_X``+h1 zcWuyT;JYEN5;2jCbWI^nFH0HXe9*E|8Ud=TW47I0vg|q~nT2U_Y-J`L@E?1eDI58K z`WVUiL^xh_+2<fgN8~q!ol13m8QrR$Sa5zgRPTJS_?C7*jX>Gu-c~1Y={NyC1YR1g z2Z7j2Cdd?8>G$l;`k1`U@xfkq#ciIS(H4sYw`Si-8y=F6r#*Oi1`>%iC-FOeJ!<BE z=t%HFTBHA*lQp5%%R3^4C6S>y*ei*P4mcfkuAX&c$)eH)C0mKRk?V-1iC<N^=03J9 zSQ-@=@SWpPR7b%TmF2vsbD#Eec=6ITQZFYdSJ65wR;=FCJC8Tal?d~H{E_v7+QKA- zu$AE`E$QJ_tyXEfQN?8?4Ye%L__X_K#)bynSFiNTU+5mvTpts)|MUIp)J`)c!Qr)s z!=FYp%}m!OrYXZ5wDNBCr9FYE2f71?bN#<3eZkJRrmKdT5Yd5`TNI4Z*0)vEF6{0~ z$qa-fe!4_Sb3`|~3X(VaZjfYYNUZ4E!TZp=-u_BnW}PfxQl!o^$(r`j!bo-Lz-mVI znV%q=4w|4SB)xEcu_nE(_ew8IOSg%C-?0{7oc4(D<Qe|Ybh{J?vfM?-7kK9#-r}5; z7om!g=TyoPk(Tyx^~)|6DsuS`o-GJvl6#T?=1(S#7P;XU%vj%DfiSx`)zhbSz1o&G zzw7-=DJSF}8lMthXf)oMx|S`?T5)Z#-9nHk=jUb&MsjwkrY~!p6m4_F2tvslx#~J` zIgT!znP|wZR!O2oUB9Eyct>;o^YWufJEr%laBc{X_f}sFQLZJ9|0xLLG9INIQ&B5y z8S41)X8f9`omvY4FYReyXU#!%hV+^e7srkD>U*{A4-~GAPS%h(V)|6(&Sc)$<k+{Y zS4qC~*>Tp4GBgWCPa=gpnA!^grEP<Iiq43)Y~sB*w2s?eR2rjBRne+rPj!54zqqBj zxvVMOy*#X+)*-rmrd%eI)rlCpt6ft^m9&`W#0n{Ou+IbFUTClSI~5XOOHm@9q<1lo zxlALbL;vz=vLbgm{U<wY+fsn~k(O-El<x+$*RAG3FE|Nfn)Ne8Ck7fSWc<nxEr%pK zH}Rj>ltoTnTA{<HdkzqiN7vbIZ@ir_f@t+|APwY$D@3m$G6rjh9@NE8_h~3W=B1{r z;HpMAveA6Eoa9s}Ac-M4r(DbQke?ukO4LgRtSv+rK>2N0qB6G4##}8q(wxb8gNH~< z3=kf65fu#OI5AYCZ(Wd%i_VmSf`Wv;3;i;6aY&gLrnRi8DTs?z53KO>%l|`M?`>mK z8FVu?TX^cMjqGgpCSl{*t+{m3G3f}iUv_(M`o*OC_Oh>TQwvr17*panO~24gz+4;V z;nS}@#SxAJ=HkVQ*aYMu3_605YB6`B?6i(t1~sg#QFM&uPO6SmuSx7%-cSwTMD>W% z6J@90((RvrWmzCa)-&8d^=P92DV+c=cCUG((SSDqo3beRpn9}XBjV%$u9{)iZ#3XO zAmKL}elzyG8rVnTKkwW{9x)oQ3Yf5cgaNqx8iH&n2f)eQ8pezU!~+B5SvC4k=Dz*O z50L;)X-^n`d$%`L4jdg^f@SZSi$89hqye|?4&c;Qo)gvIr){%gjX4hJ2#<Gu_BBdG zL1&X1>Z3Eoq=tHg(4>ZXjg?6a^;j*FH`HsHFaHN`_|ooYvH~Mm%HI+B?Q6lpk)4E_ z^PNaFIU>RADyS&C7u?qK%F-p^8??3eAKrt!6~gb~aKu^npc<DVUS3bK_$odpGiN`` z3hv#p+Z*@+ULk5pR9Z6p`Q@b``i$yO@><59y*iTy)~4ZLWkK95WjM2`wTu7g>nWLA zc>^{PG|E0MuKl!7TU$^phCh!E0y8XNX-HoFT)4MyI}ZZO@QxRQD7uvRb@eLtqKy@v zaMFHcF)w08KMNv0qD@|Juan14yDEsdkO*}(ZzI7;!Ezx$UFQ?DYcd9pJAs9@uD^i| zG#1N#kNg7?J_Un7AA;ym;l10SzVJC=akw}?vyW%Nae)7?H)gy8;$JC|Wl&#uK!YQL zpv$bp&S=tn10sAuSF~=~WbnWgGZ0pqZE!K3G#*$WW$n{PLk%9!9AORuXY((&7y&Uw z4BQ^T3sPwoT!RcE69S18%|PeF$!yR&DAxfm#2c-RmNp~`MW2avo!VYEhOoy0FRtf0 z7%gomG&;f-w>`rkwBYEB0$!LD@!tXrlOpPsLX#ruHTxz-)JqzZN7U=_Ode4$Y5or$ dk#0kWX5MTs?@AN;WSN2A=75k5^p6M`e*u)P0=xhK diff --git a/scripts/compare-engines.ts b/scripts/compare-engines.ts deleted file mode 100644 index 22dd4ab8..00000000 --- a/scripts/compare-engines.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { spawnSync } from "node:child_process"; -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; - -type RunResult = { - engine: string; - cwd: string; - cmd: string[]; - exitCode: number | null; - signal: NodeJS.Signals | null; - realSec?: number; - userSec?: number; - sysSec?: number; - maxRssKb?: number; - peakRssKb?: number; - stdout: string; - stderr: string; - timeOutput: string; -}; - -type EngineConfig = { - name: string; - bin: string; -}; - -const DEFAULT_DATASET = path.resolve( - __dirname, - "../../opencode-old/packages/opencode/src", -); -const DEFAULT_OLD_BIN = path.resolve( - __dirname, - "../../old-osgrep/dist/index.js", -); -const DEFAULT_NEW_BIN = path.resolve(__dirname, "../dist/index.js"); -const DEFAULT_RUNS = Number.parseInt(process.env.RUNS || "1", 10) || 1; - -function parseTimeOutput(stderr: string) { - const realSec = matchFloat(stderr, /([\d.]+)\s+real/); - const userSec = matchFloat(stderr, /([\d.]+)\s+user/); - const sysSec = matchFloat(stderr, /([\d.]+)\s+sys/); - const maxRssKb = matchInt( - stderr, - /^\s*([\d]+)\s+maximum resident set size/im, - ); - const peakRssKb = matchInt(stderr, /^\s*([\d]+)\s+peak memory footprint/im); - return { realSec, userSec, sysSec, maxRssKb, peakRssKb }; -} - -function matchFloat(text: string, re: RegExp) { - const m = text.match(re); - return m ? Number.parseFloat(m[1]) : undefined; -} - -function matchInt(text: string, re: RegExp) { - const m = text.match(re); - return m ? Number.parseInt(m[1], 10) : undefined; -} - -function runTimedIndex( - engine: EngineConfig, - cwd: string, - env: NodeJS.ProcessEnv, -): RunResult { - const cmd = [engine.bin, "index", "--reset"]; - const timeCmd = ["/usr/bin/time", "-l", "node", ...cmd]; - const proc = spawnSync(timeCmd[0], timeCmd.slice(1), { - cwd, - env, - encoding: "utf-8", - maxBuffer: 10 * 1024 * 1024, - }); - - const parsed = parseTimeOutput(proc.stderr ?? ""); - - return { - engine: engine.name, - cwd, - cmd, - exitCode: proc.status, - signal: proc.signal, - ...parsed, - stdout: proc.stdout ?? "", - stderr: proc.stderr ?? "", - timeOutput: proc.stderr ?? "", - }; -} - -function copyDataset(src: string, dest: string) { - fs.rmSync(dest, { recursive: true, force: true }); - fs.cpSync(src, dest, { recursive: true }); -} - -function formatMb(kb?: number) { - if (!kb || Number.isNaN(kb)) return "n/a"; - return `${(kb / 1024).toFixed(1)} MB`; -} - -function ensureDir(dir: string) { - fs.mkdirSync(dir, { recursive: true }); -} - -function main() { - const dataset = path.resolve(process.env.DATASET || DEFAULT_DATASET); - const oldBin = path.resolve(process.env.OLD_BIN || DEFAULT_OLD_BIN); - const newBin = path.resolve(process.env.NEW_BIN || DEFAULT_NEW_BIN); - const runs = DEFAULT_RUNS; - - const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "osgrep-bench-")); - const resultsDir = path.resolve(__dirname, "../benchmark/results"); - ensureDir(resultsDir); - - const env: NodeJS.ProcessEnv = { - ...process.env, - OSGREP_WORKER_THREADS: process.env.OSGREP_WORKER_THREADS || "4", - OSGREP_WORKER_TASK_TIMEOUT_MS: - process.env.OSGREP_WORKER_TASK_TIMEOUT_MS || "240000", - OSGREP_LOG_PLAIN: "1", - }; - - const engines: EngineConfig[] = [ - { name: "old", bin: oldBin }, - { name: "new", bin: newBin }, - ]; - - const results: RunResult[] = []; - - engines.forEach((engine) => { - for (let i = 0; i < runs; i += 1) { - const stageDir = path.join(tmpRoot, `${engine.name}-run-${i + 1}`); - copyDataset(dataset, stageDir); - const run = runTimedIndex(engine, stageDir, env); - results.push(run); - console.log( - `[${engine.name}] run ${i + 1}: real=${run.realSec?.toFixed(2) ?? "?"}s rss=${formatMb(run.maxRssKb)} exit=${run.exitCode}`, - ); - } - }); - - const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); - const summaryPath = path.join(resultsDir, `engine-compare-${timestamp}.json`); - const latestPath = path.join(resultsDir, `engine-compare-latest.json`); - - const payload = { - timestamp, - dataset, - runs, - envOverrides: { - OSGREP_WORKER_THREADS: env.OSGREP_WORKER_THREADS, - OSGREP_WORKER_TASK_TIMEOUT_MS: env.OSGREP_WORKER_TASK_TIMEOUT_MS, - }, - engines, - results, - }; - - fs.writeFileSync(summaryPath, JSON.stringify(payload, null, 2)); - fs.writeFileSync(latestPath, JSON.stringify(payload, null, 2)); - console.log(`Saved results to ${summaryPath}`); - console.log(`Latest alias: ${latestPath}`); - console.log(`Staging kept at: ${tmpRoot}`); -} - -main(); diff --git a/scripts/index-bench.sh b/scripts/index-bench.sh deleted file mode 100755 index 4285f6ae..00000000 --- a/scripts/index-bench.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Simple indexing benchmark harness for macOS. -# - Runs osgrep indexing against a target directory with multiple env configs. -# - Wipes Lance data and meta between runs to keep results comparable. - -TARGET_DIR="${TARGET_DIR:-/Users/ryandonofrio/Desktop/osgrep2/opencode/packages/opencode/src}" -OSGREP_BIN="${OSGREP_BIN:-node dist/index.js}" -LOG_DIR="${LOG_DIR:-./benchmarks}" -LOG_FILE="${LOG_FILE:-${LOG_DIR}/index-bench-$(date +%Y%m%d-%H%M%S).log}" -TIMEOUT_SEC="${TIMEOUT_SEC:-600}" - -mkdir -p "${LOG_DIR}" - -CONFIGS=( - "OSGREP_THREADS=1 OSGREP_WORKER_BATCH_SIZE=8" - "OSGREP_THREADS=1 OSGREP_WORKER_BATCH_SIZE=12" - "OSGREP_THREADS=2 OSGREP_WORKER_BATCH_SIZE=12" - "OSGREP_THREADS=2 OSGREP_WORKER_BATCH_SIZE=16" -) - -clean_state() { - echo "Cleaning ~/.osgrep data/meta..." - rm -rf "${HOME}/.osgrep/data" "${HOME}/.osgrep/meta.json" "${HOME}/.osgrep/meta.json.tmp" -} - -run_one() { - local env_line="$1" - echo "==== ${env_line} ====" | tee -a "${LOG_FILE}" - clean_state - SECONDS=0 - local cmd="${env_line} OSGREP_DEBUG_INDEX=1 OSGREP_PROFILE=1 OSGREP_SKIP_META_SAVE=1 ${OSGREP_BIN} index --path \"${TARGET_DIR}\" --reset" - # /usr/bin/time -l (macOS) for resource stats; falls back to builtin time if unavailable. - if command -v /usr/bin/time >/dev/null 2>&1; then - cmd="/usr/bin/time -l ${cmd}" - else - cmd="time ${cmd}" - fi - # Enforce timeout (perl alarm works on macOS) - perl -e 'alarm shift; exec @ARGV' "${TIMEOUT_SEC}" bash -lc "${cmd}" 2>&1 | tee -a "${LOG_FILE}" - echo "Elapsed: ${SECONDS}s" | tee -a "${LOG_FILE}" - echo | tee -a "${LOG_FILE}" -} - -echo "Benchmarking ${TARGET_DIR}" | tee "${LOG_FILE}" -echo "Log: ${LOG_FILE}" -echo - -for cfg in "${CONFIGS[@]}"; do - run_one "${cfg}" -done - -echo "Done. Results recorded in ${LOG_FILE}" diff --git a/scripts/sync-eval-cases.ts b/scripts/sync-eval-cases.ts deleted file mode 100644 index 3da66016..00000000 --- a/scripts/sync-eval-cases.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; - -const newEvalPath = path.resolve(__dirname, "../src/eval.ts"); -const oldEvalPath = path.resolve(__dirname, "../../old-osgrep/src/eval.ts"); - -function extractCases(content: string, label: string) { - const match = content.match( - /export const cases: EvalCase\[] = ([\s\S]*?)\nconst topK/, - ); - if (!match) { - throw new Error(`Could not find cases array in ${label}`); - } - return match[1].trim(); -} - -function replaceCases(targetContent: string, newCases: string) { - return targetContent.replace( - /export const cases: EvalCase\[] = ([\s\S]*?)\nconst topK/, - `export const cases: EvalCase[] = ${newCases}\nconst topK`, - ); -} - -function main() { - const newEval = fs.readFileSync(newEvalPath, "utf-8"); - const oldEval = fs.readFileSync(oldEvalPath, "utf-8"); - - const newCases = extractCases(newEval, newEvalPath); - const updatedOld = replaceCases(oldEval, newCases); - - fs.writeFileSync(oldEvalPath, updatedOld); - console.log(`Synced eval cases from ${newEvalPath} -> ${oldEvalPath}`); -} - -main(); diff --git a/scripts/verify_skeleton.ts b/scripts/verify_skeleton.ts deleted file mode 100644 index 8c33b888..00000000 --- a/scripts/verify_skeleton.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as path from "path"; -import { VectorDB } from "../src/lib/store/vector-db"; -import { ensureProjectPaths } from "../src/lib/utils/project-root"; - -async function main() { - const root = process.cwd(); - const paths = ensureProjectPaths(root); - const db = new VectorDB(paths.lancedbDir); - - try { - const table = await db.ensureTable(); - const results = await table - .query() - .where("path LIKE '%src/commands/skeleton.ts%' AND is_anchor = true") - .limit(1) - .toArray(); - - console.log(`Found ${results.length} anchor chunks.`); - - for (const r of results) { - console.log(`File: ${r.path}`); - const skel = r["file_skeleton"]; - if (skel && typeof skel === "string" && skel.length > 0) { - console.log("✅ Has skeleton (" + skel.length + " chars)"); - console.log(skel.substring(0, 50) + "..."); - } else { - console.log("❌ No skeleton found!"); - } - console.log("---"); - } - } catch (e) { - console.error(e); - } finally { - await db.close(); - } -} - -main(); diff --git a/src/lib/workers/orchestrator.ts b/src/lib/workers/orchestrator.ts index 45ecb5cc..bd0da040 100644 --- a/src/lib/workers/orchestrator.ts +++ b/src/lib/workers/orchestrator.ts @@ -9,8 +9,8 @@ * No worker pool needed - Rust ONNX Runtime is fast and stable. */ +import * as crypto from "node:crypto"; import * as path from "node:path"; -import { v4 as uuidv4 } from "uuid"; import { CONFIG } from "../../config"; import { buildAnchorChunk, @@ -124,7 +124,7 @@ export class WorkerOrchestrator { const next = texts[i + 1]?.displayText; prepared.push({ - id: uuidv4(), + id: crypto.randomUUID(), path: filePath, hash, content, diff --git a/test_skeleton.py b/test_skeleton.py deleted file mode 100644 index 952cc70b..00000000 --- a/test_skeleton.py +++ /dev/null @@ -1,2 +0,0 @@ -def hello(): - print("world") diff --git a/tools/eval.ts b/tools/eval.ts deleted file mode 100644 index 20e86def..00000000 --- a/tools/eval.ts +++ /dev/null @@ -1,643 +0,0 @@ -import { Searcher } from "../src/lib/search/searcher"; -import type { SearchResponse } from "../src/lib/store/types"; -import { VectorDB } from "../src/lib/store/vector-db"; -import { gracefulExit } from "../src/lib/utils/exit"; -import { - ensureProjectPaths, - findProjectRoot, -} from "../src/lib/utils/project-root"; - -export type EvalCase = { - query: string; - expectedPath: string; - avoidPath?: string; // <--- New field: If this ranks HIGHER than expected, it's a fail. - note?: string; -}; - -const DEFAULT_AVOID_PATH = "tools/eval.ts"; - -export type EvalResult = { - rr: number; - found: boolean; - recall: number; - path: string; - query: string; - note?: string; - timeMs: number; -}; - -export const cases: EvalCase[] = [ - // --- Search & Ranking --- - { - query: "How do we merge vector and keyword results before rerank?", - expectedPath: "src/lib/search/searcher.ts", - note: "Hybrid search path that stitches LanceDB vector search with FTS.", - }, - { - query: "Where do we dedupe overlapping vector and FTS candidates?", - expectedPath: "src/lib/search/searcher.ts", - note: "Combines results and removes duplicates ahead of rerank.", - }, - { - query: "How do we boost functions and downweight tests or docs?", - expectedPath: "src/lib/search/searcher.ts", - note: "applyStructureBoost handles path/type based adjustments.", - }, - { - query: "What controls the pre-rerank candidate fanout?", - expectedPath: "src/lib/search/searcher.ts", - note: "PRE_RERANK_K calculation before ColBERT scoring.", - }, - { - query: "How do we filter searches to a path prefix?", - expectedPath: "src/lib/search/searcher.ts", - note: "Path prefix WHERE clause for scoped queries.", - }, - { - query: "How do we apply ColBERT rerank scoring to candidates?", - expectedPath: "src/lib/workers/orchestrator.ts", - note: "Worker-side rerank that feeds query/docs into maxSim.", - }, - // --- Native (N-API) Core --- - { - query: "Where is the osgrep-core native binding loaded?", - expectedPath: "src/lib/native/index.ts", - note: "loadNative() dynamic import + friendly error message.", - }, - { - query: "Where do we initialize the dense and ColBERT models?", - expectedPath: "src/lib/native/index.ts", - note: "initNative() calls initModels() once.", - }, - { - query: "Where do we call native rerankColbert from JS?", - expectedPath: "src/lib/native/index.ts", - note: "rerankColbert() wrapper converts typed arrays and calls native.", - }, - { - query: "How do we normalize ColBERT query embeddings before rerank?", - expectedPath: "src/lib/workers/orchestrator.ts", - note: "encodeQuery builds normalized matrix from ONNX output.", - }, - { - query: "How are dense and ColBERT embeddings combined for each chunk?", - expectedPath: "src/lib/workers/orchestrator.ts", - note: "computeHybrid pairs Granite dense vectors with ColBERT grids.", - }, - { - query: "Where do we build anchor chunks with imports and preamble?", - expectedPath: "src/lib/index/chunker.ts", - note: "buildAnchorChunk prepends metadata-heavy anchor blocks.", - }, - { - query: "How is breadcrumb formatting added to chunk text?", - expectedPath: "src/lib/index/chunker.ts", - note: "formatChunkText injects file + context headers.", - }, - { - query: "What are the chunk overlap and max size settings?", - expectedPath: "src/lib/index/chunker.ts", - note: "MAX_CHUNK_LINES/CHARS and OVERLAP tuning in TreeSitterChunker.", - }, - { - query: "How do we fall back when a Tree-sitter grammar is missing?", - expectedPath: "src/lib/index/chunker.ts", - note: "chunk() fallback path when parser/grammar cannot load.", - }, - { - query: "Where are grammars downloaded and cached?", - expectedPath: "src/lib/index/grammar-loader.ts", - note: "GRAMMARS_DIR and ensureGrammars downloader.", - }, - { - query: "Which languages and grammars are supported for chunking?", - expectedPath: "src/lib/core/languages.ts", - note: "LANGUAGES table that maps extensions to grammars.", - }, - - // --- Indexing & Sync --- - { - query: "Where do we enforce a writer lock to prevent concurrent indexing?", - expectedPath: "src/lib/utils/lock.ts", - note: "LOCK file acquisition and stale process detection.", - }, - { - query: "Where is DEFAULT_IGNORE_PATTERNS defined for indexing?", - expectedPath: "src/lib/index/ignore-patterns.ts", - note: "DEFAULT_IGNORE_PATTERNS with lockfiles and secrets.", - }, - { - query: "Which INDEXABLE_EXTENSIONS are allowed and what is the 10MB limit?", - expectedPath: "src/config.ts", - note: "INDEXABLE_EXTENSIONS and MAX_FILE_SIZE_BYTES.", - }, - { - query: "How do we reset when VectorDB and meta cache disagree?", - expectedPath: "src/lib/index/syncer.ts", - note: "Inconsistency detection that forces drop + rebuild.", - }, - { - query: "How are batches flushed to LanceDB before updating meta cache?", - expectedPath: "src/lib/index/syncer.ts", - note: "flushBatch writes VectorDB first, then meta entries.", - }, - { - query: "How do we remove stale or deleted paths from the index?", - expectedPath: "src/lib/index/syncer.ts", - note: "Cleanup of stale paths after scanning is finished.", - }, - { - query: "How do we skip unchanged files using mtime/size hashes?", - expectedPath: "src/lib/index/syncer.ts", - note: "Meta cache check to bypass re-embedding identical files.", - }, - { - query: - "When does processFile mark shouldDelete for binary, empty, or too-big files?", - expectedPath: "src/lib/workers/orchestrator.ts", - note: "processFile returns shouldDelete for non-indexable snapshots.", - }, - { - query: "How is the file hash computed for change detection?", - expectedPath: "src/lib/utils/file-utils.ts", - note: "computeBufferHash SHA-256 helper.", - }, - { - query: "How do we snapshot a file and verify it didn't change during read?", - expectedPath: "src/lib/utils/file-utils.ts", - note: "readFileSnapshot double-checks size/mtime before returning.", - }, - - // --- Storage & Schema --- - { - query: "Where is the LanceDB schema defined, including pooled_colbert_48d?", - expectedPath: "src/lib/store/vector-db.ts", - note: "Schema with dense, colbert, and pooled embeddings.", - }, - { - query: "How do we warn about schema mismatches and ask for a reindex?", - expectedPath: "src/lib/store/vector-db.ts", - note: "insertBatch error message for field mismatches.", - }, - { - query: "Where do we create the full-text index on chunk content?", - expectedPath: "src/lib/store/vector-db.ts", - note: "createFTSIndex invoking LanceDB FTS.", - }, - - // --- CLI Commands --- - { - query: - "How does the search command trigger initial indexing when the store is empty?", - expectedPath: "src/commands/search.ts", - note: "Checks hasAnyRows and runs initialSync + spinner.", - }, - { - query: "Where does search --dry-run print formatDryRunSummary?", - expectedPath: "src/commands/search.ts", - note: "formatDryRunSummary usage for dry-run summaries.", - }, - { - query: - "How does the index command handle --reset then call createFTSIndex?", - expectedPath: "src/commands/index.ts", - note: "Indexing workflow before createFTSIndex.", - }, - { - query: "How does serve reject search paths outside the project root?", - expectedPath: "src/commands/serve.ts", - note: "Path normalization rejecting traversal outside projectRoot.", - }, - { - query: "Where does the server enforce a 1MB payload size limit?", - expectedPath: "src/commands/serve.ts", - note: "Request body guard that 413s payloads over 1MB.", - }, - { - query: - "How does serve --background redirect logs to ~/.osgrep/logs/server.log?", - expectedPath: "src/commands/serve.ts", - note: "Background flag redirecting stdio to server.log.", - }, - { - query: "Where does setup ensureSetup runs and grammars get downloaded?", - expectedPath: "src/commands/setup.ts", - note: "Setup command invoking ensureSetup and ensureGrammars.", - }, - { - query: "How does doctor check PATHS.models for missing model directories?", - expectedPath: "src/commands/doctor.ts", - note: "Health checks for PATHS.models and MODEL_IDS.", - }, - { - query: "Where is Claude Code plugin installation defined?", - expectedPath: "src/commands/claude-code.ts", - note: "Marketplace add + install flow.", - }, - - // --- Paths, Config, Environment --- - { - query: "How do we create .osgrep directories and add them to .gitignore?", - expectedPath: "src/lib/utils/project-root.ts", - note: "ensureProjectPaths scaffolds directories and gitignore entry.", - }, - { - query: "How is the project root detected via .git or existing .osgrep?", - expectedPath: "src/lib/utils/project-root.ts", - note: "findProjectRoot walking parents and honoring repo roots.", - }, - { - query: "Where are PATHS.globalRoot, models, and grammars defined?", - expectedPath: "src/config.ts", - note: "PATHS pointing to ~/.osgrep directories.", - }, - { - query: "How do workers prefer a local ./models directory when present?", - expectedPath: "src/lib/workers/orchestrator.ts", - note: "env.localModelPath override when repo ships models.", - }, - { - query: "Where are VECTOR_DIM, COLBERT_DIM, and WORKER_THREADS configured?", - expectedPath: "src/config.ts", - note: "CONFIG with VECTOR_DIM, COLBERT_DIM, WORKER_THREADS.", - }, - - // --- Extended Coverage --- - { - query: "Where do we read WORKER_TIMEOUT_MS from OSGREP_WORKER_TIMEOUT_MS?", - expectedPath: "src/config.ts", - note: "WORKER_TIMEOUT_MS env override.", - }, - { - query: "Where is TASK_TIMEOUT_MS set for worker tasks?", - expectedPath: "src/config.ts", - note: "Worker-related timeout env vars live in config.", - }, - { - query: - "How do we cap worker threads from OSGREP_WORKER_THREADS with a HARD_CAP of 4?", - expectedPath: "src/config.ts", - note: "DEFAULT_WORKER_THREADS calculation.", - }, - { - query: "Where do we set HF transformers cacheDir and allowLocalModels?", - expectedPath: "src/lib/workers/orchestrator.ts", - note: "env.cacheDir and env.allowLocalModels toggles.", - }, - { - query: "Where do we load Granite ONNX with CPU execution providers?", - expectedPath: "src/lib/native/index.ts", - note: "Inference is performed in the native Rust core via ONNX Runtime.", - }, - { - query: "Where do we limit ColBERT ONNX runtime threads to 1?", - expectedPath: "src/lib/native/index.ts", - note: "Threading is configured inside the native Rust core.", - }, - { - query: - "How do we normalize ColBERT doc vectors and quantize to int8 scale?", - expectedPath: "src/lib/native/index.ts", - note: "Docs are stored as INT8 ColBERT grids produced by native core.", - }, - { - query: "Where do we normalize ColBERT query rows before building matrix?", - expectedPath: "src/lib/workers/orchestrator.ts", - note: "encodeQuery normalizes ONNX output rows.", - }, - { - query: - "Where do we convert serialized Buffer objects to Int8Array for rerank?", - expectedPath: "src/lib/workers/orchestrator.ts", - note: "rerank converts Buffer/object into Int8Array.", - }, - { - query: "Where do we build UUIDs for chunk ids before inserting to LanceDB?", - expectedPath: "src/lib/workers/orchestrator.ts", - note: "toPreparedChunks uses uuidv4 for chunk IDs.", - }, - { - query: - "How do we include imports, exports, and top comments in anchor chunks?", - expectedPath: "src/lib/index/chunker.ts", - note: "buildAnchorChunk composes sections with metadata.", - }, - { - query: "Where do we warn about missing tree-sitter grammars and fall back?", - expectedPath: "src/lib/index/chunker.ts", - note: "chunk() logs and falls back when getLanguage fails.", - }, - { - query: "Where do we split oversized chunks with line and char overlaps?", - expectedPath: "src/lib/index/chunker.ts", - note: "splitIfTooBig uses OVERLAP_LINES and OVERLAP_CHARS.", - }, - { - query: "Where is GRAMMARS_DIR set to ~/.osgrep/grammars?", - expectedPath: "src/lib/index/grammar-loader.ts", - note: "GRAMMARS_DIR constant.", - }, - { - query: "Where do we download grammars with fetch and a custom User-Agent?", - expectedPath: "src/lib/index/grammar-loader.ts", - note: "ensureGrammars downloadFile helper.", - }, - { - query: "Where do we guard against files changing during read?", - expectedPath: "src/lib/utils/file-utils.ts", - note: "readFileSnapshot compares pre/post stats.", - }, - { - query: "Where do we detect null bytes before indexing content?", - expectedPath: "src/lib/utils/file-utils.ts", - note: "hasNullByte check in processFile path.", - }, - { - query: "Where do we register cleanup tasks and execute them at exit?", - expectedPath: "src/lib/utils/cleanup.ts", - note: "registerCleanup and runCleanup functions.", - }, - { - query: "Where does gracefulExit destroy the worker pool before exiting?", - expectedPath: "src/lib/utils/exit.ts", - note: "gracefulExit calls destroyWorkerPool and runCleanup.", - }, - { - query: "Where is the LMDB meta cache opened with compression?", - expectedPath: "src/lib/store/meta-cache.ts", - note: "MetaCache constructor uses lmdb open() with compression.", - }, - { - query: "Where do we connect to LanceDB and seed the table schema?", - expectedPath: "src/lib/store/vector-db.ts", - note: "ensureTable creates schema and deletes seed row.", - }, - { - query: "Where do we drop the LanceDB table during resets?", - expectedPath: "src/lib/store/vector-db.ts", - note: "drop() helper invoked on reset.", - }, - { - query: "Where do we close LanceDB connections and unregister cleanup?", - expectedPath: "src/lib/store/vector-db.ts", - note: "close() method clears connections and cleanup hook.", - }, - { - query: "Where do we use fast-glob to stream files for indexing?", - expectedPath: "src/lib/index/syncer.ts", - note: "fg.stream with glob options for repo walk.", - }, - { - query: "Where do we skip duplicate real paths and broken symlinks?", - expectedPath: "src/lib/index/syncer.ts", - note: "visitedRealPaths plus try/catch around realpathSync.", - }, - { - query: "Where do we abort indexing when AbortSignal is triggered?", - expectedPath: "src/lib/index/syncer.ts", - note: "Checks signal.aborted to stop scheduling.", - }, - { - query: - "Where do we flush batches when batch/deletes/meta reach batchLimit?", - expectedPath: "src/lib/index/syncer.ts", - note: "flush() checks batchLimit based on EMBED_BATCH_SIZE.", - }, - { - query: - "Where do we detect stale cached paths and delete them after indexing?", - expectedPath: "src/lib/index/syncer.ts", - note: "Removes stale paths from VectorDB and meta cache.", - }, - { - query: - "Where do we detect inconsistent VectorDB vs meta cache and force rebuild?", - expectedPath: "src/lib/index/syncer.ts", - note: "isInconsistent triggers drop and meta reset.", - }, - { - query: - "Where is createIndexingSpinner updating text for scanning and indexing files?", - expectedPath: "src/lib/index/sync-helpers.ts", - note: "createIndexingSpinner onProgress formatting.", - }, - { - query: - "Where does ensureSetup create ~/.osgrep directories with ora spinner?", - expectedPath: "src/lib/setup/setup-helpers.ts", - note: "ensureSetup directory creation feedback.", - }, - { - query: - "Where do we download models via a worker thread to avoid ONNX in main thread?", - expectedPath: "src/lib/setup/model-loader.ts", - note: "downloadModels spawns worker with ts-node/register when dev.", - }, - { - query: "Where do we check areModelsDownloaded before running setup?", - expectedPath: "src/lib/setup/model-loader.ts", - note: "areModelsDownloaded verifies cache directories.", - }, - { - query: - "Where does setup command list model and grammar status after finishing?", - expectedPath: "src/commands/setup.ts", - note: "Setup command status output with model IDs.", - }, - { - query: "Where does doctor print system platform, arch, and Node version?", - expectedPath: "src/commands/doctor.ts", - note: "Doctor command system info logging.", - }, - { - query: "Where does the list command calculate directory sizes recursively?", - expectedPath: "src/commands/list.ts", - note: "getDirectorySize walk.", - }, - { - query: "Where does the list command format sizes and time ago text?", - expectedPath: "src/commands/list.ts", - note: "formatSize and formatDate helpers.", - }, - { - query: "Where does serve register running servers to servers.json?", - expectedPath: "src/lib/utils/server-registry.ts", - note: "registerServer writes to ~/.osgrep/servers.json.", - }, - { - query: "How does serve status enumerate active servers?", - expectedPath: "src/commands/serve.ts", - note: "serve status subcommand uses listServers().", - }, - { - query: "How does serve stop --all kill background servers?", - expectedPath: "src/commands/serve.ts", - note: "serve stop iterates listServers and SIGTERMs.", - }, - { - query: "Where is the LOCK file written and stale PID detection handled?", - expectedPath: "src/lib/utils/lock.ts", - note: "acquireWriterLock parses existing lock with pid/start time.", - }, - { - query: "Where do we parse .git worktree files to find the main repo root?", - expectedPath: "src/lib/utils/git.ts", - note: "getMainRepoRoot and getGitCommonDir for worktrees.", - }, - { - query: "Where do we format search results in plain mode for agents?", - expectedPath: "src/lib/utils/formatter.ts", - note: "formatTextResults plain mode with agent tags.", - }, - { - query: "Where do we apply syntax highlighting for human output?", - expectedPath: "src/lib/utils/formatter.ts", - note: "formatTextResults uses cli-highlight when not plain.", - }, - { - query: - "Where do we merge nearby snippets from the same file before printing?", - expectedPath: "src/lib/utils/formatter.ts", - note: "Smart stitching merges overlapping chunks per file.", - }, - { - query: - "Where are search CLI options like --scores, --compact, --per-file handled?", - expectedPath: "src/commands/search.ts", - note: "Commander options declared for search command.", - }, - { - query: "Where is the search path argument normalized against project root?", - expectedPath: "src/commands/search.ts", - note: "Relative path handling before searcher.search.", - }, -]; - -const topK = 20; - -export function evaluateCase( - response: SearchResponse, - evalCase: EvalCase, - timeMs: number, -): EvalResult { - const expectedPaths = evalCase.expectedPath - .split("|") - .map((p) => p.trim().toLowerCase()) - .filter(Boolean); - - const rank = response.data.findIndex((chunk) => { - const path = chunk.metadata?.path?.toLowerCase() || ""; - return expectedPaths.some((expected) => path.includes(expected)); - }); - - const avoidPath = (evalCase.avoidPath ?? DEFAULT_AVOID_PATH).toLowerCase(); - const avoidRank = response.data.findIndex((chunk) => { - const path = chunk.metadata?.path?.toLowerCase() || ""; - return avoidPath ? path.includes(avoidPath) : false; - }); - - const hitAvoid = avoidRank >= 0 && (rank === -1 || avoidRank < rank); - const found = rank >= 0 && !hitAvoid; - const rr = found ? 1 / (rank + 1) : 0; - const recall = found && rank < 10 ? 1 : 0; - - return { - rr, - found, - recall, - path: evalCase.expectedPath, - query: evalCase.query, - note: evalCase.note, - timeMs, - }; -} - -async function run() { - const root = process.cwd(); - const searchRoot = root; - const projectRoot = findProjectRoot(searchRoot) ?? searchRoot; - const paths = ensureProjectPaths(projectRoot); - const vectorDb = new VectorDB(paths.lancedbDir); - const searcher = new Searcher(vectorDb); - - // 1. Ensure the store exists (VectorDB handles creation, but we check for data) - const hasRows = await vectorDb.hasAnyRows(); - if (!hasRows) { - console.error(`❌ Store appears to be empty!`); - console.error(` Run "osgrep index" to populate the store with data.`); - process.exit(1); - } - - // 2. Check if store has data (redundant but good for sanity) - try { - const testResult = await searcher.search("test", 1, { rerank: true }); - if (testResult.data.length === 0) { - console.error( - `⚠️ Store appears to be empty (search returned 0 results)!`, - ); - console.error(` Run "osgrep index" to populate the store with data.`); - process.exit(1); - } - } catch (err) { - console.error(`❌ Error checking store data:`, err); - process.exit(1); - } - - const results: EvalResult[] = []; - - console.log("Starting evaluation...\n"); - const startTime = performance.now(); - - for (const c of cases) { - const queryStart = performance.now(); - const res = await searcher.search(c.query, topK, { rerank: false }); - const queryEnd = performance.now(); - const timeMs = queryEnd - queryStart; - - results.push(evaluateCase(res, c, timeMs)); - } - - const totalTime = performance.now() - startTime; - const mrr = results.reduce((sum, r) => sum + r.rr, 0) / results.length; - const recallAt10 = - results.reduce((sum, r) => sum + r.recall, 0) / results.length; - const avgTime = - results.reduce((sum, r) => sum + r.timeMs, 0) / results.length; - - console.log("=".repeat(80)); - console.log(`Eval results for store at: ${paths.lancedbDir}`); - console.log("=".repeat(80)); - results.forEach((r) => { - const status = r.found ? `rank ${(1 / r.rr).toFixed(0)}` : "❌ missed"; - const emoji = r.found ? (r.rr === 1 ? "🎯" : "✓") : "❌"; - console.log(`${emoji} ${r.query}`); - console.log( - ` => ${status} (target: ${r.path}) [${r.timeMs.toFixed(0)}ms]`, - ); - if (r.note) { - console.log(` // ${r.note}`); - } - }); - console.log("=".repeat(80)); - console.log(`MRR: ${mrr.toFixed(3)}`); - console.log(`Recall@10: ${recallAt10.toFixed(3)}`); - console.log(`Avg query time: ${avgTime.toFixed(0)}ms`); - console.log(`Total time: ${totalTime.toFixed(0)}ms`); - console.log( - `Found: ${results.filter((r) => r.found).length}/${results.length}`, - ); - console.log("=".repeat(80)); - - await gracefulExit(0); -} - -if ( - // Only auto-run when executed directly (not when imported for experiments/tests) - require.main === module && - process.env.OSGREP_EVAL_AUTORUN !== "0" -) { - run().catch((err) => { - console.error("Eval failed:", err); - gracefulExit(1); - }); -} From 95d88fb57f32cdfd1896da41c8303c47fc6fedac Mon Sep 17 00:00:00 2001 From: Ryan D'Onofrio <97113725+Ryandonofrio3@users.noreply.github.com> Date: Sat, 13 Dec 2025 19:52:48 -0800 Subject: [PATCH 07/19] build --- .github/workflows/ci.yml | 63 ++++++++++--- .github/workflows/release.yml | 129 ++++++++++++++++++++++----- .gitignore | 1 + osgrep-core/package.json | 23 ++++- src/lib/index/chunker.ts | 2 +- src/lib/index/grammar-loader.ts | 2 +- src/lib/output/formatter.ts | 19 +++- src/lib/output/json-formatter.ts | 16 ---- src/lib/{core => store}/languages.ts | 0 src/lib/utils/formatter.ts | 2 +- 10 files changed, 198 insertions(+), 59 deletions(-) delete mode 100644 src/lib/output/json-formatter.ts rename src/lib/{core => store}/languages.ts (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f0de95a..c7e15210 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,39 +2,74 @@ name: CI on: pull_request: - branches: [ main ] + branches: [main] push: - branches: [ main ] + branches: [main] jobs: - ci: + # Build native module on Linux (fast sanity check) + build-native: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - - name: Setup pnpm - uses: pnpm/action-setup@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust + uses: Swatinem/rust-cache@v2 + with: + workspaces: osgrep-core + + - name: Install napi-rs CLI + run: npm install -g @napi-rs/cli + + - name: Build native module + working-directory: osgrep-core + run: napi build --platform --release + + - name: Upload artifact + uses: actions/upload-artifact@v4 with: - version: 9 - run_install: false + name: native-linux + path: osgrep-core/*.node + + # TypeScript checks and tests + ci: + runs-on: ubuntu-latest + needs: build-native + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup bun + uses: oven-sh/setup-bun@v2 - name: Setup Node uses: actions/setup-node@v4 with: node-version: '20' - cache: 'pnpm' - cache-dependency-path: 'pnpm-lock.yaml' + + - name: Download native module + uses: actions/download-artifact@v4 + with: + name: native-linux + path: osgrep-core - name: Install dependencies - run: pnpm install --frozen-lockfile + run: bun install - name: Check types - run: pnpm run typecheck + run: bun run typecheck - name: Run tests - run: pnpm run test + run: bun run test - name: Build - run: pnpm run build - + run: bun run build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 66336a7c..7343b46c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,34 +6,106 @@ on: - 'v*.*.*' workflow_dispatch: +env: + CARGO_INCREMENTAL: 0 + jobs: - publish-npm: + # ============================================================================= + # Build native binaries for each platform + # ============================================================================= + build-native: + strategy: + fail-fast: false + matrix: + include: + - target: aarch64-apple-darwin + os: macos-latest + - target: x86_64-apple-darwin + os: macos-latest + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - target: x86_64-unknown-linux-musl + os: ubuntu-latest + use-cross: true + - target: x86_64-pc-windows-msvc + os: windows-latest + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Install dependencies (Linux musl) + if: matrix.use-cross + run: | + sudo apt-get update + sudo apt-get install -y musl-tools + + - name: Install napi-rs CLI + run: npm install -g @napi-rs/cli + + - name: Build native module + working-directory: osgrep-core + run: napi build --platform --release --target ${{ matrix.target }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: bindings-${{ matrix.target }} + path: osgrep-core/*.node + if-no-files-found: error + + # ============================================================================= + # Publish platform packages + meta package + osgrep + # ============================================================================= + publish: runs-on: ubuntu-latest + needs: build-native permissions: contents: write id-token: write + steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 9 - run_install: false - - name: Setup Node uses: actions/setup-node@v4 with: node-version: '20' registry-url: 'https://registry.npmjs.org' - cache: 'pnpm' - cache-dependency-path: 'pnpm-lock.yaml' + + - name: Install napi-rs CLI + run: npm install -g @napi-rs/cli + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: osgrep-core/artifacts + + - name: Move artifacts to osgrep-core + run: | + cd osgrep-core + for dir in artifacts/bindings-*/; do + mv "$dir"*.node . 2>/dev/null || true + done + rm -rf artifacts + ls -la *.node - name: Verify tag commit is on main - working-directory: . run: | git fetch origin main --depth=1 if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then @@ -41,12 +113,6 @@ jobs: exit 1 fi - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Check types - run: pnpm run typecheck - - name: Verify tag matches package.json version run: | TAG="${GITHUB_REF##*/}" @@ -56,15 +122,36 @@ jobs: exit 1 fi - - name: Build - run: pnpm build + - name: Publish osgrep-core platform packages + working-directory: osgrep-core + run: napi prepublish -t npm + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Publish osgrep-core meta package + working-directory: osgrep-core + run: npm publish --access public --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Install bun + uses: oven-sh/setup-bun@v2 + + - name: Install osgrep dependencies + run: bun install + + - name: Check types + run: bun run typecheck + + - name: Build osgrep + run: bun run build - - name: Publish to npm - run: pnpm publish --access public --provenance --no-git-checks + - name: Publish osgrep to npm + run: npm publish --access public --provenance env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Create GitHub Release (tag only, no assets) + - name: Create GitHub Release uses: softprops/action-gh-release@v1 with: tag_name: ${{ github.ref_name }} diff --git a/.gitignore b/.gitignore index 17e4973d..22cabba0 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ osgrep-core/target/ **/target/ osgrep-core/*.node osgrep-core/bench/ +.osgrep diff --git a/osgrep-core/package.json b/osgrep-core/package.json index 123a97ec..69da5677 100644 --- a/osgrep-core/package.json +++ b/osgrep-core/package.json @@ -1,13 +1,19 @@ { "name": "osgrep-core", "version": "0.1.0", + "description": "Native Rust core for osgrep (embeddings + reranking via ONNX)", "type": "module", "main": "index.js", "types": "index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/Ryandonofrio3/osgrep.git", + "directory": "osgrep-core" + }, + "license": "Apache-2.0", "files": [ "index.js", - "index.d.ts", - "*.node" + "index.d.ts" ], "napi": { "name": "osgrep-core", @@ -17,15 +23,24 @@ "aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc" ] } }, "scripts": { "build": "napi build --platform", - "build:release": "napi build --platform --release" + "build:release": "napi build --platform --release", + "prepublishOnly": "napi prepublish -t npm" }, "devDependencies": { - "@napi-rs/cli": "^3.5.0" + "@napi-rs/cli": "^3.0.0-alpha.63" + }, + "optionalDependencies": { + "@osgrep-core/darwin-arm64": "0.1.0", + "@osgrep-core/darwin-x64": "0.1.0", + "@osgrep-core/linux-x64-gnu": "0.1.0", + "@osgrep-core/linux-x64-musl": "0.1.0", + "@osgrep-core/win32-x64-msvc": "0.1.0" } } diff --git a/src/lib/index/chunker.ts b/src/lib/index/chunker.ts index 83251145..a3635538 100644 --- a/src/lib/index/chunker.ts +++ b/src/lib/index/chunker.ts @@ -1,7 +1,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { CONFIG } from "../../config"; -import { getLanguageByExtension } from "../core/languages"; +import { getLanguageByExtension } from "../store/languages"; import { GRAMMARS_DIR } from "./grammar-loader"; // web-tree-sitter ships a CommonJS build diff --git a/src/lib/index/grammar-loader.ts b/src/lib/index/grammar-loader.ts index 4c91fd50..6ef9feef 100644 --- a/src/lib/index/grammar-loader.ts +++ b/src/lib/index/grammar-loader.ts @@ -2,7 +2,7 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; -import { LANGUAGES } from "../core/languages"; +import { LANGUAGES } from "../store/languages"; export const GRAMMARS_DIR = path.join(os.homedir(), ".osgrep", "grammars"); diff --git a/src/lib/output/formatter.ts b/src/lib/output/formatter.ts index 09f78750..2a1ca999 100644 --- a/src/lib/output/formatter.ts +++ b/src/lib/output/formatter.ts @@ -1,6 +1,6 @@ import * as path from "node:path"; import { highlight } from "cli-highlight"; -import { getLanguageByExtension } from "../core/languages"; +import { getLanguageByExtension } from "../store/languages"; import type { ChunkType, FileMetadata } from "../store/types"; const useColors = process.stdout.isTTY && !process.env.NO_COLOR; @@ -116,3 +116,20 @@ export function formatResults( if (results.length === 0) return "No results found."; return results.map((r) => formatResult(r, root, options)).join("\n\n"); } + + +export interface JsonOutput { + results?: ChunkType[]; + hits?: unknown[]; + tsv?: string; + format?: string; + metadata?: { + count: number; + query?: string; + }; +} + +export function formatJson(data: JsonOutput): string { + return JSON.stringify(data, null, 2); +} + diff --git a/src/lib/output/json-formatter.ts b/src/lib/output/json-formatter.ts deleted file mode 100644 index 8a5e2dec..00000000 --- a/src/lib/output/json-formatter.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { ChunkType } from "../store/types"; - -export interface JsonOutput { - results?: ChunkType[]; - hits?: unknown[]; - tsv?: string; - format?: string; - metadata?: { - count: number; - query?: string; - }; -} - -export function formatJson(data: JsonOutput): string { - return JSON.stringify(data, null, 2); -} diff --git a/src/lib/core/languages.ts b/src/lib/store/languages.ts similarity index 100% rename from src/lib/core/languages.ts rename to src/lib/store/languages.ts diff --git a/src/lib/utils/formatter.ts b/src/lib/utils/formatter.ts index 33be0212..99a3bd4d 100644 --- a/src/lib/utils/formatter.ts +++ b/src/lib/utils/formatter.ts @@ -17,7 +17,7 @@ const style = { blue: (s: string) => `\x1b[34m${s}\x1b[39m`, }; -import { getLanguageByExtension } from "../core/languages"; +import { getLanguageByExtension } from "../store/languages"; function detectLanguage(filePath: string): string { const ext = path.extname(filePath); From 16e6c103f3b7d0eef030000f98d8f248791bf67f Mon Sep 17 00:00:00 2001 From: Ryan D'Onofrio <97113725+Ryandonofrio3@users.noreply.github.com> Date: Sat, 13 Dec 2025 19:59:12 -0800 Subject: [PATCH 08/19] mild cleanup --- src/lib/output/formatter.ts | 28 +--------------------------- src/lib/utils/ansi.ts | 10 ++++++++++ src/lib/utils/formatter.ts | 11 ++--------- 3 files changed, 13 insertions(+), 36 deletions(-) create mode 100644 src/lib/utils/ansi.ts diff --git a/src/lib/output/formatter.ts b/src/lib/output/formatter.ts index 2a1ca999..dc2d2647 100644 --- a/src/lib/output/formatter.ts +++ b/src/lib/output/formatter.ts @@ -2,17 +2,7 @@ import * as path from "node:path"; import { highlight } from "cli-highlight"; import { getLanguageByExtension } from "../store/languages"; import type { ChunkType, FileMetadata } from "../store/types"; - -const useColors = process.stdout.isTTY && !process.env.NO_COLOR; - -const style = { - bold: (s: string) => (useColors ? `\x1b[1m${s}\x1b[22m` : s), - dim: (s: string) => (useColors ? `\x1b[2m${s}\x1b[22m` : s), - green: (s: string) => (useColors ? `\x1b[32m${s}\x1b[39m` : s), - blue: (s: string) => (useColors ? `\x1b[34m${s}\x1b[39m` : s), - cyan: (s: string) => (useColors ? `\x1b[36m${s}\x1b[39m` : s), - gray: (s: string) => (useColors ? `\x1b[90m${s}\x1b[39m` : s), -}; +import { style } from "../utils/ansi"; function detectLanguage(filePath: string): string { const ext = path.extname(filePath); @@ -117,19 +107,3 @@ export function formatResults( return results.map((r) => formatResult(r, root, options)).join("\n\n"); } - -export interface JsonOutput { - results?: ChunkType[]; - hits?: unknown[]; - tsv?: string; - format?: string; - metadata?: { - count: number; - query?: string; - }; -} - -export function formatJson(data: JsonOutput): string { - return JSON.stringify(data, null, 2); -} - diff --git a/src/lib/utils/ansi.ts b/src/lib/utils/ansi.ts new file mode 100644 index 00000000..03c4fe5c --- /dev/null +++ b/src/lib/utils/ansi.ts @@ -0,0 +1,10 @@ +const useColors = process.stdout.isTTY && !process.env.NO_COLOR; + +export const style = { + bold: (s: string) => (useColors ? `\x1b[1m${s}\x1b[22m` : s), + dim: (s: string) => (useColors ? `\x1b[2m${s}\x1b[22m` : s), + green: (s: string) => (useColors ? `\x1b[32m${s}\x1b[39m` : s), + blue: (s: string) => (useColors ? `\x1b[34m${s}\x1b[39m` : s), + cyan: (s: string) => (useColors ? `\x1b[36m${s}\x1b[39m` : s), + gray: (s: string) => (useColors ? `\x1b[90m${s}\x1b[39m` : s), +}; diff --git a/src/lib/utils/formatter.ts b/src/lib/utils/formatter.ts index 99a3bd4d..26b3022d 100644 --- a/src/lib/utils/formatter.ts +++ b/src/lib/utils/formatter.ts @@ -1,5 +1,7 @@ import * as path from "node:path"; import { highlight } from "cli-highlight"; +import { getLanguageByExtension } from "../store/languages"; +import { style } from "./ansi"; export interface TextResult { path: string; @@ -10,15 +12,6 @@ export interface TextResult { end_line: number; } -const style = { - bold: (s: string) => `\x1b[1m${s}\x1b[22m`, - dim: (s: string) => `\x1b[2m${s}\x1b[22m`, - green: (s: string) => `\x1b[32m${s}\x1b[39m`, - blue: (s: string) => `\x1b[34m${s}\x1b[39m`, -}; - -import { getLanguageByExtension } from "../store/languages"; - function detectLanguage(filePath: string): string { const ext = path.extname(filePath); const lang = getLanguageByExtension(ext); From c6fcc3c2a171b5c3c9fcfb7141ded0e210e883bc Mon Sep 17 00:00:00 2001 From: Ryan D'Onofrio <97113725+Ryandonofrio3@users.noreply.github.com> Date: Sat, 13 Dec 2025 20:04:46 -0800 Subject: [PATCH 09/19] error fixing --- src/lib/index/chunker.ts | 2 +- src/lib/native/index.ts | 7 ++++--- src/lib/workers/orchestrator.ts | 3 +++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/lib/index/chunker.ts b/src/lib/index/chunker.ts index a3635538..fa586a83 100644 --- a/src/lib/index/chunker.ts +++ b/src/lib/index/chunker.ts @@ -119,7 +119,7 @@ export function formatChunkText( const sections: string[] = []; // 1. File path (always first) - sections.push(`// ${filePath}`); + sections.push(`// File: ${filePath}`); // 2. Imports (if available) if (chunk.imports && chunk.imports.length > 0) { diff --git a/src/lib/native/index.ts b/src/lib/native/index.ts index 64c30565..fae522de 100644 --- a/src/lib/native/index.ts +++ b/src/lib/native/index.ts @@ -45,10 +45,11 @@ export async function initNative(): Promise<void> { } initialized = true; - })(); + })().finally(() => { + initPromise = null; + }); - await initPromise; - initPromise = null; + return initPromise; } /** diff --git a/src/lib/workers/orchestrator.ts b/src/lib/workers/orchestrator.ts index bd0da040..6c8ec841 100644 --- a/src/lib/workers/orchestrator.ts +++ b/src/lib/workers/orchestrator.ts @@ -268,6 +268,9 @@ export class WorkerOrchestrator { colbert = col; } else if (Buffer.isBuffer(col)) { colbert = new Int8Array(col.buffer, col.byteOffset, col.byteLength); + } else if (ArrayBuffer.isView(col)) { + // Handles Uint8Array and other typed arrays (e.g. from LanceDB) + colbert = new Int8Array(col.buffer, col.byteOffset, col.byteLength); } else if (Array.isArray(col)) { colbert = new Int8Array(col); } else { From 6257b57d7d1b7beb09cc327d211a5d98c70600d0 Mon Sep 17 00:00:00 2001 From: Ryan D'Onofrio <97113725+Ryandonofrio3@users.noreply.github.com> Date: Sat, 13 Dec 2025 20:09:50 -0800 Subject: [PATCH 10/19] cleanup tests --- README.md | 2 +- src/lib/native/index.ts | 11 +---------- tests/chunking.test.ts | 3 +-- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 415a46f2..bbe6a6b3 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Natural-language search that works like `grep`. Fast, local, and built for codin - **Semantic:** Finds concepts ("where do transactions get created?"), not just strings. - **Role Detection:** Distinguishes `ORCHESTRATION` (high-level logic) from `DEFINITION` (types/classes). -- **Local & Private:** 100% local embeddings via `onnxruntime-node`. +- **Local & Private:** 100% local embeddings via `onnxruntime`. - **Auto-Isolated:** Each repository gets its own index automatically. - **Agent-Ready:** Native output with symbols and roles. diff --git a/src/lib/native/index.ts b/src/lib/native/index.ts index fae522de..d9632ea1 100644 --- a/src/lib/native/index.ts +++ b/src/lib/native/index.ts @@ -1,13 +1,4 @@ -/** - * Native bindings to osgrep-core (Rust) - * - * This module provides the bridge to the native Rust code for: - * - Dense embeddings (384-dim, granite-30m) - * - ColBERT embeddings (48-dim per token) - * - ColBERT reranking - * - * All ML inference happens in Rust via ONNX Runtime for maximum speed. - */ + import { CONFIG, MODEL_IDS } from "../../config"; diff --git a/tests/chunking.test.ts b/tests/chunking.test.ts index a08812dc..d9e9ecb7 100644 --- a/tests/chunking.test.ts +++ b/tests/chunking.test.ts @@ -71,8 +71,7 @@ function example() {}`; }, "/repo/path/file.ts", ); - expect(displayText).toContain("// /repo/path/file.ts"); - expect(displayText).toContain("File: /repo/path/file.ts"); + expect(displayText).toContain("// File: /repo/path/file.ts"); expect(displayText).toContain("code"); }); }); From 69a94c99dafe20bfbc4765bbd996e7fe1146757e Mon Sep 17 00:00:00 2001 From: Ryan D'Onofrio <97113725+Ryandonofrio3@users.noreply.github.com> Date: Sat, 13 Dec 2025 20:14:39 -0800 Subject: [PATCH 11/19] memory leak fix --- src/commands/serve.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/commands/serve.ts b/src/commands/serve.ts index 412976a2..a0ea7ac3 100644 --- a/src/commands/serve.ts +++ b/src/commands/serve.ts @@ -70,6 +70,15 @@ export const serve = new Command("serve") // Propagate project root to worker processes process.env.OSGREP_PROJECT_ROOT = projectRoot; + // Register early to prevent race conditions where multiple sessions + // spawn servers before any finishes indexing. Port 0 = "starting up". + registerServer({ + pid: process.pid, + port: 0, + projectRoot, + startTime: Date.now(), + }); + try { await ensureSetup(); await ensureGrammars(console.log, { silent: true }); @@ -289,6 +298,7 @@ export const serve = new Command("serve") process.on("SIGINT", shutdown); process.on("SIGTERM", shutdown); } catch (error) { + unregisterServer(process.pid); const message = error instanceof Error ? error.message : "Unknown error"; console.error("Serve failed:", message); process.exitCode = 1; From b182b7b3d50d58133b10827a4d48627d684955c0 Mon Sep 17 00:00:00 2001 From: Ryan D'Onofrio <97113725+Ryandonofrio3@users.noreply.github.com> Date: Sat, 13 Dec 2025 20:18:46 -0800 Subject: [PATCH 12/19] enhance git utility functions to better handle worktree detection and common directory resolution --- src/lib/utils/git.ts | 68 +++++++++++++++++++++++++----- tests/git-worktree.test.ts | 86 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 10 deletions(-) create mode 100644 tests/git-worktree.test.ts diff --git a/src/lib/utils/git.ts b/src/lib/utils/git.ts index 1ddc9f97..a16b44ca 100644 --- a/src/lib/utils/git.ts +++ b/src/lib/utils/git.ts @@ -1,11 +1,45 @@ import * as fs from "node:fs"; import * as path from "node:path"; +function realpathOrSelf(filePath: string): string { + try { + return fs.realpathSync.native(filePath); + } catch { + return filePath; + } +} + +function parseGitdirFromGitfile(gitFilePath: string): string | null { + try { + const content = fs.readFileSync(gitFilePath, "utf-8").trim(); + if (!content.startsWith("gitdir: ")) return null; + return content.slice(8).trim(); + } catch { + return null; + } +} + export function isWorktree(dir: string): boolean { const gitPath = path.join(dir, ".git"); try { const stats = fs.statSync(gitPath); - return stats.isFile(); + + // Standard worktree: `.git` is a gitfile pointing at `.git/worktrees/<name>`. + if (stats.isFile()) { + const gitDir = parseGitdirFromGitfile(gitPath); + if (!gitDir) return false; + + const absGitDir = realpathOrSelf(path.resolve(dir, gitDir)); + return fs.existsSync(path.join(absGitDir, "commondir")); + } + + // Some tooling uses a symlinked directory for `.git` (e.g. external workspaces). + // Worktree git dirs include a `commondir` file; main repo `.git` typically does not. + if (stats.isDirectory()) { + return fs.existsSync(path.join(gitPath, "commondir")); + } + + return false; } catch { return false; } @@ -14,20 +48,34 @@ export function isWorktree(dir: string): boolean { export function getGitCommonDir(worktreeRoot: string): string | null { const gitPath = path.join(worktreeRoot, ".git"); try { - const content = fs.readFileSync(gitPath, "utf-8").trim(); - if (!content.startsWith("gitdir: ")) return null; + const stats = fs.statSync(gitPath); - const gitDir = content.slice(8).trim(); - const absGitDir = path.resolve(worktreeRoot, gitDir); + if (stats.isFile()) { + const gitDir = parseGitdirFromGitfile(gitPath); + if (!gitDir) return null; + + const absGitDir = realpathOrSelf(path.resolve(worktreeRoot, gitDir)); + + const commonDirFile = path.join(absGitDir, "commondir"); + if (fs.existsSync(commonDirFile)) { + const commonPath = fs.readFileSync(commonDirFile, "utf-8").trim(); + return realpathOrSelf(path.resolve(absGitDir, commonPath)); + } + + // Fallback: assume standard structure + return realpathOrSelf(path.resolve(absGitDir, "../../")); + } + + if (stats.isDirectory()) { + const commonDirFile = path.join(gitPath, "commondir"); + if (!fs.existsSync(commonDirFile)) return null; - const commonDirFile = path.join(absGitDir, "commondir"); - if (fs.existsSync(commonDirFile)) { const commonPath = fs.readFileSync(commonDirFile, "utf-8").trim(); - return path.resolve(absGitDir, commonPath); + const resolvedGitDir = realpathOrSelf(gitPath); + return realpathOrSelf(path.resolve(resolvedGitDir, commonPath)); } - // Fallback: assume standard structure - return path.resolve(absGitDir, "../../"); + return null; } catch { return null; } diff --git a/tests/git-worktree.test.ts b/tests/git-worktree.test.ts new file mode 100644 index 00000000..3bab62fc --- /dev/null +++ b/tests/git-worktree.test.ts @@ -0,0 +1,86 @@ +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { getGitCommonDir, getMainRepoRoot, isWorktree } from "../src/lib/utils/git"; + +function makeTempDir(prefix: string): string { + return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); +} + +function writeFile(filePath: string, content: string) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content, "utf-8"); +} + +function makeWorktreeFixture() { + const root = makeTempDir("osgrep-worktree-fixture-"); + const mainRepoRoot = path.join(root, "Documents", "GitHub", "repo"); + const worktreeRoot = path.join( + root, + "conductor", + "workspaces", + "repo", + "feature-branch", + ); + const mainGitDir = path.join(mainRepoRoot, ".git"); + const worktreeGitDir = path.join(mainGitDir, "worktrees", "feature-branch"); + + fs.mkdirSync(mainGitDir, { recursive: true }); + fs.mkdirSync(worktreeRoot, { recursive: true }); + fs.mkdirSync(worktreeGitDir, { recursive: true }); + writeFile(path.join(worktreeGitDir, "commondir"), "../.."); + + return { root, mainRepoRoot, mainGitDir, worktreeRoot, worktreeGitDir }; +} + +describe("git worktree detection", () => { + const created: string[] = []; + + afterEach(() => { + for (const dir of created.splice(0)) { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it("does not treat a normal repo root as a worktree", () => { + const { root, mainRepoRoot } = makeWorktreeFixture(); + created.push(root); + + expect(isWorktree(mainRepoRoot)).toBe(false); + expect(getGitCommonDir(mainRepoRoot)).toBeNull(); + expect(getMainRepoRoot(mainRepoRoot)).toBeNull(); + }); + + it("detects a worktree via gitfile even when outside the main repo tree", () => { + const { root, mainRepoRoot, mainGitDir, worktreeRoot, worktreeGitDir } = + makeWorktreeFixture(); + created.push(root); + + writeFile(path.join(worktreeRoot, ".git"), `gitdir: ${worktreeGitDir}\n`); + const expectedMainGitDir = fs.realpathSync.native(mainGitDir); + const expectedMainRepoRoot = fs.realpathSync.native(mainRepoRoot); + + expect(isWorktree(worktreeRoot)).toBe(true); + expect(getGitCommonDir(worktreeRoot)).toBe(expectedMainGitDir); + expect(getMainRepoRoot(worktreeRoot)).toBe(expectedMainRepoRoot); + }); + + if (process.platform === "win32") { + it.skip("detects a worktree when `.git` is a symlinked directory", () => {}); + } else { + it("detects a worktree when `.git` is a symlinked directory", () => { + const { root, mainRepoRoot, mainGitDir, worktreeRoot, worktreeGitDir } = + makeWorktreeFixture(); + created.push(root); + + fs.symlinkSync(worktreeGitDir, path.join(worktreeRoot, ".git")); + const expectedMainGitDir = fs.realpathSync.native(mainGitDir); + const expectedMainRepoRoot = fs.realpathSync.native(mainRepoRoot); + + expect(isWorktree(worktreeRoot)).toBe(true); + expect(getGitCommonDir(worktreeRoot)).toBe(expectedMainGitDir); + expect(getMainRepoRoot(worktreeRoot)).toBe(expectedMainRepoRoot); + }); + } +}); From 31797a04151e0aca1a1ab246bc0a1c73229d4f8e Mon Sep 17 00:00:00 2001 From: Ryan D'Onofrio <97113725+Ryandonofrio3@users.noreply.github.com> Date: Sat, 13 Dec 2025 20:29:17 -0800 Subject: [PATCH 13/19] little rearrangement --- src/commands/{ => agent}/claude-code.ts | 0 src/commands/{ => agent}/codex.ts | 0 src/commands/{ => agent}/droid.ts | 0 src/commands/{ => agent}/mcp.ts | 0 src/commands/{ => agent}/opencode.ts | 0 src/commands/{ => utility}/doctor.ts | 0 src/commands/{ => utility}/list.ts | 4 ++-- src/index.ts | 14 +++++++------- 8 files changed, 9 insertions(+), 9 deletions(-) rename src/commands/{ => agent}/claude-code.ts (100%) rename src/commands/{ => agent}/codex.ts (100%) rename src/commands/{ => agent}/droid.ts (100%) rename src/commands/{ => agent}/mcp.ts (100%) rename src/commands/{ => agent}/opencode.ts (100%) rename src/commands/{ => utility}/doctor.ts (100%) rename src/commands/{ => utility}/list.ts (94%) diff --git a/src/commands/claude-code.ts b/src/commands/agent/claude-code.ts similarity index 100% rename from src/commands/claude-code.ts rename to src/commands/agent/claude-code.ts diff --git a/src/commands/codex.ts b/src/commands/agent/codex.ts similarity index 100% rename from src/commands/codex.ts rename to src/commands/agent/codex.ts diff --git a/src/commands/droid.ts b/src/commands/agent/droid.ts similarity index 100% rename from src/commands/droid.ts rename to src/commands/agent/droid.ts diff --git a/src/commands/mcp.ts b/src/commands/agent/mcp.ts similarity index 100% rename from src/commands/mcp.ts rename to src/commands/agent/mcp.ts diff --git a/src/commands/opencode.ts b/src/commands/agent/opencode.ts similarity index 100% rename from src/commands/opencode.ts rename to src/commands/agent/opencode.ts diff --git a/src/commands/doctor.ts b/src/commands/utility/doctor.ts similarity index 100% rename from src/commands/doctor.ts rename to src/commands/utility/doctor.ts diff --git a/src/commands/list.ts b/src/commands/utility/list.ts similarity index 94% rename from src/commands/list.ts rename to src/commands/utility/list.ts index 67a84189..a39b44d0 100644 --- a/src/commands/list.ts +++ b/src/commands/utility/list.ts @@ -1,8 +1,8 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { Command } from "commander"; -import { gracefulExit } from "../lib/utils/exit"; -import { ensureProjectPaths, findProjectRoot } from "../lib/utils/project-root"; +import { gracefulExit } from "../../lib/utils/exit"; +import { ensureProjectPaths, findProjectRoot } from "../../lib/utils/project-root"; const style = { bold: (s: string) => `\x1b[1m${s}\x1b[22m`, diff --git a/src/index.ts b/src/index.ts index ca94ba29..5e2403d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,14 +2,14 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { program } from "commander"; -import { installClaudeCode } from "./commands/claude-code"; -import { installCodex } from "./commands/codex"; -import { doctor } from "./commands/doctor"; -import { installDroid } from "./commands/droid"; +import { installClaudeCode } from "./commands/agent/claude-code"; +import { installCodex } from "./commands/agent/codex"; +import { doctor } from "./commands/utility/doctor"; +import { installDroid } from "./commands/agent/droid"; import { index } from "./commands/index"; -import { list } from "./commands/list"; -import { mcp } from "./commands/mcp"; -import { installOpencode, uninstallOpencode } from "./commands/opencode"; +import { list } from "./commands/utility/list"; +import { mcp } from "./commands/agent/mcp"; +import { installOpencode, uninstallOpencode } from "./commands/agent/opencode"; import { search } from "./commands/search"; import { serve } from "./commands/serve"; import { setup } from "./commands/setup"; From 26273a0a2ba9c8eefb7dad1ad8edd61ca0de9555 Mon Sep 17 00:00:00 2001 From: Ryan D'Onofrio <97113725+Ryandonofrio3@users.noreply.github.com> Date: Sat, 13 Dec 2025 20:32:51 -0800 Subject: [PATCH 14/19] typecheck --- src/commands/agent/mcp.ts | 6 +++--- src/commands/utility/doctor.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/commands/agent/mcp.ts b/src/commands/agent/mcp.ts index 0276a984..3c6b7788 100644 --- a/src/commands/agent/mcp.ts +++ b/src/commands/agent/mcp.ts @@ -7,9 +7,9 @@ import { ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { Command } from "commander"; -import { initialSync } from "../lib/index/syncer"; -import { ensureSetup } from "../lib/setup/setup-helpers"; -import { ensureProjectPaths, findProjectRoot } from "../lib/utils/project-root"; +import { initialSync } from "../../lib/index/syncer"; +import { ensureSetup } from "../../lib/setup/setup-helpers"; +import { ensureProjectPaths, findProjectRoot } from "../../lib/utils/project-root"; export const mcp = new Command("mcp") .description("Start MCP server for osgrep") diff --git a/src/commands/utility/doctor.ts b/src/commands/utility/doctor.ts index 6595f6b3..0719f14c 100644 --- a/src/commands/utility/doctor.ts +++ b/src/commands/utility/doctor.ts @@ -2,10 +2,10 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { Command } from "commander"; -import { PATHS } from "../config"; -import { initNative } from "../lib/native"; -import { gracefulExit } from "../lib/utils/exit"; -import { findProjectRoot } from "../lib/utils/project-root"; +import { PATHS } from "../../config"; +import { initNative } from "../../lib/native"; +import { gracefulExit } from "../../lib/utils/exit"; +import { findProjectRoot } from "../../lib/utils/project-root"; export const doctor = new Command("doctor") .description("Check osgrep health and paths") From 816901b295eaf1d58fe39c5a45bb4d29eb0e8819 Mon Sep 17 00:00:00 2001 From: Ryan D'Onofrio <97113725+Ryandonofrio3@users.noreply.github.com> Date: Sat, 13 Dec 2025 20:55:06 -0800 Subject: [PATCH 15/19] coreML --- .gitignore | 1 + osgrep-core/Cargo.toml | 5 +++ osgrep-core/index.d.ts | 60 ---------------------------------- osgrep-core/src/colbert_ort.rs | 19 ++++++++++- osgrep-core/src/dense_ort.rs | 35 ++++++++++++++++++-- 5 files changed, 56 insertions(+), 64 deletions(-) diff --git a/.gitignore b/.gitignore index 22cabba0..e08a9c8d 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ pnpm-lock.yaml package-lock.json yarn.lock +bun.lockb # Native core (Rust / N-API) build outputs osgrep-core/target/ **/target/ diff --git a/osgrep-core/Cargo.toml b/osgrep-core/Cargo.toml index 228e24e7..16f173ba 100644 --- a/osgrep-core/Cargo.toml +++ b/osgrep-core/Cargo.toml @@ -22,6 +22,11 @@ tokenizers = "0.21" hf-hub = "0.4" # ONNX Runtime (the only ML backend we need) +# CoreML enabled on macOS for GPU acceleration +[target.'cfg(target_os = "macos")'.dependencies] +ort = { version = "2.0.0-rc.10", default-features = true, features = ["download-binaries", "coreml"] } + +[target.'cfg(not(target_os = "macos"))'.dependencies] ort = { version = "2.0.0-rc.10", default-features = true, features = ["download-binaries"] } [build-dependencies] diff --git a/osgrep-core/index.d.ts b/osgrep-core/index.d.ts index 7b588f84..e69de29b 100644 --- a/osgrep-core/index.d.ts +++ b/osgrep-core/index.d.ts @@ -1,60 +0,0 @@ -// TypeScript declarations for the `osgrep-core` native module (N-API). - -export interface DenseResult { - /** Flat array of embeddings [batch_size * 384] */ - embeddings: number[]; - /** Number of texts encoded */ - count: number; -} - -export interface ColbertPackedResult { - /** Packed embeddings as flat i8 array (all docs concatenated) */ - embeddings: Int8Array | number[]; - /** Token IDs for skiplist filtering */ - tokenIds: Uint32Array | number[]; - /** Number of tokens per document */ - lengths: Uint32Array | number[]; - /** Byte offsets into embeddings for each doc */ - offsets: Uint32Array | number[]; -} - -export interface RerankResult { - /** Original indices of top-k documents */ - indices: number[]; - /** MaxSim scores for top-k documents */ - scores: number[]; -} - -export interface EmbedResult { - /** Dense embeddings [batch_size * 384] */ - dense: number[]; - /** Packed ColBERT embeddings (i8) */ - colbertEmbeddings: Int8Array | number[]; - /** Token IDs for skiplist filtering (all docs concatenated) */ - colbertTokenIds: Uint32Array | number[]; - /** Token counts per document */ - colbertLengths: Uint32Array | number[]; - /** Byte offsets per document */ - colbertOffsets: Uint32Array | number[]; -} - -export function initModels(denseRepo: string, colbertRepo: string): void; -export function isInitialized(): boolean; - -export function embedDense(texts: string[]): DenseResult; -export function embedColbertPacked(texts: string[]): ColbertPackedResult; - -/** Returns query embeddings as a flat array [seq_len * 48]. */ -export function encodeQueryColbert(query: string): Float64Array | number[]; - -export function rerankColbert( - queryEmbeddings: Float64Array | number[], - docEmbeddings: Int8Array | number[], - docTokenIds: Uint32Array | number[], - docLengths: number[] | Uint32Array, - docOffsets: number[] | Uint32Array, - candidateIndices: number[] | Uint32Array, - topK: number, -): RerankResult; - -export function embedBatch(texts: string[]): EmbedResult; diff --git a/osgrep-core/src/colbert_ort.rs b/osgrep-core/src/colbert_ort.rs index 4d8e8731..7637e62f 100644 --- a/osgrep-core/src/colbert_ort.rs +++ b/osgrep-core/src/colbert_ort.rs @@ -4,6 +4,9 @@ use tokenizers::Tokenizer; use hf_hub::{api::sync::Api, Repo, RepoType}; use std::collections::HashSet; +#[cfg(target_os = "macos")] +use ort::execution_providers::CoreMLExecutionProvider; + fn log_native(msg: impl AsRef<str>) { // Intentionally no-op: native logging was polluting CLI output. // If you need debugging, add structured logging at the JS layer instead. @@ -62,9 +65,23 @@ impl ColbertEncoderOrt { log_native(format!("[ColBERT-ORT] Loading model from {:?}", model_path)); + // Initialize ONNX Runtime session + // On macOS, use CoreML for GPU acceleration with CPU fallback + #[cfg(target_os = "macos")] + let session = Session::builder()? + .with_execution_providers([ + CoreMLExecutionProvider::default() + .with_subgraphs(true) // Enable CoreML for subgraphs + .build(), + ])? + .with_optimization_level(GraphOptimizationLevel::Level3)? + .with_intra_threads(8)? + .commit_from_file(&model_path)?; + + #[cfg(not(target_os = "macos"))] let session = Session::builder()? .with_optimization_level(GraphOptimizationLevel::Level3)? - .with_intra_threads(8)? // Use more threads for parallel ops + .with_intra_threads(8)? .commit_from_file(&model_path)?; let tokenizer = Tokenizer::from_file(&tokenizer_path) diff --git a/osgrep-core/src/dense_ort.rs b/osgrep-core/src/dense_ort.rs index 5903a2e9..8baa33d7 100644 --- a/osgrep-core/src/dense_ort.rs +++ b/osgrep-core/src/dense_ort.rs @@ -3,6 +3,9 @@ use ort::value::Value; use tokenizers::Tokenizer; use hf_hub::{api::sync::Api, Repo, RepoType}; +#[cfg(target_os = "macos")] +use ort::execution_providers::CoreMLExecutionProvider; + fn log_native(msg: impl AsRef<str>) { // Intentionally no-op: native logging was polluting CLI output. // If you need debugging, add structured logging at the JS layer instead. @@ -33,10 +36,23 @@ impl DenseEncoderOrt { log_native(format!("[ORT] Loading model from {:?}", model_path)); - // Initialize ONNX Runtime session with CPU provider + // Initialize ONNX Runtime session + // On macOS, use CoreML for GPU acceleration with CPU fallback + #[cfg(target_os = "macos")] + let session = Session::builder()? + .with_execution_providers([ + CoreMLExecutionProvider::default() + .with_subgraphs(true) // Enable CoreML for subgraphs + .build(), + ])? + .with_optimization_level(GraphOptimizationLevel::Level3)? + .with_intra_threads(4)? + .commit_from_file(&model_path)?; + + #[cfg(not(target_os = "macos"))] let session = Session::builder()? .with_optimization_level(GraphOptimizationLevel::Level3)? - .with_intra_threads(4)? // Use 4 threads for intra-op parallelism + .with_intra_threads(4)? .commit_from_file(&model_path)?; // Load tokenizer @@ -69,7 +85,20 @@ impl DenseEncoderOrt { pub fn load(model_path: &str, tokenizer_path: &str, hidden_size: usize) -> anyhow::Result<Self> { log_native(format!("[ORT] Loading model from {}", model_path)); - // Initialize ONNX Runtime session with CPU provider + // Initialize ONNX Runtime session + // On macOS, use CoreML for GPU acceleration with CPU fallback + #[cfg(target_os = "macos")] + let session = Session::builder()? + .with_execution_providers([ + CoreMLExecutionProvider::default() + .with_subgraphs(true) + .build(), + ])? + .with_optimization_level(GraphOptimizationLevel::Level3)? + .with_intra_threads(4)? + .commit_from_file(model_path)?; + + #[cfg(not(target_os = "macos"))] let session = Session::builder()? .with_optimization_level(GraphOptimizationLevel::Level3)? .with_intra_threads(4)? From 17a9e7de55b29ca77d3253fec2e35934fa415f96 Mon Sep 17 00:00:00 2001 From: Ryan D'Onofrio <97113725+Ryandonofrio3@users.noreply.github.com> Date: Sat, 13 Dec 2025 21:13:22 -0800 Subject: [PATCH 16/19] undo core ml for now --- bun.lockb | Bin 244983 -> 171526 bytes osgrep-core/Cargo.toml | 5 --- osgrep-core/index.d.ts | 60 +++++++++++++++++++++++++++++++++ osgrep-core/src/colbert_ort.rs | 19 +---------- osgrep-core/src/dense_ort.rs | 33 ++---------------- 5 files changed, 63 insertions(+), 54 deletions(-) diff --git a/bun.lockb b/bun.lockb index fed97fd65488a2e85c94685a1c1a107ac3bf37e2..169254e1c5e4109bf9fdf1e4cd66d4e0acc200d4 100755 GIT binary patch delta 32708 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 delta 76749 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 diff --git a/osgrep-core/Cargo.toml b/osgrep-core/Cargo.toml index 16f173ba..228e24e7 100644 --- a/osgrep-core/Cargo.toml +++ b/osgrep-core/Cargo.toml @@ -22,11 +22,6 @@ tokenizers = "0.21" hf-hub = "0.4" # ONNX Runtime (the only ML backend we need) -# CoreML enabled on macOS for GPU acceleration -[target.'cfg(target_os = "macos")'.dependencies] -ort = { version = "2.0.0-rc.10", default-features = true, features = ["download-binaries", "coreml"] } - -[target.'cfg(not(target_os = "macos"))'.dependencies] ort = { version = "2.0.0-rc.10", default-features = true, features = ["download-binaries"] } [build-dependencies] diff --git a/osgrep-core/index.d.ts b/osgrep-core/index.d.ts index e69de29b..7b588f84 100644 --- a/osgrep-core/index.d.ts +++ b/osgrep-core/index.d.ts @@ -0,0 +1,60 @@ +// TypeScript declarations for the `osgrep-core` native module (N-API). + +export interface DenseResult { + /** Flat array of embeddings [batch_size * 384] */ + embeddings: number[]; + /** Number of texts encoded */ + count: number; +} + +export interface ColbertPackedResult { + /** Packed embeddings as flat i8 array (all docs concatenated) */ + embeddings: Int8Array | number[]; + /** Token IDs for skiplist filtering */ + tokenIds: Uint32Array | number[]; + /** Number of tokens per document */ + lengths: Uint32Array | number[]; + /** Byte offsets into embeddings for each doc */ + offsets: Uint32Array | number[]; +} + +export interface RerankResult { + /** Original indices of top-k documents */ + indices: number[]; + /** MaxSim scores for top-k documents */ + scores: number[]; +} + +export interface EmbedResult { + /** Dense embeddings [batch_size * 384] */ + dense: number[]; + /** Packed ColBERT embeddings (i8) */ + colbertEmbeddings: Int8Array | number[]; + /** Token IDs for skiplist filtering (all docs concatenated) */ + colbertTokenIds: Uint32Array | number[]; + /** Token counts per document */ + colbertLengths: Uint32Array | number[]; + /** Byte offsets per document */ + colbertOffsets: Uint32Array | number[]; +} + +export function initModels(denseRepo: string, colbertRepo: string): void; +export function isInitialized(): boolean; + +export function embedDense(texts: string[]): DenseResult; +export function embedColbertPacked(texts: string[]): ColbertPackedResult; + +/** Returns query embeddings as a flat array [seq_len * 48]. */ +export function encodeQueryColbert(query: string): Float64Array | number[]; + +export function rerankColbert( + queryEmbeddings: Float64Array | number[], + docEmbeddings: Int8Array | number[], + docTokenIds: Uint32Array | number[], + docLengths: number[] | Uint32Array, + docOffsets: number[] | Uint32Array, + candidateIndices: number[] | Uint32Array, + topK: number, +): RerankResult; + +export function embedBatch(texts: string[]): EmbedResult; diff --git a/osgrep-core/src/colbert_ort.rs b/osgrep-core/src/colbert_ort.rs index 7637e62f..1038c7bd 100644 --- a/osgrep-core/src/colbert_ort.rs +++ b/osgrep-core/src/colbert_ort.rs @@ -4,9 +4,6 @@ use tokenizers::Tokenizer; use hf_hub::{api::sync::Api, Repo, RepoType}; use std::collections::HashSet; -#[cfg(target_os = "macos")] -use ort::execution_providers::CoreMLExecutionProvider; - fn log_native(msg: impl AsRef<str>) { // Intentionally no-op: native logging was polluting CLI output. // If you need debugging, add structured logging at the JS layer instead. @@ -65,23 +62,9 @@ impl ColbertEncoderOrt { log_native(format!("[ColBERT-ORT] Loading model from {:?}", model_path)); - // Initialize ONNX Runtime session - // On macOS, use CoreML for GPU acceleration with CPU fallback - #[cfg(target_os = "macos")] - let session = Session::builder()? - .with_execution_providers([ - CoreMLExecutionProvider::default() - .with_subgraphs(true) // Enable CoreML for subgraphs - .build(), - ])? - .with_optimization_level(GraphOptimizationLevel::Level3)? - .with_intra_threads(8)? - .commit_from_file(&model_path)?; - - #[cfg(not(target_os = "macos"))] let session = Session::builder()? .with_optimization_level(GraphOptimizationLevel::Level3)? - .with_intra_threads(8)? + .with_intra_threads(4)? // Reduced from 8 to avoid thread contention .commit_from_file(&model_path)?; let tokenizer = Tokenizer::from_file(&tokenizer_path) diff --git a/osgrep-core/src/dense_ort.rs b/osgrep-core/src/dense_ort.rs index 8baa33d7..85b89467 100644 --- a/osgrep-core/src/dense_ort.rs +++ b/osgrep-core/src/dense_ort.rs @@ -3,9 +3,6 @@ use ort::value::Value; use tokenizers::Tokenizer; use hf_hub::{api::sync::Api, Repo, RepoType}; -#[cfg(target_os = "macos")] -use ort::execution_providers::CoreMLExecutionProvider; - fn log_native(msg: impl AsRef<str>) { // Intentionally no-op: native logging was polluting CLI output. // If you need debugging, add structured logging at the JS layer instead. @@ -36,20 +33,7 @@ impl DenseEncoderOrt { log_native(format!("[ORT] Loading model from {:?}", model_path)); - // Initialize ONNX Runtime session - // On macOS, use CoreML for GPU acceleration with CPU fallback - #[cfg(target_os = "macos")] - let session = Session::builder()? - .with_execution_providers([ - CoreMLExecutionProvider::default() - .with_subgraphs(true) // Enable CoreML for subgraphs - .build(), - ])? - .with_optimization_level(GraphOptimizationLevel::Level3)? - .with_intra_threads(4)? - .commit_from_file(&model_path)?; - - #[cfg(not(target_os = "macos"))] + // Initialize ONNX Runtime session with CPU provider let session = Session::builder()? .with_optimization_level(GraphOptimizationLevel::Level3)? .with_intra_threads(4)? @@ -85,20 +69,7 @@ impl DenseEncoderOrt { pub fn load(model_path: &str, tokenizer_path: &str, hidden_size: usize) -> anyhow::Result<Self> { log_native(format!("[ORT] Loading model from {}", model_path)); - // Initialize ONNX Runtime session - // On macOS, use CoreML for GPU acceleration with CPU fallback - #[cfg(target_os = "macos")] - let session = Session::builder()? - .with_execution_providers([ - CoreMLExecutionProvider::default() - .with_subgraphs(true) - .build(), - ])? - .with_optimization_level(GraphOptimizationLevel::Level3)? - .with_intra_threads(4)? - .commit_from_file(model_path)?; - - #[cfg(not(target_os = "macos"))] + // Initialize ONNX Runtime session with CPU provider let session = Session::builder()? .with_optimization_level(GraphOptimizationLevel::Level3)? .with_intra_threads(4)? From b261fdbe563213bf11c7e9e6e4dc8cdf6078abba Mon Sep 17 00:00:00 2001 From: Ryan D'Onofrio <97113725+Ryandonofrio3@users.noreply.github.com> Date: Sun, 14 Dec 2025 13:15:02 -0800 Subject: [PATCH 17/19] Implement server management enhancements and live indexing features in osgrep. Added process checks for running servers, improved project root detection, and introduced file watching for live indexing. Updated search command to handle server responses and added tracing functionality. Enhanced ignore patterns for generated files in search results. --- plugins/osgrep/hooks/start.js | 62 ++- plugins/osgrep/skills/osgrep/SKILL.md | 1 + src/commands/search.ts | 218 +++++++--- src/commands/serve.ts | 590 ++++++++++++++++++++++++-- src/commands/trace.ts | 163 +++++++ src/index.ts | 2 + src/lib/graph/graph-builder.ts | 173 ++++++++ src/lib/index/ignore-patterns.ts | 38 ++ src/lib/native/index.ts | 15 + src/lib/search/searcher.ts | 52 ++- 10 files changed, 1200 insertions(+), 114 deletions(-) create mode 100644 src/commands/trace.ts create mode 100644 src/lib/graph/graph-builder.ts diff --git a/plugins/osgrep/hooks/start.js b/plugins/osgrep/hooks/start.js index 41066c87..0b645ced 100644 --- a/plugins/osgrep/hooks/start.js +++ b/plugins/osgrep/hooks/start.js @@ -11,24 +11,72 @@ function readPayload() { } } +function isProcessRunning(pid) { + if (!Number.isFinite(pid) || pid <= 0) return false; + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} + +function findProjectRoot(startDir) { + const start = _path.resolve(startDir || process.cwd()); + const osgrepDir = _path.join(start, ".osgrep"); + const gitDir = _path.join(start, ".git"); + if (fs.existsSync(osgrepDir) || fs.existsSync(gitDir)) return start; + return start; +} + +function getServerForProject(projectRoot) { + try { + const regPath = _path.join(require("node:os").homedir(), ".osgrep", "servers.json"); + if (!fs.existsSync(regPath)) return null; + const data = JSON.parse(fs.readFileSync(regPath, "utf-8")); + if (!Array.isArray(data)) return null; + return data.find((s) => s && s.projectRoot === projectRoot && isProcessRunning(s.pid)) || null; + } catch { + return null; + } +} + function main() { const payload = readPayload(); const cwd = payload.cwd || process.cwd(); + if (process.env.OSGREP_DISABLE_AUTO_SERVE === "1") { + const response = { + hookSpecificOutput: { + hookEventName: "SessionStart", + additionalContext: + "osgrep serve auto-start disabled (OSGREP_DISABLE_AUTO_SERVE=1).", + }, + }; + process.stdout.write(JSON.stringify(response)); + return; + } + + const projectRoot = findProjectRoot(cwd); + const existing = getServerForProject(projectRoot); const logPath = "/tmp/osgrep.log"; const out = fs.openSync(logPath, "a"); - const child = spawn("osgrep", ["serve"], { - cwd, - detached: true, - stdio: ["ignore", out, out], - }); - child.unref(); + if (!existing) { + const child = spawn("osgrep", ["serve", "--background"], { + cwd, + detached: true, + stdio: ["ignore", out, out], + }); + child.unref(); + } const response = { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: - 'osgrep serve started; prefer `osgrep "<complete question>"` over grep (plain output is agent-friendly).', + existing + ? `osgrep serve already running (PID: ${existing.pid}, Port: ${existing.port}).` + : 'osgrep serve started; prefer `osgrep "<complete question>"` over grep (plain output is agent-friendly).', }, }; process.stdout.write(JSON.stringify(response)); diff --git a/plugins/osgrep/skills/osgrep/SKILL.md b/plugins/osgrep/skills/osgrep/SKILL.md index d56b159c..fff85ad1 100644 --- a/plugins/osgrep/skills/osgrep/SKILL.md +++ b/plugins/osgrep/skills/osgrep/SKILL.md @@ -6,6 +6,7 @@ allowed-tools: "Bash(osgrep:*), Read" ## What osgrep does + Finds code by meaning. When you'd ask a colleague "where do we handle auth?", use osgrep. - grep/ripgrep: exact string match, fast diff --git a/src/commands/search.ts b/src/commands/search.ts index d4049aaa..8933d516 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -19,7 +19,7 @@ import { gracefulExit } from "../lib/utils/exit"; import { formatTextResults, type TextResult } from "../lib/utils/formatter"; import { isLocked } from "../lib/utils/lock"; import { ensureProjectPaths, findProjectRoot } from "../lib/utils/project-root"; -import { getServerForProject } from "../lib/utils/server-registry"; +import { getServerForProject, unregisterServer } from "../lib/utils/server-registry"; function toTextResults(data: SearchResponse["data"]): TextResult[] { return data.map((r) => { @@ -327,78 +327,151 @@ export const search: Command = new CommanderCommand("search") findProjectRoot(execPathForServer) ?? execPathForServer; const server = getServerForProject(projectRootForServer); - if (server) { - try { - const response = await fetch(`http://localhost:${server.port}/search`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - query: pattern, - limit, - path: exec_path - ? path.relative(projectRootForServer, path.resolve(exec_path)) - : undefined, - }), - }); + if (process.env.DEBUG_SERVER) { + console.error(`[search] projectRootForServer: ${projectRootForServer}`); + console.error(`[search] server found: ${JSON.stringify(server)}`); + } - if (response.ok) { - const body = (await response.json()) as { results: any[] }; + if (server && Number.isFinite(server.port) && server.port > 0) { + if (process.env.DEBUG_SERVER) { + console.error(`[search] attempting fetch to server on port ${server.port}`); + } + try { + // Fast preflight so a hung server doesn't add multi-second latency. + const healthTimeoutMsRaw = Number.parseInt( + process.env.OSGREP_SERVER_HEALTH_TIMEOUT_MS || "", + 10, + ); + const healthTimeoutMs = + Number.isFinite(healthTimeoutMsRaw) && healthTimeoutMsRaw > 0 + ? healthTimeoutMsRaw + : process.stdout.isTTY + ? 250 + : 150; + { + const ac = new AbortController(); + let timeout: NodeJS.Timeout | undefined; + try { + timeout = setTimeout(() => ac.abort(), healthTimeoutMs); + const health = await fetch( + `http://localhost:${server.port}/health`, + { signal: ac.signal }, + ); + if (!health.ok) throw new Error(`health_${health.status}`); + } finally { + if (timeout) clearTimeout(timeout); + } + } - const searchResult = { data: body.results }; - const filteredData = searchResult.data.filter( - (r) => typeof r.score !== "number" || r.score >= minScore, + const timeoutMsRaw = Number.parseInt( + process.env.OSGREP_SERVER_TIMEOUT_MS || "", + 10, + ); + const timeoutMs = + Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 + ? timeoutMsRaw + : process.stdout.isTTY + ? 1200 + : 700; + const ac = new AbortController(); + let timeout: NodeJS.Timeout | undefined; + try { + timeout = setTimeout(() => ac.abort(), timeoutMs); + + const response = await fetch( + `http://localhost:${server.port}/search`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: pattern, + limit, + path: exec_path + ? path.relative(projectRootForServer, path.resolve(exec_path)) + : undefined, + }), + signal: ac.signal, + }, ); - const compactHits = options.compact - ? toCompactHits(filteredData) - : []; - - if (options.compact) { - const compactText = compactHits.length - ? formatCompactTable(compactHits, projectRootForServer, pattern, { - isTTY: !!process.stdout.isTTY, - plain: !!options.plain, - }) - : "No matches found."; - console.log(compactText); - return; // EXIT - } - - if (!filteredData.length) { - console.log("No matches found."); - return; // EXIT + if (process.env.DEBUG_SERVER) { + console.error(`[search] server response status: ${response.status}`); } + if (response.ok) { + if (process.env.DEBUG_SERVER) { + console.error("[search] server returned OK, using server results"); + } + const body = (await response.json()) as { results: any[] }; + + const searchResult = { data: body.results }; + const filteredData = searchResult.data.filter( + (r) => typeof r.score !== "number" || r.score >= minScore, + ); - const isTTY = process.stdout.isTTY; - const shouldBePlain = options.plain || !isTTY; - - if (shouldBePlain) { - const mappedResults = toTextResults(filteredData); - const output = formatTextResults( - mappedResults, - pattern, - projectRootForServer, - { - isPlain: true, - compact: options.compact, + const compactHits = options.compact + ? toCompactHits(filteredData) + : []; + + if (options.compact) { + const compactText = compactHits.length + ? formatCompactTable( + compactHits, + projectRootForServer, + pattern, + { + isTTY: !!process.stdout.isTTY, + plain: !!options.plain, + }, + ) + : "No matches found."; + console.log(compactText); + return; // EXIT + } + + if (!filteredData.length) { + console.log("No matches found."); + return; // EXIT + } + + const isTTY = process.stdout.isTTY; + const shouldBePlain = options.plain || !isTTY; + + if (shouldBePlain) { + const mappedResults = toTextResults(filteredData); + const output = formatTextResults( + mappedResults, + pattern, + projectRootForServer, + { + isPlain: true, + compact: options.compact, + content: options.content, + perFile: parseInt(options.perFile, 10), + showScores: options.scores, + }, + ); + console.log(output); + } else { + const { formatResults } = await import("../lib/output/formatter"); + const output = formatResults(filteredData, projectRootForServer, { content: options.content, - perFile: parseInt(options.perFile, 10), - showScores: options.scores, - }, - ); - console.log(output); - } else { - const { formatResults } = await import("../lib/output/formatter"); - const output = formatResults(filteredData, projectRootForServer, { - content: options.content, - }); - console.log(output); - } + }); + console.log(output); + } - return; // EXIT successful server search + return; // EXIT successful server search + } + } finally { + if (timeout) clearTimeout(timeout); } } catch (e) { - if (process.env.DEBUG) { + // If the server isn't reachable, remove the stale registry entry so we + // don't pay this timeout cost on every query. + const msg = e instanceof Error ? e.message : String(e); + if (msg.includes("ECONNREFUSED") || msg.includes("health_")) { + unregisterServer(server.pid); + } + if (process.env.DEBUG || process.env.DEBUG_SERVER) { console.error( "[search] server request failed, falling back to local:", e, @@ -407,8 +480,20 @@ export const search: Command = new CommanderCommand("search") } } + if (process.env.DEBUG_SERVER) { + console.error("[search] falling through to local search"); + } + try { + const DEBUG_TIMING = process.env.DEBUG_SEARCH_TIMING === "1"; + const t = (label: string) => DEBUG_TIMING && console.time(`[cmd] ${label}`); + const te = (label: string) => DEBUG_TIMING && console.timeEnd(`[cmd] ${label}`); + + t("total-cmd"); + t("ensureSetup"); await ensureSetup(); + te("ensureSetup"); + const searchRoot = exec_path ? path.resolve(exec_path) : root; const projectRoot = findProjectRoot(searchRoot) ?? searchRoot; const paths = ensureProjectPaths(projectRoot); @@ -416,7 +501,9 @@ export const search: Command = new CommanderCommand("search") // Propagate project root to worker processes process.env.OSGREP_PROJECT_ROOT = projectRoot; + t("VectorDB-init"); vectorDb = new VectorDB(paths.lancedbDir); + te("VectorDB-init"); // Check for active indexing lock and warn if present // This allows agents (via shim) to know results might be partial. @@ -426,7 +513,9 @@ export const search: Command = new CommanderCommand("search") ); } + t("hasAnyRows"); const hasRows = await vectorDb.hasAnyRows(); + te("hasAnyRows"); const needsSync = options.sync || !hasRows; if (needsSync) { @@ -489,6 +578,7 @@ export const search: Command = new CommanderCommand("search") const searcher = new Searcher(vectorDb); + t("searcher.search"); const searchResult = await searcher.search( pattern, limit, @@ -496,6 +586,8 @@ export const search: Command = new CommanderCommand("search") undefined, exec_path ? path.relative(projectRoot, path.resolve(exec_path)) : "", ); + te("searcher.search"); + te("total-cmd"); const filteredData = searchResult.data.filter( (r) => typeof r.score !== "number" || r.score >= minScore, diff --git a/src/commands/serve.ts b/src/commands/serve.ts index a0ea7ac3..f2d8173a 100644 --- a/src/commands/serve.ts +++ b/src/commands/serve.ts @@ -2,15 +2,24 @@ import { spawn } from "node:child_process"; import * as fs from "node:fs"; import * as http from "node:http"; import * as path from "node:path"; +import chokidar from "chokidar"; +import type { FSWatcher } from "chokidar"; import { Command } from "commander"; import { PATHS } from "../config"; import { ensureGrammars } from "../lib/index/grammar-loader"; +import { DEFAULT_IGNORE_PATTERNS } from "../lib/index/ignore-patterns"; import { createIndexingSpinner } from "../lib/index/sync-helpers"; import { initialSync } from "../lib/index/syncer"; +import { GraphBuilder } from "../lib/graph/graph-builder"; import { Searcher } from "../lib/search/searcher"; +import type { MetaEntry } from "../lib/store/meta-cache"; +import { MetaCache } from "../lib/store/meta-cache"; +import type { VectorRecord } from "../lib/store/types"; import { ensureSetup } from "../lib/setup/setup-helpers"; import { VectorDB } from "../lib/store/vector-db"; import { gracefulExit } from "../lib/utils/exit"; +import { isIndexableFile } from "../lib/utils/file-utils"; +import { acquireWriterLockWithRetry } from "../lib/utils/lock"; import { ensureProjectPaths, findProjectRoot } from "../lib/utils/project-root"; import { getServerForProject, @@ -19,6 +28,7 @@ import { registerServer, unregisterServer, } from "../lib/utils/server-registry"; +import { processFile } from "../lib/workers/orchestrator"; export const serve = new Command("serve") .description("Run osgrep as a background server with live indexing") @@ -37,7 +47,11 @@ export const serve = new Command("serve") // Check if already running const existing = getServerForProject(projectRoot); - if (existing && isProcessRunning(existing.pid)) { + if ( + existing && + existing.pid !== process.pid && + isProcessRunning(existing.pid) + ) { console.log( `Server already running for ${projectRoot} (PID: ${existing.pid}, Port: ${existing.port})`, ); @@ -61,6 +75,18 @@ export const serve = new Command("serve") env: { ...process.env, OSGREP_BACKGROUND: "true" }, }); child.unref(); + + // Ensure the spawned server can be discovered/stopped immediately (even + // before it finishes indexing and starts listening). + if (typeof child.pid === "number") { + registerServer({ + pid: child.pid, + port: 0, + projectRoot, + startTime: Date.now(), + }); + } + console.log(`Started background server (PID: ${child.pid})`); return; } @@ -85,6 +111,317 @@ export const serve = new Command("serve") const vectorDb = new VectorDB(paths.lancedbDir); const searcher = new Searcher(vectorDb); + const metaCache = new MetaCache(paths.lmdbPath); + + // Serialize DB writes (and optionally searches) to avoid LanceDB contention. + let dbWriteBarrier: Promise<void> = Promise.resolve(); + let isWriting = false; + + // Live indexing: watch filesystem changes and incrementally update the index. + // Enabled by default for `serve` (can disable with OSGREP_WATCH=0). + const watchEnabled = process.env.OSGREP_WATCH !== "0"; + const watchVerbose = process.env.OSGREP_WATCH_VERBOSE === "1"; + const watchMode = (process.env.OSGREP_WATCH_MODE || "auto").toLowerCase(); + const watchDebounceMsRaw = Number.parseInt( + process.env.OSGREP_WATCH_DEBOUNCE_MS || "", + 10, + ); + const watchDebounceMs = + Number.isFinite(watchDebounceMsRaw) && watchDebounceMsRaw >= 0 + ? watchDebounceMsRaw + : 250; + + const pendingUpserts = new Set<string>(); + const pendingUnlinks = new Set<string>(); + let watchTimer: NodeJS.Timeout | undefined; + let watcher: FSWatcher | null = null; + let nativeWatcher: fs.FSWatcher | null = null; + let didLogWatchFallback = false; + + const shouldIgnoreRelPath = (relPathRaw: string): boolean => { + const rel = relPathRaw.split(path.sep).join("/"); + if (!rel || rel === "." || rel.startsWith("../")) return true; + if (rel === ".git" || rel.startsWith(".git/")) return true; + if (rel === ".osgrep" || rel.startsWith(".osgrep/")) return true; + // Large/irrelevant directories (mirrors DEFAULT_IGNORE_PATTERNS intent). + if (rel === "node_modules" || rel.startsWith("node_modules/")) return true; + if (rel.includes("/node_modules/")) return true; + if (rel === "dist" || rel.startsWith("dist/")) return true; + if (rel.includes("/dist/")) return true; + if (rel === "build" || rel.startsWith("build/")) return true; + if (rel.includes("/build/")) return true; + if (rel === "out" || rel.startsWith("out/")) return true; + if (rel.includes("/out/")) return true; + if (rel === "target" || rel.startsWith("target/")) return true; + if (rel.includes("/target/")) return true; + if (rel === "coverage" || rel.startsWith("coverage/")) return true; + if (rel.includes("/coverage/")) return true; + if (rel === "benchmark" || rel.startsWith("benchmark/")) return true; + if (rel.includes("/benchmark/")) return true; + if (rel === ".idea" || rel.startsWith(".idea/")) return true; + if (rel === ".vscode" || rel.startsWith(".vscode/")) return true; + if (rel.endsWith(".DS_Store")) return true; + return false; + }; + + const scheduleFlush = () => { + if (watchTimer) clearTimeout(watchTimer); + watchTimer = setTimeout(() => { + void flushPending().catch((err) => { + console.error("[serve] live index flush failed:", err); + }); + }, watchDebounceMs); + }; + + const recordUpsert = (absPath: string) => { + const rel = path.relative(projectRoot, absPath); + if (shouldIgnoreRelPath(rel)) return; + pendingUnlinks.delete(rel); + pendingUpserts.add(rel); + scheduleFlush(); + }; + + const recordUnlink = (absPath: string) => { + const rel = path.relative(projectRoot, absPath); + if (shouldIgnoreRelPath(rel)) return; + pendingUpserts.delete(rel); + pendingUnlinks.add(rel); + scheduleFlush(); + }; + + const flushPending = async () => { + if (!watchEnabled) return; + if (pendingUpserts.size === 0 && pendingUnlinks.size === 0) return; + + const upserts = Array.from(pendingUpserts); + const unlinks = Array.from(pendingUnlinks); + pendingUpserts.clear(); + pendingUnlinks.clear(); + + // Phase 1: prepare work outside the writer lock (hashing/embedding can be slow). + type PreparedUpsert = { + relPath: string; + absPath: string; + meta: MetaEntry; + shouldDelete: boolean; + vectorsCount: number; + vectors?: VectorRecord[]; + }; + + const prepared: PreparedUpsert[] = []; + + for (const relPath of upserts) { + const absPath = path.join(projectRoot, relPath); + try { + const stats = await fs.promises.stat(absPath); + if (!isIndexableFile(absPath, stats.size)) { + // If it was previously indexed, treat as deletion; still store meta to avoid rework. + prepared.push({ + relPath, + absPath, + meta: { hash: "", mtimeMs: stats.mtimeMs, size: stats.size }, + shouldDelete: true, + vectorsCount: 0, + }); + continue; + } + + const cached = metaCache.get(relPath); + if ( + cached && + cached.mtimeMs === stats.mtimeMs && + cached.size === stats.size + ) { + continue; + } + + const result = await processFile({ path: relPath, absolutePath: absPath }); + + prepared.push({ + relPath, + absPath, + meta: { hash: result.hash, mtimeMs: result.mtimeMs, size: result.size }, + shouldDelete: result.shouldDelete === true, + vectorsCount: result.vectors.length, + vectors: result.vectors, + }); + } catch (err) { + const code = (err as NodeJS.ErrnoException)?.code; + if (code === "ENOENT") { + unlinks.push(relPath); + continue; + } + console.error(`[serve] live index: failed to prepare ${relPath}:`, err); + } + } + + const plannedUnlinks = unlinks.length; + const plannedUpserts = prepared.length; + + // Phase 2: apply updates under writer lock; serialize DB operations. + const apply = async () => { + let lock: { release: () => Promise<void> } | null = null; + try { + isWriting = true; + lock = await acquireWriterLockWithRetry(paths.osgrepDir, { + maxRetries: 2, + retryDelayMs: 250, + }); + + if (unlinks.length > 0) { + await vectorDb.deletePaths(unlinks); + for (const relPath of unlinks) { + metaCache.delete(relPath); + } + } + + for (const item of prepared) { + // If the file changed again since we prepared, skip and let the next event handle it. + try { + const stats = await fs.promises.stat(item.absPath); + if ( + stats.mtimeMs !== item.meta.mtimeMs || + stats.size !== item.meta.size + ) { + pendingUpserts.add(item.relPath); + continue; + } + } catch (err) { + const code = (err as NodeJS.ErrnoException)?.code; + if (code === "ENOENT") { + pendingUnlinks.add(item.relPath); + } + continue; + } + + await vectorDb.deletePaths([item.relPath]); + if (!item.shouldDelete && item.vectorsCount > 0) { + await vectorDb.insertBatch(item.vectors as VectorRecord[]); + } + metaCache.put(item.relPath, item.meta); + } + + if (watchVerbose && (plannedUnlinks > 0 || plannedUpserts > 0)) { + console.log( + `[serve] live index applied: upserts=${plannedUpserts} unlinks=${plannedUnlinks}`, + ); + } + } finally { + if (lock) await lock.release(); + isWriting = false; + } + }; + + dbWriteBarrier = dbWriteBarrier.then(apply, apply); + await dbWriteBarrier; + + // If we re-queued work due to races, schedule another pass. + if (pendingUpserts.size > 0 || pendingUnlinks.size > 0) { + scheduleFlush(); + } + }; + + if (watchEnabled) { + const startChokidar = (mode: "chokidar" | "poll") => { + const ignored: (string | RegExp)[] = [ + ...DEFAULT_IGNORE_PATTERNS, + "**/.git/**", + "**/.osgrep/**", + ]; + + watcher = chokidar.watch(projectRoot, { + ignored, + ignoreInitial: true, + persistent: true, + usePolling: mode === "poll", + interval: mode === "poll" ? 1000 : undefined, + awaitWriteFinish: { + stabilityThreshold: 200, + pollInterval: 100, + }, + }); + + watcher.on("add", recordUpsert); + watcher.on("change", recordUpsert); + watcher.on("unlink", recordUnlink); + watcher.on("error", async (err) => { + const code = (err as NodeJS.ErrnoException)?.code; + if (code === "EMFILE" && mode !== "poll") { + if (!didLogWatchFallback) { + didLogWatchFallback = true; + console.error( + "[serve] watcher hit EMFILE; falling back to polling mode (set OSGREP_WATCH_MODE=poll to force).", + ); + } + try { + await watcher?.close(); + } catch {} + watcher = null; + startChokidar("poll"); + return; + } + console.error("[serve] watcher error:", err); + }); + }; + + const startNative = () => { + nativeWatcher = fs.watch( + projectRoot, + { recursive: true }, + (eventType, filename) => { + const name = + typeof filename === "string" + ? filename + : typeof (filename as any)?.toString === "function" + ? String((filename as any).toString("utf-8")) + : ""; + if (!name) return; + const rel = name.split(path.sep).join("/"); + if (shouldIgnoreRelPath(rel)) return; + const absPath = path.join(projectRoot, name); + // "rename" can be add/unlink/move; stat in flush determines outcome. + if (eventType === "rename" || eventType === "change") { + recordUpsert(absPath); + } + }, + ); + nativeWatcher.on("error", (err) => { + const code = (err as NodeJS.ErrnoException)?.code; + if (code === "EMFILE") { + if (!didLogWatchFallback) { + didLogWatchFallback = true; + console.error( + "[serve] native watcher hit EMFILE; falling back to polling mode (set OSGREP_WATCH_MODE=poll to force).", + ); + } + try { + nativeWatcher?.close(); + } catch {} + nativeWatcher = null; + startChokidar("poll"); + return; + } + console.error("[serve] native watcher error:", err); + }); + }; + + if (watchMode === "off") { + // noop + } else if (watchMode === "native") { + startNative(); + } else if (watchMode === "poll") { + startChokidar("poll"); + } else if (watchMode === "chokidar") { + startChokidar("chokidar"); + } else { + // auto + if (process.platform === "darwin" || process.platform === "win32") { + startNative(); + } else { + startChokidar("chokidar"); + } + } + } // Only show spinner if not in background (or check isTTY) // If spawned in background with stdio ignore, console.log goes nowhere. @@ -117,7 +454,13 @@ export const serve = new Command("serve") if (req.method === "GET" && req.url === "/health") { res.statusCode = 200; res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify({ status: "ok" })); + res.end( + JSON.stringify({ + status: "ok", + indexing: isWriting, + watch: watchEnabled, + }), + ); return; } @@ -174,31 +517,79 @@ export const serve = new Command("serve") // Add AbortController for cancellation const ac = new AbortController(); - req.on("close", () => { + req.on("aborted", () => { ac.abort(); }); + res.on("close", () => { + if (!res.writableEnded) ac.abort(); + }); - const result = await searcher.search( - query, - limit, - { rerank: true }, - undefined, - searchPath, - ac.signal, + const timeoutMsRaw = Number.parseInt( + process.env.OSGREP_SERVER_SEARCH_TIMEOUT_MS || "", + 10, ); + const timeoutMs = + Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 + ? timeoutMsRaw + : 60000; + let timeout: NodeJS.Timeout | undefined; + + try { + timeout = setTimeout(() => { + ac.abort(); + if (!res.headersSent && !res.writableEnded) { + res.statusCode = 504; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ error: "search_timeout" })); + } + }, timeoutMs); + + const debug = process.env.DEBUG_SERVER === "1"; + if (debug) { + console.log( + `[serve] Starting search for "${query}", indexing=${isWriting} signal.aborted=${ac.signal.aborted}`, + ); + } - if (ac.signal.aborted) { - // Request was cancelled, don't write response if possible - // (Though usually 'close' means the socket is gone anyway) - return; - } + const result = await searcher.search( + query, + limit, + { rerank: true }, + undefined, + searchPath, + ac.signal, + ); + if (debug) { + console.log( + `[serve] Search completed, ${result.data.length} results`, + ); + } - res.statusCode = 200; - res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify({ results: result.data })); + if (ac.signal.aborted) { + console.log("[serve] Signal aborted after search"); + return; + } + + res.statusCode = 200; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ results: result.data })); + } finally { + if (timeout) clearTimeout(timeout); + } } catch (err) { + console.log(`[serve] Search error: ${err instanceof Error ? err.name + ': ' + err.message : err}`); if (err instanceof Error && err.name === "AbortError") { - // Request cancelled + if (!res.headersSent && !res.writableEnded) { + res.statusCode = 504; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ error: "search_cancelled" })); + } + return; + } + if (isWriting && !res.headersSent && !res.writableEnded) { + res.statusCode = 503; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ error: "indexing_in_progress" })); return; } res.statusCode = 500; @@ -219,6 +610,85 @@ export const serve = new Command("serve") return; } + if (req.method === "POST" && req.url === "/trace") { + const chunks: Buffer[] = []; + let totalSize = 0; + let aborted = false; + + req.on("data", (chunk) => { + if (aborted) return; + totalSize += chunk.length; + if (totalSize > 100_000) { + aborted = true; + res.statusCode = 413; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ error: "payload_too_large" })); + req.destroy(); + return; + } + chunks.push(chunk); + }); + + req.on("end", async () => { + if (aborted) return; + try { + const body = chunks.length + ? JSON.parse(Buffer.concat(chunks).toString("utf-8")) + : {}; + const symbol = typeof body.symbol === "string" ? body.symbol : ""; + + if (!symbol) { + res.statusCode = 400; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ error: "symbol_required" })); + return; + } + + const graphBuilder = new GraphBuilder(vectorDb); + const graph = await graphBuilder.buildGraph(symbol, { + depth: typeof body.depth === "number" ? body.depth : 1, + callersOnly: body.callers === true, + calleesOnly: body.callees === true, + pathPrefix: typeof body.path === "string" ? body.path : undefined, + }); + + res.statusCode = 200; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ + symbol, + center: graph.center + ? { + file: graph.center.file, + line: graph.center.line, + role: graph.center.role, + } + : null, + callers: graph.callers.map((c) => ({ + symbol: c.symbol, + file: c.file, + line: c.line, + })), + callees: graph.callees, + })); + } catch (err) { + res.statusCode = 500; + res.setHeader("Content-Type", "application/json"); + res.end( + JSON.stringify({ + error: (err as Error)?.message || "trace_failed", + }), + ); + } + }); + + req.on("error", (err) => { + console.error("[serve] trace request error:", err); + aborted = true; + }); + + return; + } + res.statusCode = 404; res.end(); } catch (err) { @@ -272,6 +742,18 @@ export const serve = new Command("serve") const shutdown = async () => { unregisterServer(process.pid); + if (watchTimer) { + clearTimeout(watchTimer); + watchTimer = undefined; + } + try { + await watcher?.close(); + } catch {} + try { + nativeWatcher?.close(); + } catch {} + nativeWatcher = null + // Properly await server close await new Promise<void>((resolve, reject) => { server.close((err) => { @@ -292,6 +774,9 @@ export const serve = new Command("serve") } catch (e) { console.error("Error closing vector DB:", e); } + try { + metaCache.close(); + } catch {} await gracefulExit(); }; @@ -325,28 +810,67 @@ serve .command("stop") .description("Stop background servers") .option("--all", "Stop all servers", false) - .action((options) => { + .action(async (options) => { + const waitForExit = async (pid: number, timeoutMs: number) => { + const deadline = Date.now() + Math.max(0, timeoutMs); + while (Date.now() < deadline) { + if (!isProcessRunning(pid)) return true; + await new Promise((r) => setTimeout(r, 75)); + } + return !isProcessRunning(pid); + }; + + const stopPid = async (pid: number): Promise<boolean> => { + try { + // If the process is stopped (job control), SIGTERM won't be handled until resumed. + process.kill(pid, "SIGCONT"); + } catch {} + + try { + process.kill(pid, "SIGTERM"); + } catch (e) { + unregisterServer(pid); + return false; + } + + const exited = await waitForExit(pid, 2000); + if (exited) { + unregisterServer(pid); + return true; + } + + try { + process.kill(pid, "SIGKILL"); + } catch (e) { + unregisterServer(pid); + return false; + } + + const killed = await waitForExit(pid, 2000); + unregisterServer(pid); + return killed; + }; + if (options.all) { const servers = listServers(); let count = 0; - servers.forEach((s) => { - try { - process.kill(s.pid, "SIGTERM"); - count++; - } catch (e) { - console.error(`Failed to stop PID ${s.pid}:`, e); - } - }); + for (const s of servers) { + const ok = await stopPid(s.pid); + if (ok) count++; + else console.error(`Failed to stop PID ${s.pid}`); + } console.log(`Stopped ${count} servers.`); } else { const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd(); const server = getServerForProject(projectRoot); if (server) { - try { - process.kill(server.pid, "SIGTERM"); - console.log(`Stopped server for ${projectRoot} (PID: ${server.pid})`); - } catch (e) { - console.error(`Failed to stop PID ${server.pid}:`, e); + const ok = await stopPid(server.pid); + if (ok) { + console.log( + `Stopped server for ${projectRoot} (PID: ${server.pid})`, + ); + } else { + console.error(`Failed to stop PID ${server.pid}`); } } else { console.log(`No server found for ${projectRoot}`); diff --git a/src/commands/trace.ts b/src/commands/trace.ts new file mode 100644 index 00000000..025479b0 --- /dev/null +++ b/src/commands/trace.ts @@ -0,0 +1,163 @@ +import { Command } from "commander"; +import { GraphBuilder, type CallGraph } from "../lib/graph/graph-builder"; +import { VectorDB } from "../lib/store/vector-db"; +import { gracefulExit } from "../lib/utils/exit"; +import { ensureProjectPaths, findProjectRoot } from "../lib/utils/project-root"; + +const style = { + bold: (s: string) => `\x1b[1m${s}\x1b[22m`, + dim: (s: string) => `\x1b[2m${s}\x1b[22m`, + cyan: (s: string) => `\x1b[36m${s}\x1b[39m`, + green: (s: string) => `\x1b[32m${s}\x1b[39m`, + yellow: (s: string) => `\x1b[33m${s}\x1b[39m`, +}; + +/** + * Format call graph for minimal agent-friendly output. + * Format: file:line (space-separated for multiple) + */ +function formatPlain(graph: CallGraph, symbol: string): string { + const lines: string[] = []; + lines.push(symbol); + + if (graph.center) { + lines.push(` def: ${graph.center.file}:${graph.center.line}`); + } else { + lines.push(" def: (not found)"); + } + + if (graph.callees.length > 0) { + lines.push(` calls: ${graph.callees.join(" ")}`); + } + + if (graph.callers.length > 0) { + const callerLocs = graph.callers + .map((c) => `${c.file}:${c.line}`) + .join(" "); + lines.push(` called_by: ${callerLocs}`); + } + + return lines.join("\n"); +} + +/** + * Format call graph as a tree for human-readable output. + */ +function formatTree(graph: CallGraph, symbol: string): string { + const lines: string[] = []; + + if (graph.center) { + const role = graph.center.role ? ` ${style.dim(graph.center.role)}` : ""; + lines.push( + `${style.bold(symbol)} (${style.cyan(`${graph.center.file}:${graph.center.line}`)})${role}`, + ); + } else { + lines.push(`${style.bold(symbol)} ${style.dim("(definition not found)")}`); + } + + const hasCallees = graph.callees.length > 0; + const hasCallers = graph.callers.length > 0; + + if (hasCallees) { + const branch = hasCallers ? "\u251c\u2500\u2500" : "\u2514\u2500\u2500"; + lines.push(`${branch} ${style.yellow("calls:")}`); + graph.callees.forEach((callee, i) => { + const prefix = hasCallers ? "\u2502 " : " "; + const sym = + i === graph.callees.length - 1 + ? "\u2514\u2500\u2500" + : "\u251c\u2500\u2500"; + lines.push(`${prefix}${sym} ${callee}`); + }); + } + + if (hasCallers) { + lines.push(`\u2514\u2500\u2500 ${style.green("called by:")}`); + graph.callers.forEach((caller, i) => { + const sym = + i === graph.callers.length - 1 + ? "\u2514\u2500\u2500" + : "\u251c\u2500\u2500"; + lines.push( + ` ${sym} ${caller.symbol} (${style.cyan(`${caller.file}:${caller.line}`)})`, + ); + }); + } + + if (!hasCallees && !hasCallers) { + lines.push(style.dim(" (no callers or callees found)")); + } + + return lines.join("\n"); +} + +/** + * Format call graph as JSON for programmatic use. + */ +function formatJson(graph: CallGraph, symbol: string): string { + return JSON.stringify( + { + symbol, + center: graph.center + ? { + file: graph.center.file, + line: graph.center.line, + role: graph.center.role, + } + : null, + callers: graph.callers.map((c) => ({ + symbol: c.symbol, + file: c.file, + line: c.line, + })), + callees: graph.callees, + }, + null, + 2, + ); +} + +export const trace = new Command("trace") + .description("Show call graph for a symbol (callers and callees)") + .argument("<symbol>", "Symbol name to trace") + .option("-d, --depth <n>", "Traversal depth (default: 1)", "1") + .option("--callers", "Show only callers (who calls this)") + .option("--callees", "Show only callees (what this calls)") + .option("-p, --path <prefix>", "Filter to path prefix") + .option("--pretty", "Pretty tree output (default for TTY)") + .option("--plain", "Plain minimal output (default for non-TTY)") + .option("--json", "JSON output") + .action(async (symbol, cmd) => { + const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd(); + const paths = ensureProjectPaths(projectRoot); + const db = new VectorDB(paths.lancedbDir); + + try { + const builder = new GraphBuilder(db); + + const graph = await builder.buildGraph(symbol, { + depth: Number.parseInt(cmd.depth, 10) || 1, + callersOnly: cmd.callers as boolean, + calleesOnly: cmd.callees as boolean, + pathPrefix: cmd.path as string | undefined, + }); + + // Determine output format + let output: string; + if (cmd.json) { + output = formatJson(graph, symbol); + } else if (cmd.pretty) { + output = formatTree(graph, symbol); + } else if (cmd.plain || !process.stdout.isTTY) { + output = formatPlain(graph, symbol); + } else { + output = formatTree(graph, symbol); + } + + console.log(output); + } finally { + await db.close(); + } + + await gracefulExit(); + }); diff --git a/src/index.ts b/src/index.ts index 5e2403d5..5a1d770a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import { search } from "./commands/search"; import { serve } from "./commands/serve"; import { setup } from "./commands/setup"; import { symbols } from "./commands/symbols"; +import { trace } from "./commands/trace"; program .version( @@ -47,6 +48,7 @@ program.addCommand(search, { isDefault: true }); program.addCommand(index); program.addCommand(list); program.addCommand(symbols); +program.addCommand(trace); program.addCommand(setup); program.addCommand(serve); program.addCommand(mcp); diff --git a/src/lib/graph/graph-builder.ts b/src/lib/graph/graph-builder.ts new file mode 100644 index 00000000..9c04ec49 --- /dev/null +++ b/src/lib/graph/graph-builder.ts @@ -0,0 +1,173 @@ +import type { VectorDB } from "../store/vector-db"; +import type { VectorRecord } from "../store/types"; +import { escapeSqlString, normalizePath } from "../utils/filter-builder"; + +export interface GraphNode { + symbol: string; + file: string; + line: number; + role: string; + calls: string[]; +} + +export interface CallGraph { + center: GraphNode | null; + callers: GraphNode[]; + callees: string[]; +} + +export interface TraceOptions { + depth?: number; + callersOnly?: boolean; + calleesOnly?: boolean; + pathPrefix?: string; +} + +export class GraphBuilder { + constructor(private db: VectorDB) {} + + /** + * Find all chunks where the symbol is defined. + * Returns multiple if the same symbol is defined in different files. + */ + async findDefinitions(symbol: string): Promise<GraphNode[]> { + const table = await this.db.ensureTable(); + const whereClause = `array_contains(defined_symbols, '${escapeSqlString(symbol)}')`; + + const records = (await table + .query() + .where(whereClause) + .limit(20) + .toArray()) as VectorRecord[]; + + return records.map((r) => this.recordToNode(r, symbol)); + } + + /** + * Find chunks that reference (call) the given symbol. + * Excludes chunks that also define the symbol (to avoid self-references). + */ + async findCallers(symbol: string, limit = 20): Promise<GraphNode[]> { + const table = await this.db.ensureTable(); + const whereClause = `array_contains(referenced_symbols, '${escapeSqlString(symbol)}')`; + + const records = (await table + .query() + .where(whereClause) + .limit(limit + 20) // Fetch extra to filter self-definitions + .toArray()) as VectorRecord[]; + + // Filter out self-definitions (where this chunk also defines the symbol) + const filtered = records.filter((r) => { + const defined = this.toStringArray(r.defined_symbols); + return !defined.includes(symbol); + }); + + return filtered.slice(0, limit).map((r) => { + // Find the primary symbol for this chunk (caller) + const defined = this.toStringArray(r.defined_symbols); + const callerSymbol = defined[0] || r.parent_symbol || "(anonymous)"; + return this.recordToNode(r, callerSymbol); + }); + } + + /** + * Given a list of symbol names, return only those that have definitions + * in the index (i.e., internal to the project, not external libraries). + */ + async filterToInternal(symbols: string[]): Promise<string[]> { + if (symbols.length === 0) return []; + + const table = await this.db.ensureTable(); + const internal: string[] = []; + + // Batch check which symbols have definitions + // For efficiency, we use a single query with OR conditions + // But LanceDB doesn't support OR in array_contains well, so we check individually + // This is acceptable since callees are typically <20 per function + for (const sym of symbols) { + const whereClause = `array_contains(defined_symbols, '${escapeSqlString(sym)}')`; + const records = await table.query().where(whereClause).limit(1).toArray(); + if (records.length > 0) { + internal.push(sym); + } + } + + return internal; + } + + /** + * Build the call graph for a symbol. + * Returns the definition (center), callers, and callees (filtered to internal). + */ + async buildGraph( + symbol: string, + options?: TraceOptions, + ): Promise<CallGraph> { + const { callersOnly, calleesOnly, pathPrefix } = options || {}; + + // Find definitions + let definitions = await this.findDefinitions(symbol); + + // Apply path prefix filter if specified + if (pathPrefix) { + const normalizedPrefix = normalizePath(pathPrefix); + definitions = definitions.filter((d) => + d.file.startsWith(normalizedPrefix), + ); + } + + // For now, take the first definition as center (could show all) + const center = definitions[0] || null; + + // Get callers if not callees-only + let callers: GraphNode[] = []; + if (!calleesOnly) { + callers = await this.findCallers(symbol); + if (pathPrefix) { + const normalizedPrefix = normalizePath(pathPrefix); + callers = callers.filter((c) => c.file.startsWith(normalizedPrefix)); + } + } + + // Get callees if not callers-only + let callees: string[] = []; + if (!callersOnly && center) { + // Get the raw referenced_symbols from the center definition + const rawCallees = center.calls; + // Filter to only internal symbols (have definitions in index) + callees = await this.filterToInternal(rawCallees); + } + + return { center, callers, callees }; + } + + private recordToNode(record: VectorRecord, symbol: string): GraphNode { + return { + symbol, + file: record.path, + line: record.start_line, + role: record.role || "IMPL", + calls: this.toStringArray(record.referenced_symbols), + }; + } + + private toStringArray(val: unknown): string[] { + if (!val) return []; + if (Array.isArray(val)) { + return val.filter((v) => typeof v === "string"); + } + if (typeof (val as any).toArray === "function") { + try { + const arr = (val as any).toArray(); + if (Array.isArray(arr)) return arr.filter((v) => typeof v === "string"); + return Array.from(arr || []).filter( + (v) => typeof v === "string", + ) as string[]; + } catch { + return []; + } + } + return []; + } +} diff --git a/src/lib/index/ignore-patterns.ts b/src/lib/index/ignore-patterns.ts index f7245511..f6adb7a1 100644 --- a/src/lib/index/ignore-patterns.ts +++ b/src/lib/index/ignore-patterns.ts @@ -58,3 +58,41 @@ export const DEFAULT_IGNORE_PATTERNS = [ "**/.vscode/**", "Thumbs.db", ]; + +// Patterns for generated/auto-generated code files. +// These are still indexed but receive a score penalty in search results. +// Uses regex patterns (not globs) for matching against file paths. +export const GENERATED_FILE_PATTERNS: RegExp[] = [ + // TypeScript/JavaScript codegen + /\.gen\.[jt]sx?$/i, + /\.generated\.[jt]sx?$/i, + /\.g\.[jt]sx?$/i, + /_generated\.[jt]sx?$/i, + // TypeScript declaration files (ambient types, often auto-generated) + /\.d\.ts$/i, + // GraphQL codegen + /\.graphql\.[jt]sx?$/i, + /\/__generated__\//i, + // Protocol Buffers + /\.pb\.[a-z]+$/i, // .pb.go, .pb.ts, etc. + /_pb2\.py$/i, // Python protobuf + /_pb2_grpc\.py$/i, + // Go codegen + /_gen\.go$/i, + /_string\.go$/i, // stringer tool + /\.gen\.go$/i, + /mock_.*\.go$/i, // mockgen + // C# codegen + /\.Designer\.cs$/i, + /\.g\.cs$/i, + /\.g\.i\.cs$/i, + // OpenAPI / Swagger + /openapi.*\.gen\./i, + /swagger.*\.gen\./i, + // Prisma + /prisma\/client\//i, + // Generic patterns + /\/generated\//i, + /\/gen\//i, + /\/codegen\//i, +]; diff --git a/src/lib/native/index.ts b/src/lib/native/index.ts index d9632ea1..a331eeab 100644 --- a/src/lib/native/index.ts +++ b/src/lib/native/index.ts @@ -28,11 +28,17 @@ export async function initNative(): Promise<void> { if (initialized) return; if (initPromise) return initPromise; + const DEBUG_TIMING = process.env.DEBUG_SEARCH_TIMING === "1"; + initPromise = (async () => { + if (DEBUG_TIMING) console.time("[native] loadNative"); const n = await loadNative(); + if (DEBUG_TIMING) console.timeEnd("[native] loadNative"); if (!n.isInitialized()) { + if (DEBUG_TIMING) console.time("[native] initModels"); n.initModels(MODEL_IDS.embed, MODEL_IDS.colbert); + if (DEBUG_TIMING) console.timeEnd("[native] initModels"); } initialized = true; @@ -129,10 +135,13 @@ export interface HybridEmbedding { * Returns per-text embeddings ready for storage */ export async function embedBatch(texts: string[]): Promise<HybridEmbedding[]> { + const DEBUG_TIMING = process.env.DEBUG_SEARCH_TIMING === "1"; await initNative(); const n = await loadNative(); + if (DEBUG_TIMING) console.time(`[native] embedBatch (${texts.length} texts)`); const result = n.embedBatch(texts); + if (DEBUG_TIMING) console.timeEnd(`[native] embedBatch (${texts.length} texts)`); const dim = CONFIG.VECTOR_DIM; const colbertDim = CONFIG.COLBERT_DIM; @@ -182,10 +191,13 @@ export async function embedBatch(texts: string[]): Promise<HybridEmbedding[]> { * Returns query embedding matrix as Float32Array */ export async function encodeQueryColbert(query: string): Promise<Float32Array> { + const DEBUG_TIMING = process.env.DEBUG_SEARCH_TIMING === "1"; await initNative(); const n = await loadNative(); + if (DEBUG_TIMING) console.time("[native] encodeQueryColbert"); const result = n.encodeQueryColbert(query); + if (DEBUG_TIMING) console.timeEnd("[native] encodeQueryColbert"); return new Float32Array(result); } @@ -221,6 +233,7 @@ export interface RerankResult { * Rerank documents using pre-indexed ColBERT embeddings */ export async function rerankColbert(input: RerankInput): Promise<RerankResult> { + const DEBUG_TIMING = process.env.DEBUG_SEARCH_TIMING === "1"; await initNative(); const n = await loadNative(); @@ -236,6 +249,7 @@ export async function rerankColbert(input: RerankInput): Promise<RerankResult> { ? input.docTokenIds : Uint32Array.from(input.docTokenIds as any); + if (DEBUG_TIMING) console.time(`[native] rerankColbert (${input.candidateIndices.length} docs)`); const result = n.rerankColbert( q, docs, @@ -245,6 +259,7 @@ export async function rerankColbert(input: RerankInput): Promise<RerankResult> { input.candidateIndices, input.topK ); + if (DEBUG_TIMING) console.timeEnd(`[native] rerankColbert (${input.candidateIndices.length} docs)`); return { indices: Array.from(result.indices), diff --git a/src/lib/search/searcher.ts b/src/lib/search/searcher.ts index 349434bb..5ded9463 100644 --- a/src/lib/search/searcher.ts +++ b/src/lib/search/searcher.ts @@ -1,5 +1,6 @@ import type { Table } from "@lancedb/lancedb"; import { CONFIG } from "../../config"; +import { GENERATED_FILE_PATTERNS } from "../index/ignore-patterns"; import type { ChunkType, SearchFilter, @@ -192,6 +193,8 @@ export class Searcher { }; } + private static readonly GENERATED_PENALTY = 0.4; + private applyStructureBoost( record: Partial<VectorRecord>, score: number, @@ -205,6 +208,14 @@ export class Searcher { const pathStr = (record.path || "").toLowerCase(); + // Generated file penalty - auto-generated code rarely contains business logic + const isGenerated = GENERATED_FILE_PATTERNS.some((pattern) => + pattern.test(pathStr), + ); + if (isGenerated) { + adjusted *= Searcher.GENERATED_PENALTY; + } + // Use path-segment and filename patterns to avoid false positives like "latest" const isTestPath = /(^|\/)(__tests__|tests?|specs?|benchmark)(\/|$)/i.test(pathStr) || @@ -273,8 +284,6 @@ export class Searcher { return deduped; } - private ftsIndexChecked = false; - async search( query: string, top_k?: number, @@ -283,22 +292,34 @@ export class Searcher { pathPrefix?: string, signal?: AbortSignal, ): Promise<SearchResponse> { + const DEBUG_TIMING = process.env.DEBUG_SEARCH_TIMING === "1"; + const DEBUG_ABORT = process.env.DEBUG_SERVER === "1" || process.env.DEBUG_ABORT === "1"; + const t = (label: string) => DEBUG_TIMING && console.time(`[search] ${label}`); + const te = (label: string) => DEBUG_TIMING && console.timeEnd(`[search] ${label}`); + + t("total"); const finalLimitRaw = top_k ?? 10; const finalLimit = Number.isFinite(finalLimitRaw) && finalLimitRaw > 0 ? finalLimitRaw : 10; const doRerank = options?.rerank ?? true; + if (DEBUG_ABORT) console.log(`[searcher] start, signal.aborted=${signal?.aborted}`); + if (signal?.aborted) { const err = new Error("Aborted"); err.name = "AbortError"; throw err; } + t("encodeQuery"); + if (DEBUG_ABORT) console.log("[searcher] before encodeQuery"); const { dense: queryVector, colbert: queryMatrixRaw, colbertDim, } = await encodeQuery(query); + te("encodeQuery"); + if (DEBUG_ABORT) console.log(`[searcher] after encodeQuery, signal.aborted=${signal?.aborted}`); if (signal?.aborted) { const err = new Error("Aborted"); @@ -342,29 +363,33 @@ export class Searcher { finalLimit * Searcher.PRE_RERANK_K_MULT, Searcher.PRE_RERANK_K_MIN, ); + t("ensureTable"); let table: Table; try { table = await this.db.ensureTable(); } catch { + te("ensureTable"); + te("total"); return { data: [] }; } + te("ensureTable"); - // Ensure FTS index exists (lazy init on first search) - if (!this.ftsIndexChecked) { - this.ftsIndexChecked = true; // Set immediately to prevent retry spam - try { - await this.db.createFTSIndex(); - } catch (e) { - console.warn("[Searcher] Failed to ensure FTS index:", e); - } - } + // Skip FTS index check during search - it should be created during indexing. + // The createFTSIndex call takes ~500ms even when index exists due to LanceDB overhead. + // If FTS search fails below, we fall back gracefully. + t("vectorSearch"); + if (DEBUG_ABORT) console.log("[searcher] before vectorSearch"); let vectorQuery = table.vectorSearch(queryVector).limit(PRE_RERANK_K); if (whereClause) { vectorQuery = vectorQuery.where(whereClause); } const vectorResults = (await vectorQuery.toArray()) as VectorRecord[]; + te("vectorSearch"); + if (DEBUG_ABORT) console.log(`[searcher] after vectorSearch (${vectorResults.length} results), signal.aborted=${signal?.aborted}`); + t("ftsSearch"); + if (DEBUG_ABORT) console.log("[searcher] before ftsSearch"); let ftsResults: VectorRecord[] = []; try { const ftsText = Searcher.normalizeFtsQuery(query); @@ -378,6 +403,8 @@ export class Searcher { const msg = e instanceof Error ? e.message : String(e); console.warn(`[Searcher] FTS search failed: ${msg}`); } + te("ftsSearch"); + if (DEBUG_ABORT) console.log(`[searcher] after ftsSearch (${ftsResults.length} results), signal.aborted=${signal?.aborted}`); if (signal?.aborted) { const err = new Error("Aborted"); @@ -415,6 +442,7 @@ export class Searcher { const rerankCandidates = fused.slice(0, Searcher.RERANK_CANDIDATES_K); + t("rerank"); const scores = doRerank ? await rerank({ query: queryMatrixRaw, @@ -436,6 +464,7 @@ export class Searcher { // Small tie-breaker so later items don't all share 0 return fusedScore || 1 / (idx + 1); }); + te("rerank"); type ScoredItem = { record: (typeof rerankCandidates)[number]; @@ -481,6 +510,7 @@ export class Searcher { // Item 12: Score Calibration const maxScore = finalResults.length > 0 ? finalResults[0]._score : 1.0; + te("total"); return { data: finalResults.map((r: (typeof finalResults)[number]) => { const chunk = this.mapRecordToChunk(r, r._score || 0); From d2305352065cf9abdcbb586983e8f82996528e69 Mon Sep 17 00:00:00 2001 From: Ryan D'Onofrio <97113725+Ryandonofrio3@users.noreply.github.com> Date: Sun, 14 Dec 2025 23:25:49 -0800 Subject: [PATCH 18/19] Add `osgrep trace` command for call flow analysis and enhance README documentation. Update server response handling for initial sync state and improve output formatting for search results. --- README.md | 53 +++++++++++ plugins/osgrep/hooks/start.js | 4 +- plugins/osgrep/skills/osgrep/SKILL.md | 34 ++++++- src/commands/search.ts | 18 +++- src/commands/serve.ts | 95 ++++++++++++++----- src/commands/trace.ts | 127 ++++++++++++++++++++++++-- 6 files changed, 298 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index bbe6a6b3..cf94a6c9 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,59 @@ osgrep serve stop --all Claude Code hooks start/stop this automatically; you rarely need to run it manually. +### `osgrep trace` + +Shows callers and callees for a symbol in one shot — replaces multiple search/read cycles when tracing code flow. + +```bash +osgrep trace handleRequest +``` + +**Output (default plain format):** +``` +handleRequest + def: src/server/handler.ts:45 + calls: validateAuth routeRequest sendResponse + called_by: src/index.ts:12 src/api/router.ts:87 +``` + +**Options:** + +| Flag | Description | Default | +| --- | --- | --- | +| `--depth <n>` | Traversal depth for nested calls | `1` | +| `--callers` | Show only callers (who calls this) | `false` | +| `--callees` | Show only callees (what this calls) | `false` | +| `--path <prefix>` | Filter results to path prefix | - | +| `--pretty` | Tree view output for humans | `false` | +| `--json` | JSON output for programmatic use | `false` | + +**Examples:** + +```bash +# Basic trace +osgrep trace Searcher + +# Only show what calls this function +osgrep trace handleAuth --callers + +# Only show what this function calls +osgrep trace handleAuth --callees + +# Filter to specific directory +osgrep trace validateToken --path src/auth + +# Pretty tree output +osgrep trace processRequest --pretty +``` + +**Server endpoint:** +```bash +curl -X POST http://localhost:4444/trace \ + -H "Content-Type: application/json" \ + -d '{"symbol": "Searcher", "depth": 1, "callers": true}' +``` + ### `osgrep list` Lists all indexed repositories (stores) and their metadata. diff --git a/plugins/osgrep/hooks/start.js b/plugins/osgrep/hooks/start.js index 0b645ced..9c8fa941 100644 --- a/plugins/osgrep/hooks/start.js +++ b/plugins/osgrep/hooks/start.js @@ -75,8 +75,8 @@ function main() { hookEventName: "SessionStart", additionalContext: existing - ? `osgrep serve already running (PID: ${existing.pid}, Port: ${existing.port}).` - : 'osgrep serve started; prefer `osgrep "<complete question>"` over grep (plain output is agent-friendly).', + ? `osgrep serve running (PID: ${existing.pid}, Port: ${existing.port}).` + : 'osgrep serve starting (indexing in background). Searches work immediately but may show partial results until indexing completes.', }, }; process.stdout.write(JSON.stringify(response)); diff --git a/plugins/osgrep/skills/osgrep/SKILL.md b/plugins/osgrep/skills/osgrep/SKILL.md index fff85ad1..34f71786 100644 --- a/plugins/osgrep/skills/osgrep/SKILL.md +++ b/plugins/osgrep/skills/osgrep/SKILL.md @@ -1,6 +1,6 @@ --- name: osgrep -description: Semantic code search. Use alongside grep - grep for exact strings, osgrep for concepts. +description: Semantic code search and call tracing. Use alongside grep - grep for exact strings, osgrep for concepts and call flow. allowed-tools: "Bash(osgrep:*), Read" --- @@ -47,6 +47,38 @@ Read src/auth/handler.ts:45-120 Read the specific line range, not the whole file. +## Trace command + +When you need to understand call flow (who calls what, what calls who): + +```bash +osgrep trace handleRequest +``` + +**Output:** +``` +handleRequest + def: src/server/handler.ts:45 + calls: validateAuth routeRequest sendResponse + called_by: src/index.ts:12 src/api/router.ts:87 +``` + +Use trace when: +- You found a function and need to know what calls it +- You need to understand what a function depends on +- You're tracing request/data flow through the codebase + +```bash +# Only callers (who calls this?) +osgrep trace handleAuth --callers + +# Only callees (what does this call?) +osgrep trace handleAuth --callees + +# Filter to specific path +osgrep trace validateToken --path src/auth +``` + ## Other options ```bash # Just file paths when you only need locations diff --git a/src/commands/search.ts b/src/commands/search.ts index 8933d516..585e8b6c 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -401,7 +401,23 @@ export const search: Command = new CommanderCommand("search") if (process.env.DEBUG_SERVER) { console.error("[search] server returned OK, using server results"); } - const body = (await response.json()) as { results: any[] }; + const body = (await response.json()) as { + results: any[]; + partial?: boolean; + initialSync?: { + filesProcessed: number; + filesIndexed: number; + totalFiles: number; + }; + }; + + // Show warning if results are partial (server still indexing) + if (body.partial && body.initialSync) { + const { filesProcessed, filesIndexed } = body.initialSync; + console.warn( + `⚠️ Index building (${filesProcessed} files processed, ${filesIndexed} indexed). Results may be incomplete.`, + ); + } const searchResult = { data: body.results }; const filteredData = searchResult.data.filter( diff --git a/src/commands/serve.ts b/src/commands/serve.ts index f2d8173a..627b32fd 100644 --- a/src/commands/serve.ts +++ b/src/commands/serve.ts @@ -8,7 +8,7 @@ import { Command } from "commander"; import { PATHS } from "../config"; import { ensureGrammars } from "../lib/index/grammar-loader"; import { DEFAULT_IGNORE_PATTERNS } from "../lib/index/ignore-patterns"; -import { createIndexingSpinner } from "../lib/index/sync-helpers"; +import type { InitialSyncProgress } from "../lib/index/sync-helpers"; import { initialSync } from "../lib/index/syncer"; import { GraphBuilder } from "../lib/graph/graph-builder"; import { Searcher } from "../lib/search/searcher"; @@ -117,6 +117,21 @@ export const serve = new Command("serve") let dbWriteBarrier: Promise<void> = Promise.resolve(); let isWriting = false; + // Track initial sync state for HTTP endpoints + let initialSyncState: { + inProgress: boolean; + filesProcessed: number; + filesIndexed: number; + totalFiles: number; + currentFile: string; + } = { + inProgress: true, + filesProcessed: 0, + filesIndexed: 0, + totalFiles: 0, + currentFile: "Starting...", + }; + // Live indexing: watch filesystem changes and incrementally update the index. // Enabled by default for `serve` (can disable with OSGREP_WATCH=0). const watchEnabled = process.env.OSGREP_WATCH !== "0"; @@ -423,31 +438,31 @@ export const serve = new Command("serve") } } - // Only show spinner if not in background (or check isTTY) - // If spawned in background with stdio ignore, console.log goes nowhere. - // But we might want to log to a file in the future. + // Helper to run initial sync after server is listening + const runInitialSync = async () => { + const onProgress = (info: InitialSyncProgress) => { + initialSyncState.filesProcessed = info.processed; + initialSyncState.filesIndexed = info.indexed; + initialSyncState.totalFiles = info.total; + initialSyncState.currentFile = info.filePath ?? ""; + }; - if (!process.env.OSGREP_BACKGROUND) { - const { spinner, onProgress } = createIndexingSpinner( - projectRoot, - "Indexing before starting server...", - ); try { - await initialSync({ - projectRoot, - onProgress, - }); + await initialSync({ projectRoot, onProgress }); await vectorDb.createFTSIndex(); - spinner.succeed("Initial index ready. Starting server..."); + initialSyncState.inProgress = false; + initialSyncState.currentFile = ""; + + if (!process.env.OSGREP_BACKGROUND) { + console.log("Initial index ready."); + } } catch (e) { - spinner.fail("Indexing failed"); - throw e; + console.error("Initial sync failed:", e); + // Mark as done but leave currentFile as error indicator + initialSyncState.inProgress = false; + initialSyncState.currentFile = "sync_failed"; } - } else { - // In background, just sync quietly - await initialSync({ projectRoot }); - await vectorDb.createFTSIndex(); - } + }; const server = http.createServer(async (req, res) => { try { @@ -456,7 +471,16 @@ export const serve = new Command("serve") res.setHeader("Content-Type", "application/json"); res.end( JSON.stringify({ - status: "ok", + status: initialSyncState.inProgress ? "initializing" : "ok", + initialSync: initialSyncState.inProgress + ? { + inProgress: true, + filesProcessed: initialSyncState.filesProcessed, + filesIndexed: initialSyncState.filesIndexed, + totalFiles: initialSyncState.totalFiles, + currentFile: initialSyncState.currentFile, + } + : null, indexing: isWriting, watch: watchEnabled, }), @@ -572,7 +596,26 @@ export const serve = new Command("serve") res.statusCode = 200; res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify({ results: result.data })); + const response: { + results: typeof result.data; + partial?: boolean; + initialSync?: { + filesProcessed: number; + filesIndexed: number; + totalFiles: number; + }; + } = { results: result.data }; + + if (initialSyncState.inProgress) { + response.partial = true; + response.initialSync = { + filesProcessed: initialSyncState.filesProcessed, + filesIndexed: initialSyncState.filesIndexed, + totalFiles: initialSyncState.totalFiles, + }; + } + + res.end(JSON.stringify(response)); } finally { if (timeout) clearTimeout(timeout); } @@ -730,6 +773,7 @@ export const serve = new Command("serve") console.log( `osgrep server listening on http://localhost:${actualPort} (${projectRoot})`, ); + console.log("Starting initial index..."); } registerServer({ pid: process.pid, @@ -737,6 +781,11 @@ export const serve = new Command("serve") projectRoot, startTime: Date.now(), }); + + // Start initial sync after server is listening (non-blocking) + runInitialSync().catch((err) => { + console.error("Initial sync error:", err); + }); }); const shutdown = async () => { diff --git a/src/commands/trace.ts b/src/commands/trace.ts index 025479b0..28ac9572 100644 --- a/src/commands/trace.ts +++ b/src/commands/trace.ts @@ -12,11 +12,121 @@ const style = { yellow: (s: string) => `\x1b[33m${s}\x1b[39m`, }; +/** + * Dedupe and count callees. Returns "foo bar baz" or "foo bar (x3) baz" for repeats. + */ +function formatCallees(callees: string[]): string { + const counts = new Map<string, number>(); + for (const c of callees) { + counts.set(c, (counts.get(c) || 0) + 1); + } + return [...counts.entries()] + .map(([name, count]) => (count > 1 ? `${name} (x${count})` : name)) + .join(" "); +} + +/** + * Group callers by file. Returns map of file -> line numbers. + */ +function groupCallersByFile( + callers: Array<{ file: string; line: number; symbol: string }>, +): Map<string, number[]> { + const byFile = new Map<string, number[]>(); + for (const c of callers) { + const lines = byFile.get(c.file) || []; + lines.push(c.line); + byFile.set(c.file, lines); + } + // Sort lines within each file + for (const lines of byFile.values()) { + lines.sort((a, b) => a - b); + } + return byFile; +} + +/** + * Count total items for format decision. + */ +function countItems(graph: CallGraph): number { + const uniqueCallees = new Set(graph.callees).size; + const callerFiles = groupCallersByFile(graph.callers).size; + return uniqueCallees + callerFiles; +} + /** * Format call graph for minimal agent-friendly output. - * Format: file:line (space-separated for multiple) + * Uses tree format for small results, compact grouped format for large. */ function formatPlain(graph: CallGraph, symbol: string): string { + const itemCount = countItems(graph); + + // Use tree format for small results (cleaner), compact for large + if (itemCount <= 10) { + return formatPlainTree(graph, symbol); + } + return formatPlainCompact(graph, symbol); +} + +/** + * Tree format for small results - cleaner to read. + */ +function formatPlainTree(graph: CallGraph, symbol: string): string { + const lines: string[] = []; + + if (graph.center) { + lines.push(`${symbol} (${graph.center.file}:${graph.center.line})`); + } else { + lines.push(`${symbol} (not found)`); + } + + const hasCallees = graph.callees.length > 0; + const hasCallers = graph.callers.length > 0; + + if (hasCallees) { + const branch = hasCallers ? "├──" : "└──"; + lines.push(`${branch} calls:`); + + // Dedupe callees + const counts = new Map<string, number>(); + for (const c of graph.callees) { + counts.set(c, (counts.get(c) || 0) + 1); + } + const calleeList = [...counts.entries()]; + + calleeList.forEach(([name, count], i) => { + const prefix = hasCallers ? "│ " : " "; + const sym = i === calleeList.length - 1 ? "└──" : "├──"; + const countStr = count > 1 ? ` (x${count})` : ""; + lines.push(`${prefix}${sym} ${name}${countStr}`); + }); + } + + if (hasCallers) { + lines.push("└── called by:"); + const byFile = groupCallersByFile(graph.callers); + const files = [...byFile.entries()]; + + files.forEach(([file, fileLines], i) => { + const sym = i === files.length - 1 ? "└──" : "├──"; + const lineStr = + fileLines.length === 1 + ? `line ${fileLines[0]}` + : `lines ${fileLines.join(", ")}`; + lines.push(` ${sym} ${file}: ${lineStr}`); + }); + } + + if (!hasCallees && !hasCallers) { + lines.push(" (no callers or callees found)"); + } + + return lines.join("\n"); +} + +/** + * Compact grouped format for large results. + */ +function formatPlainCompact(graph: CallGraph, symbol: string): string { const lines: string[] = []; lines.push(symbol); @@ -27,14 +137,19 @@ function formatPlain(graph: CallGraph, symbol: string): string { } if (graph.callees.length > 0) { - lines.push(` calls: ${graph.callees.join(" ")}`); + lines.push(` calls: ${formatCallees(graph.callees)}`); } if (graph.callers.length > 0) { - const callerLocs = graph.callers - .map((c) => `${c.file}:${c.line}`) - .join(" "); - lines.push(` called_by: ${callerLocs}`); + lines.push(" called_by:"); + const byFile = groupCallersByFile(graph.callers); + for (const [file, fileLines] of byFile) { + const lineStr = + fileLines.length === 1 + ? `line ${fileLines[0]}` + : `lines ${fileLines.join(", ")}`; + lines.push(` ${file}: ${lineStr}`); + } } return lines.join("\n"); From d0cf7b215b469c980918c8debdb3cc970f0788a3 Mon Sep 17 00:00:00 2001 From: Ryan D'Onofrio <97113725+Ryandonofrio3@users.noreply.github.com> Date: Mon, 15 Dec 2025 05:08:57 -0800 Subject: [PATCH 19/19] ready for model work --- README.md | 9 +- src/commands/agent/opencode.ts | 3 - src/commands/search.ts | 159 +++++++++ src/commands/serve.ts | 35 +- src/lib/search/expander.ts | 567 ++++++++++++++++++++++++++++++ src/lib/search/expansion-types.ts | 101 ++++++ src/lib/search/searcher.ts | 62 ++++ tests/expander.test.ts | 524 +++++++++++++++++++++++++++ 8 files changed, 1454 insertions(+), 6 deletions(-) create mode 100644 src/lib/search/expander.ts create mode 100644 src/lib/search/expansion-types.ts create mode 100644 tests/expander.test.ts diff --git a/README.md b/README.md index cf94a6c9..47008e29 100644 --- a/README.md +++ b/README.md @@ -83,15 +83,20 @@ osgrep "how is the database connection pooled?" | `--scores` | Show relevance scores (0-1) for each result. | `false` | | `--min-score <n>` | Filter out results below this score threshold. | `0` | | `--compact` | Show file paths only (like `grep -l`). | `false` | +| `--deep` | Include related code (callers, definitions) for architectural context. | `false` | | `-s`, `--sync` | Force re-index changed files before searching. | `false` | | `-r`, `--reset` | Reset the index and re-index from scratch. | `false` | + **Examples:** ```bash # General concept search osgrep "API rate limiting logic" -# Deep dive (show more matches per file) +# Deep dive with architectural context (shows callers and definitions) +osgrep "how does authentication work" --deep + +# Show more matches per file osgrep "error handling" --per-file 5 # Just give me the files @@ -137,7 +142,7 @@ Runs a lightweight HTTP server with live file watching so searches stay hot in R - Keeps LanceDB and the embedding worker resident for <50ms responses. - Watches the repo (via chokidar) and incrementally re-indexes on change. - Health endpoint: `GET /health` -- Search endpoint: `POST /search` with `{ query, limit, path, rerank }` +- Search endpoint: `POST /search` with `{ query, limit, path, deep }` - Writes lock: `.osgrep/server.json` with `port`/`pid` **Options:** diff --git a/src/commands/agent/opencode.ts b/src/commands/agent/opencode.ts index 82a871d0..b503de38 100644 --- a/src/commands/agent/opencode.ts +++ b/src/commands/agent/opencode.ts @@ -80,9 +80,6 @@ Read the specific line range, not the whole file. # Trace call graph (who calls X, what X calls) osgrep trace handleAuth -# Skeleton of a huge file (to find which ranges to read) -osgrep skeleton src/giant-2000-line-file.ts - # Just file paths when you only need locations osgrep "authentication" --compact diff --git a/src/commands/search.ts b/src/commands/search.ts index 585e8b6c..d5a27922 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -8,6 +8,11 @@ import { } from "../lib/index/sync-helpers"; import { initialSync } from "../lib/index/syncer"; import { Searcher } from "../lib/search/searcher"; +import type { + ExpandedResult, + ExpandOptions, + ExpansionNode, +} from "../lib/search/expansion-types"; import { ensureSetup } from "../lib/setup/setup-helpers"; import type { ChunkType, @@ -21,6 +26,115 @@ import { isLocked } from "../lib/utils/lock"; import { ensureProjectPaths, findProjectRoot } from "../lib/utils/project-root"; import { getServerForProject, unregisterServer } from "../lib/utils/server-registry"; +/** + * Get expand options for --deep mode. + * Sweet spot config: callers + symbols at depth 2. + */ +function getDeepExpandOptions(deep: boolean): ExpandOptions | undefined { + if (!deep) return undefined; + + return { + maxDepth: 2, + maxExpanded: 20, + maxTokens: 0, + strategies: ["callers", "symbols"], + }; +} + +/** + * Format expanded results for plain text output. + */ +function formatExpandedPlain( + expanded: ExpandedResult, + projectRoot: string, +): string { + const lines: string[] = []; + + // Stats header + const { stats } = expanded; + lines.push( + `\n--- Expanded Results (${expanded.expanded.length} chunks) ---`, + ); + lines.push( + `symbols: ${stats.symbolsResolved} | callers: ${stats.callersFound} | neighbors: ${stats.neighborsAdded}${ + stats.budgetRemaining !== undefined + ? ` | tokens: ${stats.totalTokens} (${stats.budgetRemaining} remaining)` + : "" + }${expanded.truncated ? " [truncated]" : ""}`, + ); + lines.push(""); + + // Group by relationship type + const byRelation = new Map<string, ExpansionNode[]>(); + for (const node of expanded.expanded) { + const key = node.relationship; + if (!byRelation.has(key)) byRelation.set(key, []); + byRelation.get(key)!.push(node); + } + + for (const [relation, nodes] of byRelation) { + lines.push(`[${relation}]`); + for (const node of nodes) { + const rawPath = + typeof (node.chunk.metadata as FileMetadata | undefined)?.path === + "string" + ? ((node.chunk.metadata as FileMetadata).path as string) + : "Unknown"; + const relPath = path.isAbsolute(rawPath) + ? path.relative(projectRoot, rawPath) + : rawPath; + const start = node.chunk.generated_metadata?.start_line ?? 0; + const defined = node.chunk.defined_symbols?.slice(0, 3).join(", ") || ""; + lines.push( + ` ${relPath}:${start + 1} via="${node.via}" d=${node.depth}${ + defined ? ` [${defined}]` : "" + }`, + ); + } + lines.push(""); + } + + return lines.join("\n"); +} + +/** + * Format expanded results in compact TSV format. + */ +function formatExpandedCompact( + expanded: ExpandedResult, + projectRoot: string, +): string { + const lines: string[] = []; + lines.push(`expanded\tcount=${expanded.expanded.length}\ttruncated=${expanded.truncated}`); + lines.push("path\tlines\trelation\tvia\tdepth\tdefined"); + + for (const node of expanded.expanded) { + const rawPath = + typeof (node.chunk.metadata as FileMetadata | undefined)?.path === "string" + ? ((node.chunk.metadata as FileMetadata).path as string) + : "Unknown"; + const relPath = path.isAbsolute(rawPath) + ? path.relative(projectRoot, rawPath) + : rawPath; + const start = node.chunk.generated_metadata?.start_line ?? 0; + const end = node.chunk.generated_metadata?.end_line ?? start; + const defined = node.chunk.defined_symbols?.slice(0, 3).join(",") || "-"; + + lines.push( + [ + relPath, + `${start + 1}-${end + 1}`, + node.relationship, + node.via, + String(node.depth), + defined, + ].join("\t"), + ); + } + + return lines.join("\n"); +} + function toTextResults(data: SearchResponse["data"]): TextResult[] { return data.map((r) => { const rawPath = @@ -281,6 +395,12 @@ export const search: Command = new CommanderCommand("search") ) .option("--plain", "Disable ANSI colors and use simpler formatting", false) + .option( + "--deep", + "Include related code (callers, definitions) for architectural context", + false, + ) + .option( "-s, --sync", "Syncs the local files to the store before searching", @@ -304,6 +424,7 @@ export const search: Command = new CommanderCommand("search") minScore: string; compact: boolean; plain: boolean; + deep: boolean; sync: boolean; dryRun: boolean; } = cmd.optsWithGlobals(); @@ -389,6 +510,7 @@ export const search: Command = new CommanderCommand("search") path: exec_path ? path.relative(projectRootForServer, path.resolve(exec_path)) : undefined, + deep: options.deep, }), signal: ac.signal, }, @@ -409,6 +531,7 @@ export const search: Command = new CommanderCommand("search") filesIndexed: number; totalFiles: number; }; + expanded?: ExpandedResult; }; // Show warning if results are partial (server still indexing) @@ -441,6 +564,11 @@ export const search: Command = new CommanderCommand("search") ) : "No matches found."; console.log(compactText); + // Add expanded results in compact format + if (body.expanded && body.expanded.expanded.length > 0) { + console.log(""); + console.log(formatExpandedCompact(body.expanded, projectRootForServer)); + } return; // EXIT } @@ -467,12 +595,20 @@ export const search: Command = new CommanderCommand("search") }, ); console.log(output); + // Add expanded results + if (body.expanded && body.expanded.expanded.length > 0) { + console.log(formatExpandedPlain(body.expanded, projectRootForServer)); + } } else { const { formatResults } = await import("../lib/output/formatter"); const output = formatResults(filteredData, projectRootForServer, { content: options.content, }); console.log(output); + // Add expanded results in plain format + if (body.expanded && body.expanded.expanded.length > 0) { + console.log(formatExpandedPlain(body.expanded, projectRootForServer)); + } } return; // EXIT successful server search @@ -593,6 +729,7 @@ export const search: Command = new CommanderCommand("search") } const searcher = new Searcher(vectorDb); + const expandOpts = getDeepExpandOptions(options.deep); t("searcher.search"); const searchResult = await searcher.search( @@ -603,6 +740,15 @@ export const search: Command = new CommanderCommand("search") exec_path ? path.relative(projectRoot, path.resolve(exec_path)) : "", ); te("searcher.search"); + + // Expand results if requested (Phase 1-4 expansion) + let expanded: ExpandedResult | undefined; + if (expandOpts && searchResult.data.length > 0) { + t("searcher.expand"); + expanded = await searcher.expand(searchResult.data, pattern, expandOpts); + te("searcher.expand"); + } + te("total-cmd"); const filteredData = searchResult.data.filter( @@ -627,6 +773,11 @@ export const search: Command = new CommanderCommand("search") if (options.compact) { console.log(compactText); + // Add expanded results in compact format + if (expanded && expanded.expanded.length > 0) { + console.log(""); + console.log(formatExpandedCompact(expanded, projectRoot)); + } return; } @@ -643,6 +794,10 @@ export const search: Command = new CommanderCommand("search") showScores: options.scores, }); console.log(output); + // Add expanded results + if (expanded && expanded.expanded.length > 0) { + console.log(formatExpandedPlain(expanded, projectRoot)); + } } else { // Use new holographic formatter for TTY const { formatResults } = await import("../lib/output/formatter"); @@ -650,6 +805,10 @@ export const search: Command = new CommanderCommand("search") content: options.content, }); console.log(output); + // Add expanded results in plain format (for now) + if (expanded && expanded.expanded.length > 0) { + console.log(formatExpandedPlain(expanded, projectRoot)); + } } } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; diff --git a/src/commands/serve.ts b/src/commands/serve.ts index 627b32fd..d2514153 100644 --- a/src/commands/serve.ts +++ b/src/commands/serve.ts @@ -12,6 +12,7 @@ import type { InitialSyncProgress } from "../lib/index/sync-helpers"; import { initialSync } from "../lib/index/syncer"; import { GraphBuilder } from "../lib/graph/graph-builder"; import { Searcher } from "../lib/search/searcher"; +import type { ExpandOptions } from "../lib/search/expansion-types"; import type { MetaEntry } from "../lib/store/meta-cache"; import { MetaCache } from "../lib/store/meta-cache"; import type { VectorRecord } from "../lib/store/types"; @@ -568,10 +569,23 @@ export const serve = new Command("serve") } }, timeoutMs); + // Parse deep option from request body + let expandOpts: ExpandOptions | undefined; + if (body.deep === true) { + expandOpts = { + maxDepth: 2, + maxExpanded: 20, + maxTokens: 0, + strategies: ["callers", "symbols"], + }; + } + const debug = process.env.DEBUG_SERVER === "1"; if (debug) { console.log( - `[serve] Starting search for "${query}", indexing=${isWriting} signal.aborted=${ac.signal.aborted}`, + `[serve] Starting search for "${query}", indexing=${isWriting} signal.aborted=${ac.signal.aborted}${ + expandOpts ? " deep=true" : "" + }`, ); } @@ -594,6 +608,20 @@ export const serve = new Command("serve") return; } + // Expand results if requested + let expanded; + if (expandOpts && result.data.length > 0) { + if (debug) { + console.log(`[serve] Expanding results with depth=${expandOpts.maxDepth}`); + } + expanded = await searcher.expand(result.data, query, expandOpts); + if (debug) { + console.log( + `[serve] Expansion completed, ${expanded.expanded.length} expanded chunks`, + ); + } + } + res.statusCode = 200; res.setHeader("Content-Type", "application/json"); const response: { @@ -604,6 +632,7 @@ export const serve = new Command("serve") filesIndexed: number; totalFiles: number; }; + expanded?: typeof expanded; } = { results: result.data }; if (initialSyncState.inProgress) { @@ -615,6 +644,10 @@ export const serve = new Command("serve") }; } + if (expanded) { + response.expanded = expanded; + } + res.end(JSON.stringify(response)); } finally { if (timeout) clearTimeout(timeout); diff --git a/src/lib/search/expander.ts b/src/lib/search/expander.ts new file mode 100644 index 00000000..bf70d178 --- /dev/null +++ b/src/lib/search/expander.ts @@ -0,0 +1,567 @@ +/** + * Expander - Retrieval augmentation for osgrep search results. + * + * Transforms search results from "find relevant chunks" to "build understanding context" + * by following symbol references, finding callers, and including neighboring files. + * + * Design Principles: + * 1. Hot Path Stays Hot - No overhead when --expand is not used + * 2. Language Agnostic - Uses existing Tree-sitter extracted symbols, no import parsing + * 3. Graceful Degradation - Partial expansion is fine, never fail the search + * 4. Token Budget Aware - Respects LLM context limits + */ + +import * as path from "node:path"; +import type { Table } from "@lancedb/lancedb"; +import type { ChunkType } from "../store/types"; +import type { VectorDB } from "../store/vector-db"; +import type { + ExpandedResult, + ExpandOptions, + ExpansionNode, + ExpansionStats, +} from "./expansion-types"; + +/** Default options for expansion */ +const DEFAULT_OPTIONS: Required<ExpandOptions> = { + maxDepth: 1, + maxExpanded: 20, + maxTokens: 0, // 0 = unlimited + strategies: ["symbols", "callers", "neighbors"], +}; + +/** Tokens per character ratio (rough estimate for code) */ +const TOKENS_PER_CHAR = 0.25; + +/** + * Escape a string for use in SQL WHERE clauses. + */ +function escapeSql(str: string): string { + return str.replace(/'/g, "''"); +} + +/** + * Convert LanceDB array fields to string arrays. + */ +function toStrArray(val?: unknown): string[] { + if (!val) return []; + if (Array.isArray(val)) { + return val.filter((v) => typeof v === "string"); + } + if (typeof (val as any).toArray === "function") { + try { + const arr = (val as any).toArray(); + if (Array.isArray(arr)) return arr.filter((v) => typeof v === "string"); + return Array.from(arr || []).filter((v) => typeof v === "string"); + } catch { + return []; + } + } + return []; +} + +/** + * Estimate token count for a string. + */ +function estimateTokens(content: string): number { + return Math.ceil(content.length * TOKENS_PER_CHAR); +} + +/** + * Calculate file proximity score between two paths. + * Higher score = more related (same directory > nearby > distant). + */ +function fileProximity(fromPath: string, toPath: string): number { + if (fromPath === toPath) return 0; // Same file, skip + + const fromParts = fromPath.split("/"); + const toParts = toPath.split("/"); + + let common = 0; + for (let i = 0; i < Math.min(fromParts.length, toParts.length); i++) { + if (fromParts[i] === toParts[i]) common++; + else break; + } + + // More common path segments = higher score + return common / Math.max(fromParts.length, toParts.length); +} + +/** + * Extract import path hints from import strings. + * e.g., "../auth/handler" -> "auth/handler" + */ +function extractPathHint(importStr: string): string | null { + // Skip node_modules imports + if (!importStr.startsWith(".") && !importStr.startsWith("/")) return null; + + // Remove relative prefix and extension + return importStr + .replace(/^\.\.?\//, "") + .replace(/\.[^/.]+$/, "") + .split("/") + .slice(-2) + .join("/"); +} + +export class Expander { + constructor(private db: VectorDB) {} + + /** + * Expand search results to include related chunks. + * + * @param results Original search results + * @param query The original search query + * @param opts Expansion options + * @returns Expanded results with relationship metadata + */ + async expand( + results: ChunkType[], + query: string, + opts: ExpandOptions = {}, + ): Promise<ExpandedResult> { + const options = { ...DEFAULT_OPTIONS, ...opts }; + const { maxDepth, maxExpanded, maxTokens, strategies } = options; + + const stats: ExpansionStats = { + symbolsResolved: 0, + callersFound: 0, + neighborsAdded: 0, + totalChunks: results.length, + totalTokens: 0, + }; + + // Track seen chunk IDs to avoid duplicates + const seen = new Set<string>(); + for (const r of results) { + const id = this.getChunkId(r); + if (id) seen.add(id); + stats.totalTokens += estimateTokens(r.text || ""); + } + + // Token budget tracking + let budgetRemaining = maxTokens > 0 ? maxTokens - stats.totalTokens : Infinity; + + let table: Table; + try { + table = await this.db.ensureTable(); + } catch { + // Database not ready, return original results + return { + query, + original: results, + expanded: [], + truncated: false, + stats, + }; + } + + const allExpanded: ExpansionNode[] = []; + let truncated = false; + + // Multi-hop expansion using BFS + let frontier: { chunk: ChunkType; depth: number; score: number }[] = results.map( + (r) => ({ + chunk: r, + depth: 0, + score: 1.0, + }), + ); + + for (let depth = 1; depth <= maxDepth; depth++) { + if (truncated || budgetRemaining <= 0) break; + + const nextFrontier: typeof frontier = []; + + for (const node of frontier) { + if (allExpanded.length >= maxExpanded || budgetRemaining <= 0) { + truncated = true; + break; + } + + // Symbol resolution (Strategy 1) + if (strategies.includes("symbols")) { + const symbolNodes = await this.expandSymbols( + table, + node.chunk, + node.score, + depth, + seen, + maxExpanded - allExpanded.length, + budgetRemaining, + ); + + for (const n of symbolNodes) { + if (allExpanded.length >= maxExpanded || budgetRemaining <= 0) { + truncated = true; + break; + } + allExpanded.push(n); + nextFrontier.push({ chunk: n.chunk, depth, score: n.score }); + stats.symbolsResolved++; + const tokens = estimateTokens(n.chunk.text || ""); + stats.totalTokens += tokens; + budgetRemaining -= tokens; + } + } + + // Caller resolution (Strategy 2) + if (strategies.includes("callers") && !truncated) { + const callerNodes = await this.expandCallers( + table, + node.chunk, + node.score, + depth, + seen, + maxExpanded - allExpanded.length, + budgetRemaining, + ); + + for (const n of callerNodes) { + if (allExpanded.length >= maxExpanded || budgetRemaining <= 0) { + truncated = true; + break; + } + allExpanded.push(n); + nextFrontier.push({ chunk: n.chunk, depth, score: n.score }); + stats.callersFound++; + const tokens = estimateTokens(n.chunk.text || ""); + stats.totalTokens += tokens; + budgetRemaining -= tokens; + } + } + + // Neighbor expansion (Strategy 3) - only at depth 1 + if (strategies.includes("neighbors") && depth === 1 && !truncated) { + const neighborNodes = await this.expandNeighbors( + table, + node.chunk, + node.score, + depth, + seen, + maxExpanded - allExpanded.length, + budgetRemaining, + ); + + for (const n of neighborNodes) { + if (allExpanded.length >= maxExpanded || budgetRemaining <= 0) { + truncated = true; + break; + } + allExpanded.push(n); + stats.neighborsAdded++; + const tokens = estimateTokens(n.chunk.text || ""); + stats.totalTokens += tokens; + budgetRemaining -= tokens; + } + } + } + + frontier = nextFrontier; + } + + // Final sort by score (descending) + allExpanded.sort((a, b) => b.score - a.score); + + stats.totalChunks = results.length + allExpanded.length; + if (maxTokens > 0) { + stats.budgetRemaining = Math.max(0, budgetRemaining); + } + + return { + query, + original: results, + expanded: allExpanded, + truncated, + stats, + }; + } + + /** + * Expand by resolving referenced symbols to their definitions. + */ + private async expandSymbols( + table: Table, + chunk: ChunkType, + parentScore: number, + depth: number, + seen: Set<string>, + maxToAdd: number, + budgetRemaining: number, + ): Promise<ExpansionNode[]> { + const refs = toStrArray(chunk.referenced_symbols); + if (refs.length === 0) return []; + + const chunkPath = this.getChunkPath(chunk); + const importHints = toStrArray(chunk.imports).map(extractPathHint).filter(Boolean) as string[]; + const expanded: ExpansionNode[] = []; + + // Limit symbols to process per chunk + const symbolsToProcess = refs.slice(0, 10); + + for (const symbol of symbolsToProcess) { + if (expanded.length >= maxToAdd || budgetRemaining <= 0) break; + + try { + // Query for chunks that define this symbol + const results = await table + .query() + .where(`array_contains(defined_symbols, '${escapeSql(symbol)}')`) + .limit(10) + .toArray(); + + // Sort by proximity to requesting file (import hints help disambiguate) + const sorted = results + .map((r: any) => ({ + record: r, + proximity: fileProximity(chunkPath, r.path || ""), + importMatch: importHints.some((h) => (r.path || "").includes(h)), + })) + .sort((a, b) => { + // Import matches first + if (a.importMatch !== b.importMatch) return b.importMatch ? 1 : -1; + // Then by proximity + return b.proximity - a.proximity; + }) + .slice(0, 3); + + for (const { record, proximity } of sorted) { + const id = record.id; + if (!id || seen.has(id)) continue; + + const tokens = estimateTokens(record.content || record.display_text || ""); + if (tokens > budgetRemaining) continue; + + seen.add(id); + budgetRemaining -= tokens; + + const mappedChunk = this.mapRecordToChunk(record); + const score = parentScore * 0.7 * (0.5 + proximity * 0.5); + + expanded.push({ + chunk: mappedChunk, + relationship: "symbols", + via: symbol, + depth, + score, + }); + + if (expanded.length >= maxToAdd) break; + } + } catch (err) { + // Graceful degradation - skip this symbol + continue; + } + } + + return expanded; + } + + /** + * Expand by finding callers (chunks that reference this chunk's defined symbols). + */ + private async expandCallers( + table: Table, + chunk: ChunkType, + parentScore: number, + depth: number, + seen: Set<string>, + maxToAdd: number, + budgetRemaining: number, + ): Promise<ExpansionNode[]> { + const defined = toStrArray(chunk.defined_symbols); + if (defined.length === 0) return []; + + const chunkPath = this.getChunkPath(chunk); + const expanded: ExpansionNode[] = []; + + // Limit symbols to process + const symbolsToProcess = defined.slice(0, 5); + + for (const symbol of symbolsToProcess) { + if (expanded.length >= maxToAdd || budgetRemaining <= 0) break; + + try { + // Find chunks that reference this symbol + const results = await table + .query() + .where(`array_contains(referenced_symbols, '${escapeSql(symbol)}')`) + .limit(10) + .toArray(); + + // Sort by proximity + const sorted = results + .map((r: any) => ({ + record: r, + proximity: fileProximity(chunkPath, r.path || ""), + })) + .filter((x) => x.record.path !== chunkPath) // Exclude same file + .sort((a, b) => b.proximity - a.proximity) + .slice(0, 3); + + for (const { record, proximity } of sorted) { + const id = record.id; + if (!id || seen.has(id)) continue; + + const tokens = estimateTokens(record.content || record.display_text || ""); + if (tokens > budgetRemaining) continue; + + seen.add(id); + budgetRemaining -= tokens; + + const mappedChunk = this.mapRecordToChunk(record); + const score = parentScore * 0.6 * (0.5 + proximity * 0.5); + + expanded.push({ + chunk: mappedChunk, + relationship: "callers", + via: `uses ${symbol}`, + depth, + score, + }); + + if (expanded.length >= maxToAdd) break; + } + } catch { + continue; + } + } + + return expanded; + } + + /** + * Expand by including anchor chunks from the same directory. + */ + private async expandNeighbors( + table: Table, + chunk: ChunkType, + parentScore: number, + depth: number, + seen: Set<string>, + maxToAdd: number, + budgetRemaining: number, + ): Promise<ExpansionNode[]> { + const chunkPath = this.getChunkPath(chunk); + if (!chunkPath) return []; + + const dir = path.dirname(chunkPath); + const expanded: ExpansionNode[] = []; + + try { + // Get anchor chunks from same directory (file summaries) + const results = await table + .query() + .where(`path LIKE '${escapeSql(dir)}/%' AND is_anchor = true`) + .limit(5) + .toArray(); + + for (const record of results as any[]) { + if (expanded.length >= maxToAdd || budgetRemaining <= 0) break; + + const id = record.id; + if (!id || seen.has(id)) continue; + if (record.path === chunkPath) continue; // Skip same file + + const tokens = estimateTokens(record.content || record.display_text || ""); + if (tokens > budgetRemaining) continue; + + seen.add(id); + budgetRemaining -= tokens; + + const mappedChunk = this.mapRecordToChunk(record); + + expanded.push({ + chunk: mappedChunk, + relationship: "neighbors", + via: "same directory", + depth, + score: parentScore * 0.4, + }); + } + } catch { + // Graceful degradation + } + + return expanded; + } + + /** + * Get a unique ID for a chunk. + */ + private getChunkId(chunk: ChunkType): string { + if (chunk.metadata && typeof (chunk.metadata as any).hash === "string") { + return (chunk.metadata as any).hash; + } + const p = this.getChunkPath(chunk); + const start = chunk.generated_metadata?.start_line ?? 0; + return `${p}:${start}`; + } + + /** + * Get the file path from a chunk. + */ + private getChunkPath(chunk: ChunkType): string { + if (chunk.metadata && typeof (chunk.metadata as any).path === "string") { + return (chunk.metadata as any).path; + } + return ""; + } + + /** + * Map a LanceDB record to ChunkType. + */ + private mapRecordToChunk(record: any): ChunkType { + const startLine = record.start_line ?? 0; + const endLine = typeof record.end_line === "number" ? record.end_line : startLine; + const numLines = Math.max(1, endLine - startLine + 1); + + // Clean content (strip headers) + const content = record.display_text || record.content || ""; + const lines = content.split("\n"); + let startIdx = 0; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (!line) continue; + if (line.startsWith("// File:") || line.startsWith("File:")) continue; + if (line.startsWith("Imports:") || line.startsWith("Exports:")) continue; + if (line === "---" || line === "(anchor)") continue; + if (line.startsWith("//")) continue; + startIdx = i; + break; + } + + const bodyLines = lines.slice(startIdx); + const MAX_LINES = 15; + let truncatedText = bodyLines.slice(0, MAX_LINES).join("\n"); + if (bodyLines.length > MAX_LINES) { + truncatedText += `\n... (+${bodyLines.length - MAX_LINES} more lines)`; + } + + return { + type: "text", + text: truncatedText.trim(), + score: 0, + metadata: { + path: record.path || "", + hash: record.hash || "", + is_anchor: !!record.is_anchor, + }, + generated_metadata: { + start_line: startLine, + end_line: endLine, + num_lines: numLines, + type: record.chunk_type, + }, + complexity: record.complexity, + is_exported: record.is_exported, + role: record.role, + parent_symbol: record.parent_symbol, + defined_symbols: toStrArray(record.defined_symbols), + referenced_symbols: toStrArray(record.referenced_symbols), + imports: toStrArray(record.imports), + exports: toStrArray(record.exports), + }; + } +} diff --git a/src/lib/search/expansion-types.ts b/src/lib/search/expansion-types.ts new file mode 100644 index 00000000..1e5b7c9b --- /dev/null +++ b/src/lib/search/expansion-types.ts @@ -0,0 +1,101 @@ +/** + * Types for the retrieval augmentation (--expand) feature. + * + * This feature transforms osgrep from "find relevant chunks" to + * "build understanding context" by following symbol references, + * finding callers, and including neighboring files. + */ + +import type { ChunkType } from "../store/types"; + +/** + * Strategy for expanding search results. + * - symbols: Follow referenced_symbols → defined_symbols + * - callers: Reverse lookup - who references my defined_symbols + * - neighbors: Same directory files (anchor chunks) + * - coexports: Files that export symbols I import + */ +export type ExpandStrategy = "symbols" | "callers" | "neighbors" | "coexports"; + +/** + * Options for result expansion. + */ +export interface ExpandOptions { + /** Maximum traversal depth (default: 1) */ + maxDepth?: number; + /** Maximum number of expanded chunks to return (default: 20) */ + maxExpanded?: number; + /** Maximum token budget for expanded results (default: unlimited) */ + maxTokens?: number; + /** Which strategies to use (default: all) */ + strategies?: ExpandStrategy[]; +} + +/** + * A single expanded chunk with relationship metadata. + */ +export interface ExpansionNode { + /** The actual chunk data */ + chunk: ChunkType; + /** How this chunk is related to the original results */ + relationship: ExpandStrategy; + /** Human-readable explanation of the relationship */ + via: string; + /** How many hops from the original result */ + depth: number; + /** Relevance score (higher = more relevant, decays with depth) */ + score: number; +} + +/** + * Statistics about the expansion process. + */ +export interface ExpansionStats { + /** Number of symbols successfully resolved to definitions */ + symbolsResolved: number; + /** Number of callers found */ + callersFound: number; + /** Number of neighbor files added */ + neighborsAdded: number; + /** Total chunks in the expanded result */ + totalChunks: number; + /** Estimated total tokens in the result */ + totalTokens: number; + /** Remaining token budget (if maxTokens was set) */ + budgetRemaining?: number; +} + +/** + * The result of expanding search results. + */ +export interface ExpandedResult { + /** The original search query */ + query: string; + /** Original search results (unchanged) */ + original: ChunkType[]; + /** Expanded chunks with relationship metadata */ + expanded: ExpansionNode[]; + /** Whether any limit was hit during expansion */ + truncated: boolean; + /** Expansion statistics */ + stats: ExpansionStats; +} + +/** + * Internal representation of a chunk for expansion processing. + * Contains the raw data needed for symbol/caller resolution. + */ +export interface ExpansionChunk { + id: string; + path: string; + startLine: number; + endLine: number; + content: string; + definedSymbols: string[]; + referencedSymbols: string[]; + imports: string[]; + exports: string[]; + isAnchor: boolean; + role?: string; + complexity?: number; +} diff --git a/src/lib/search/searcher.ts b/src/lib/search/searcher.ts index 5ded9463..c72c3f32 100644 --- a/src/lib/search/searcher.ts +++ b/src/lib/search/searcher.ts @@ -10,6 +10,8 @@ import type { import type { VectorDB } from "../store/vector-db"; import { escapeSqlString, normalizePath } from "../utils/filter-builder"; import { encodeQuery, rerank } from "../workers/orchestrator"; +import { Expander } from "./expander"; +import type { ExpandedResult, ExpandOptions } from "./expansion-types"; export class Searcher { constructor(private db: VectorDB) {} @@ -528,4 +530,64 @@ export class Searcher { }), }; } + + /** + * Expand search results to include related chunks. + * This is opt-in and adds no overhead when not used. + * + * @param results Original search results + * @param query The original search query + * @param opts Expansion options + * @returns Expanded results with relationship metadata + */ + async expand( + results: ChunkType[], + query: string, + opts?: ExpandOptions, + ): Promise<ExpandedResult> { + const expander = new Expander(this.db); + return expander.expand(results, query, opts); + } + + /** + * Search and expand in one call. + * + * @param query Search query + * @param top_k Number of results + * @param searchOpts Search options + * @param filters Search filters + * @param pathPrefix Path prefix filter + * @param signal Abort signal + * @param expandOpts Expansion options (if provided, results will be expanded) + * @returns Search response with optional expansion + */ + async searchAndExpand( + query: string, + top_k?: number, + searchOpts?: { rerank?: boolean }, + filters?: SearchFilter, + pathPrefix?: string, + signal?: AbortSignal, + expandOpts?: ExpandOptions, + ): Promise<SearchResponse & { expanded?: ExpandedResult }> { + const searchResult = await this.search( + query, + top_k, + searchOpts, + filters, + pathPrefix, + signal, + ); + + if (!expandOpts) { + return searchResult; + } + + const expanded = await this.expand(searchResult.data, query, expandOpts); + + return { + ...searchResult, + expanded, + }; + } } diff --git a/tests/expander.test.ts b/tests/expander.test.ts new file mode 100644 index 00000000..ad91f56a --- /dev/null +++ b/tests/expander.test.ts @@ -0,0 +1,524 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; +import type { ChunkType } from "../src/lib/store/types"; + +// Mock VectorDB +const mockQuery = vi.fn(); +const mockTable = { + query: vi.fn(() => ({ + where: vi.fn(() => ({ + limit: vi.fn(() => ({ + toArray: mockQuery, + })), + })), + })), +}; + +const mockDb = { + ensureTable: vi.fn(async () => mockTable), +}; + +vi.mock("../src/lib/store/vector-db", () => ({ + VectorDB: vi.fn(() => mockDb), +})); + +import { Expander } from "../src/lib/search/expander"; +import type { ExpandOptions } from "../src/lib/search/expansion-types"; + +describe("Expander", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockQuery.mockResolvedValue([]); + }); + + const createMockChunk = (overrides: Partial<ChunkType> = {}): ChunkType => ({ + type: "text", + text: "function test() { return 42; }", + score: 0.9, + metadata: { + path: "src/lib/utils.ts", + hash: "abc123", + }, + generated_metadata: { + start_line: 10, + end_line: 15, + num_lines: 6, + }, + defined_symbols: ["test"], + referenced_symbols: ["helper", "config"], + imports: [], + exports: [], + ...overrides, + }); + + describe("symbol resolution", () => { + it("resolves symbols to correct definitions", async () => { + const expander = new Expander(mockDb as any); + + // Mock definition found for "helper" symbol + mockQuery.mockResolvedValueOnce([ + { + id: "def-helper", + path: "src/lib/helper.ts", + content: "export function helper() {}", + display_text: "export function helper() {}", + start_line: 1, + end_line: 3, + defined_symbols: ["helper"], + referenced_symbols: [], + is_anchor: false, + }, + ]); + + const results = [createMockChunk()]; + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols"], + }); + + expect(expanded.expanded.length).toBeGreaterThan(0); + expect(expanded.expanded[0].relationship).toBe("symbols"); + expect(expanded.expanded[0].via).toBe("helper"); + expect(expanded.stats.symbolsResolved).toBeGreaterThan(0); + }); + + it("prefers same-directory definitions", async () => { + const expander = new Expander(mockDb as any); + + // Return two definitions - one in same dir, one in different dir + mockQuery.mockResolvedValueOnce([ + { + id: "def-distant", + path: "src/other/helper.ts", + content: "export function helper() {}", + start_line: 1, + end_line: 3, + defined_symbols: ["helper"], + referenced_symbols: [], + }, + { + id: "def-nearby", + path: "src/lib/helper.ts", + content: "export function helper() {}", + start_line: 1, + end_line: 3, + defined_symbols: ["helper"], + referenced_symbols: [], + }, + ]); + + const results = [createMockChunk()]; + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols"], + maxExpanded: 1, + }); + + // Should prefer the same-directory definition + expect(expanded.expanded.length).toBe(1); + const expandedPath = + (expanded.expanded[0].chunk.metadata as any)?.path || ""; + expect(expandedPath).toContain("src/lib"); + }); + + it("respects maxExpanded limit", async () => { + const expander = new Expander(mockDb as any); + + // Return many definitions for each symbol query + const manyDefs = Array.from({ length: 50 }, (_, i) => ({ + id: `def-${i}`, + path: `src/file${i}.ts`, + content: `export function func${i}() {}`, + start_line: 1, + end_line: 3, + defined_symbols: [`func${i}`], + referenced_symbols: [], + })); + + // Mock returns subset of definitions for each query + mockQuery.mockImplementation(() => Promise.resolve(manyDefs.slice(0, 5))); + + const results = [ + createMockChunk({ + referenced_symbols: manyDefs.map((_, i) => `func${i}`), + }), + ]; + + const maxExpanded = 5; + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols"], + maxExpanded, + }); + + // Should respect the limit + expect(expanded.expanded.length).toBeLessThanOrEqual(maxExpanded); + }); + + it("handles circular references without infinite loop", async () => { + const expander = new Expander(mockDb as any); + + // Chunk A refs B, query returns chunk that refs A + mockQuery.mockResolvedValueOnce([ + { + id: "def-helper", + path: "src/lib/helper.ts", + content: "export function helper() { test(); }", + start_line: 1, + end_line: 3, + defined_symbols: ["helper"], + referenced_symbols: ["test"], // References back! + }, + ]); + + const results = [createMockChunk()]; + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols"], + maxDepth: 2, + }); + + // Should complete without error and not include duplicates + const ids = new Set( + expanded.expanded.map( + (e) => (e.chunk.metadata as any)?.hash || e.chunk.text, + ), + ); + expect(ids.size).toBe(expanded.expanded.length); + }); + + it("returns empty expansion gracefully when no definitions found", async () => { + const expander = new Expander(mockDb as any); + + // No definitions found + mockQuery.mockResolvedValue([]); + + const results = [createMockChunk()]; + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols"], + }); + + // Should return original results with empty expanded + expect(expanded.original).toEqual(results); + expect(expanded.expanded).toEqual([]); + expect(expanded.stats.symbolsResolved).toBe(0); + }); + }); + + describe("caller expansion", () => { + it("finds callers that reference defined symbols", async () => { + const expander = new Expander(mockDb as any); + + // Mock caller found + mockQuery.mockResolvedValueOnce([ + { + id: "caller-1", + path: "src/routes/handler.ts", + content: "import { test } from '../lib/utils'; test();", + start_line: 5, + end_line: 10, + defined_symbols: ["handleRequest"], + referenced_symbols: ["test"], + is_anchor: false, + }, + ]); + + const results = [createMockChunk()]; + const expanded = await expander.expand(results, "test query", { + strategies: ["callers"], + }); + + expect(expanded.expanded.length).toBeGreaterThan(0); + expect(expanded.expanded[0].relationship).toBe("callers"); + expect(expanded.expanded[0].via).toContain("uses"); + expect(expanded.stats.callersFound).toBeGreaterThan(0); + }); + }); + + describe("neighbor expansion", () => { + it("includes anchor chunks from same directory", async () => { + const expander = new Expander(mockDb as any); + + // First query is for symbols (returns nothing) + mockQuery.mockResolvedValueOnce([]); + // Second query for callers (returns nothing) + mockQuery.mockResolvedValueOnce([]); + // Third query for neighbors + mockQuery.mockResolvedValueOnce([ + { + id: "neighbor-anchor", + path: "src/lib/other.ts", + content: "// File: src/lib/other.ts\nExports: helper", + start_line: 0, + end_line: 5, + defined_symbols: [], + referenced_symbols: [], + is_anchor: true, + }, + ]); + + const results = [createMockChunk()]; + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols", "callers", "neighbors"], + }); + + const neighbors = expanded.expanded.filter( + (e) => e.relationship === "neighbors", + ); + expect(neighbors.length).toBeGreaterThanOrEqual(0); + if (neighbors.length > 0) { + expect(neighbors[0].via).toBe("same directory"); + expect(expanded.stats.neighborsAdded).toBeGreaterThan(0); + } + }); + }); + + describe("multi-hop expansion", () => { + it("follows chains of dependencies with depth > 1", async () => { + const expander = new Expander(mockDb as any); + + // Depth 1: A -> B + mockQuery + .mockResolvedValueOnce([ + { + id: "def-B", + path: "src/lib/b.ts", + content: "export function funcB() { funcC(); }", + start_line: 1, + end_line: 3, + defined_symbols: ["funcB"], + referenced_symbols: ["funcC"], + }, + ]) + // Depth 2: B -> C + .mockResolvedValueOnce([ + { + id: "def-C", + path: "src/lib/c.ts", + content: "export function funcC() {}", + start_line: 1, + end_line: 3, + defined_symbols: ["funcC"], + referenced_symbols: [], + }, + ]); + + const results = [ + createMockChunk({ + referenced_symbols: ["funcB"], + }), + ]; + + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols"], + maxDepth: 2, + }); + + // Should have found both B and C + expect(expanded.expanded.length).toBeGreaterThanOrEqual(1); + }); + + it("scores decay with depth", async () => { + const expander = new Expander(mockDb as any); + + // Return definitions at different depths + mockQuery.mockResolvedValue([ + { + id: "def-1", + path: "src/lib/dep.ts", + content: "export function dep() {}", + start_line: 1, + end_line: 3, + defined_symbols: ["dep"], + referenced_symbols: [], + }, + ]); + + const results = [createMockChunk({ referenced_symbols: ["dep"] })]; + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols"], + maxDepth: 1, + }); + + // All expanded chunks should have score < 1 (decayed from parent) + for (const node of expanded.expanded) { + expect(node.score).toBeLessThan(1.0); + } + }); + }); + + describe("token budgeting", () => { + it("stops expanding when token budget exhausted", async () => { + const expander = new Expander(mockDb as any); + + // Return large chunks + const largeDefs = Array.from({ length: 10 }, (_, i) => ({ + id: `def-${i}`, + path: `src/file${i}.ts`, + content: "x".repeat(1000), // ~250 tokens each + start_line: 1, + end_line: 100, + defined_symbols: [`func${i}`], + referenced_symbols: [], + })); + + mockQuery.mockResolvedValue(largeDefs); + + const results = [ + createMockChunk({ + referenced_symbols: largeDefs.map((_, i) => `func${i}`), + }), + ]; + + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols"], + maxTokens: 500, // Should only fit 1-2 chunks + }); + + expect(expanded.stats.totalTokens).toBeLessThanOrEqual(500 + 100); // Some buffer + expect(expanded.stats.budgetRemaining).toBeDefined(); + }); + + it("prioritizes high-value chunks within budget", async () => { + const expander = new Expander(mockDb as any); + + mockQuery.mockResolvedValue([ + { + id: "def-nearby", + path: "src/lib/nearby.ts", + content: "export function nearby() {}", + start_line: 1, + end_line: 3, + defined_symbols: ["nearby"], + referenced_symbols: [], + }, + ]); + + const results = [ + createMockChunk({ + referenced_symbols: ["nearby"], + }), + ]; + + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols"], + maxTokens: 5000, // Generous budget + }); + + // Should include the nearby definition with high score + if (expanded.expanded.length > 0) { + expect(expanded.expanded[0].score).toBeGreaterThan(0); + } + }); + }); + + describe("graceful degradation", () => { + it("returns original results when database unavailable", async () => { + const failingDb = { + ensureTable: vi.fn(async () => { + throw new Error("Database unavailable"); + }), + }; + + const expander = new Expander(failingDb as any); + const results = [createMockChunk()]; + + const expanded = await expander.expand(results, "test query"); + + expect(expanded.original).toEqual(results); + expect(expanded.expanded).toEqual([]); + expect(expanded.truncated).toBe(false); + }); + + it("continues expansion when individual queries fail", async () => { + const expander = new Expander(mockDb as any); + + // First query fails, second succeeds + mockQuery + .mockRejectedValueOnce(new Error("Query failed")) + .mockResolvedValueOnce([ + { + id: "def-success", + path: "src/lib/success.ts", + content: "export function success() {}", + start_line: 1, + end_line: 3, + defined_symbols: ["success"], + referenced_symbols: [], + }, + ]); + + const results = [ + createMockChunk({ + referenced_symbols: ["failing", "success"], + }), + ]; + + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols"], + }); + + // Should still have results from successful query + expect(expanded.expanded.length).toBeGreaterThanOrEqual(0); + }); + }); +}); + +describe("ExpandOptions", () => { + it("uses default options when none provided", async () => { + const expander = new Expander(mockDb as any); + mockQuery.mockResolvedValue([]); + + const results = [ + { + type: "text" as const, + text: "test", + score: 0.9, + metadata: { path: "test.ts", hash: "abc" }, + generated_metadata: { start_line: 1, end_line: 5 }, + defined_symbols: [], + referenced_symbols: [], + }, + ]; + + const expanded = await expander.expand(results, "query"); + + // Should complete without error using defaults + expect(expanded.original).toEqual(results); + }); + + it("respects custom strategies", async () => { + const expander = new Expander(mockDb as any); + + mockQuery.mockResolvedValue([ + { + id: "def-1", + path: "src/lib/dep.ts", + content: "export function dep() {}", + start_line: 1, + end_line: 3, + defined_symbols: ["dep"], + referenced_symbols: [], + }, + ]); + + const results = [ + { + type: "text" as const, + text: "function test() { dep(); }", + score: 0.9, + metadata: { path: "test.ts", hash: "abc" }, + generated_metadata: { start_line: 1, end_line: 5 }, + defined_symbols: ["test"], + referenced_symbols: ["dep"], + }, + ]; + + // Only use symbols strategy + const expanded = await expander.expand(results, "query", { + strategies: ["symbols"], + }); + + // Should only have symbol expansions + for (const node of expanded.expanded) { + expect(node.relationship).toBe("symbols"); + } + expect(expanded.stats.callersFound).toBe(0); + expect(expanded.stats.neighborsAdded).toBe(0); + }); +});