From b558d48a73895d0c129372c1af93bfb303c68e46 Mon Sep 17 00:00:00 2001 From: nexiumbiz-debug <265948173+nexiumbiz-debug@users.noreply.github.com> Date: Fri, 15 May 2026 03:11:15 -0400 Subject: [PATCH] Add citation provenance readiness module --- citation-provenance-readiness/README.md | 42 ++ citation-provenance-readiness/docs/demo.mp4 | Bin 0 -> 61964 bytes citation-provenance-readiness/docs/demo.svg | 21 + .../examples/project.json | 123 ++++++ citation-provenance-readiness/package.json | 16 + .../requirement-map.md | 34 ++ .../scripts/render-demo.mjs | 46 ++ .../src/citation-provenance-readiness.mjs | 404 ++++++++++++++++++ .../citation-provenance-readiness.test.mjs | 49 +++ 9 files changed, 735 insertions(+) create mode 100644 citation-provenance-readiness/README.md create mode 100644 citation-provenance-readiness/docs/demo.mp4 create mode 100644 citation-provenance-readiness/docs/demo.svg create mode 100644 citation-provenance-readiness/examples/project.json create mode 100644 citation-provenance-readiness/package.json create mode 100644 citation-provenance-readiness/requirement-map.md create mode 100644 citation-provenance-readiness/scripts/render-demo.mjs create mode 100644 citation-provenance-readiness/src/citation-provenance-readiness.mjs create mode 100644 citation-provenance-readiness/tests/citation-provenance-readiness.test.mjs diff --git a/citation-provenance-readiness/README.md b/citation-provenance-readiness/README.md new file mode 100644 index 0000000..eb4b3d6 --- /dev/null +++ b/citation-provenance-readiness/README.md @@ -0,0 +1,42 @@ +# Citation Provenance Readiness + +This module is a focused slice for SCIBASE issue `#13`, AI-Assisted Research Tools. + +It does not call external AI services or citation APIs. Instead, it gives reviewers a deterministic readiness gate for manuscript claims before a paper leaves the workspace: + +- extracts evidence-backed summary modes for a manuscript packet +- checks whether research claims have citation keys, bibliography records, and local evidence artifacts +- flags citation, statistics, and compliance gaps +- ranks candidate citations with transparent overlap, DOI, recency, and open-access signals +- emits APA and Nature-style insertion plans for reviewer action +- produces a stable digest for audit packets + +## Local Verification + +```bash +cd citation-provenance-readiness +npm run check +npm test +npm run demo +npm run render-demo +``` + +The demo is based on synthetic sample data in `examples/project.json`. + +## Demo Output + +```text +Project: Protocol-Guided Perturbation Screening +Status: hold +Readiness score: 22 +Claims reviewed: 3 +Missing bibliography keys: missing2026 +Top citation insertion: nguyen2025 +Top reviewer action: Add at least one source-backed citation or mark the claim as internal preliminary evidence. +``` + +## Why This Slice Is Distinct + +Existing issue `#13` submissions mostly cover broad AI summarizer, peer-review, and citation demos. This module targets the provenance boundary: whether a concrete manuscript packet has traceable sources, evidence artifacts, statistical context, citation insertions, and compliance statements before an AI-assisted research tool recommends release. + +The implementation is dependency-free, credential-free, and uses synthetic data only. diff --git a/citation-provenance-readiness/docs/demo.mp4 b/citation-provenance-readiness/docs/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..2a66c001c1452b699d6959d73c341dffb8358db8 GIT binary patch literal 61964 zcmYJaW0)pQ6E4`cZQJJbv~5h=#?!WKOxw0?W7@WD+uDA=vuF3Zsxkw2L`Fnr{;A3Y z0sj3|x$ut?k&Dfq;OZOdU*2fPhZFt&I(welRr<;NRaB z8=|LON2?Mo$#g43tE5-gZY&&ZMD#>Pwhksl%pB}Q4y-IJtVB#~TuerWOg{<~ZA#7{s4lprwB4S};Vx?zcVqyOgnmak!@h~#Fy1FvBSQwkw z0t{>zY#q!P{~LwD+{qg7gR!-9vaq#rgM zSXhVvwzgIV=0BX?{}r+lIRY$|o%oXJl(_XW;b18~xPC$-%(F=Eubk(81t8jH!cxwTa_TwG8#_+<#~bV?LH2+Q8Vr z?!Pn)^$aZx9RC}{!olQ!iMg6sn3+2n{^)G&OlqexMyJG0g~&$ji(;#1Tl zy-*dut5dOme0QTN*OEZ?x67}vJQ%;S`R{CM9}<^U{V?>@tq__-P7g9XI;}*3mFY2@ zpWXKYnP`OQxA>l5Dr(?nNHaVF7<4Y}|c3X%{;#`lZ3>Xdbuom-_( zjyhhC_=B}5|B$EQF^HWyn-IxJ2CL-9ihpQiz+SQCio zvbSc23UqpCpWn@*7y3mxA@dhp1eU+HV`z+bi=ISC3XM!jAY=>r4<#gN{CK<+DRc!~ zRy}wYI$!0_Vfqg>Cixfz-%xTE2n}@L?C%;7Z=ZQnVFExZhTK0^oK)>pq}FI3QOwh; z7YiClFI8xO?Z-f$OjOM>Wdrg-Dms9KsP@I?KQmyd7xklQ-RItc5wBKmx3X9^ud0qO zQZ81eRZ=`0U;oBXw{`F-Ev?o9JIHSS2&F8+?KVR0FdsX!vb1mI*wsS?dEWEPvSV3md!9jr;z97ooeF<(9MZvXJ zeRt(!^0fv56T66_DZYXrpU?<0MoVJls*CpfPJ#OBp5A0LotDs=`m7gV9INb;+5?7} zw@+C4B&FQ|1J9c;nN zga>WEeIqU>?$IDk>dJ`TZE*fmY2Xp@5iXA7VhTG{J9$JDeR?GUBf5!yZfn59aPxrXCRt8guHwdVDvec7Y zD4D@YN)5FoR-{va;0^VriKYKSGb9-b=CTBS{n{!c_h%E>;;KABAZ#a*7re=33Ldmh z(3?)^9av0~EO*7G#fT0uM{rpb#vQw`U}g<)T=gn2VMvsKvX@rnNg_=G<{daAr`Ck| zv@3ffE?!6_Nj{VN_E|jXWH&9%HL&Ayfg~EWAH2``_T;~0&ld56{5RD=^19pp?3nXB zayP$P=WoN?SydqT%=-1z9=|xa71bzK zzrndP*g`h2fP+66h&od4;;)P9_+$exiN$ko64&%gqPcub+dWlgU;s^{MyXqN#!R}@ zH6deo39s8{Ing>@FY=-SjwFpbbJ-Pp;8qhS(4sSnWA(=q+d|C`Oa0k&<`i)j2_L{AV(|<87f?s1vtBqvfmgQ+R z>6RBt2Df@{z3mJdrzs@RX38;ZM!{UIh9pWlNuZGs;|M~?=S^Ol{*ob7- zt}kqlv3eq#BxlJ?y~UfZh;|_*)7M_zM>%2C?xSfYE6Xt|J8Kxv*K$lO(|L48e|^Fj zQm01nl9jlOHN*+xdpBy9i#E=Wbpg>#+(_H{TlW&ekpW2{QjGSPSOqE+{*7r#pP_a( z-eIMfUxGOMDNnK20SAGJY2G4r7z#9xBJTnPf;YYDSIg(0BJ`E&nw_tnLk8lT^AYG! zuD$iJ^_~MS4O#*O=L|_snDX@1xBL5Vg@H{EKx)_@T}gPGnWZFgb4PdC*s#U3eQU@l zQElwWhwzfXthVnjjxGB}ck7EJjs%n8Gj-iQf|E0S+7D`Tr|7>R2XfN^s)*ppw?*#g z87~{B$d<`s$R#aPjk4^v*W$eF^z8?2H}$$I&-n0Hp2>0cW~yaONJKstZfqF%s6O(G zve*Ishz%O}mFS6MAc<2C+F00%Q|clsVrGuW_P@YDM;wjaolt~$__^GK3X3;7@$|rC z2=JI^MGiL7a6r~+p3B2!F}tI)6Bo|VXXep^!Z&<+_>#!<5r3~7I^d2QL22txz1C)9 z5TvzKS@w3a`bE1a%^kruO z2@QBK>Qb&V04N{Dse~eczwy}bB1&6H<%J`zcsfI0%`l_?W`Rk_P6qKfG2AfCS|dVY zjg@HU zT%D7XpcFeqt-Qst2%0Voth^YIeVS?gX8o3j$Ivw6;2@}HV>pJ4Q4AKM39uI|t{LZl znVK`!ecCwZj2iP&!%bKxECx+a54_j(J_194igo4ncP%Nk}{wg&U;vZOSczL}*D_HYevZKK7u+b~_R z=&$bc9;)N9>qsrtwGsjVOZhIQq951<&c9R4+dL~@!_|6`q>)tVD9J;{1)h8yp*2u) z7FykPNXt}o#BGSxKCtqeL0MK|6Cn#s4M%8rVhzM zEJ*ZIm%8J+B|xEAtyLbOMs;yTRe3|nMu>>MiYMShCzs;1`iZmy;u#etShPbOC!z?W=sWZn0 zM(>AE%FpFf6W!fA8*l%#rP1}(NDk%z1F5C_4eLnBiH_1ao^%e?$Ai|uIT%x8MpWgJ z0fc&}qKQ#=?YT%y94T5qZV~_MfY39jKww2O21Ld>w|koq1nfdigF#X#qzmoE@w=EE zurcwkWUkv#<>796DXNH)*oqk{$;%Whi@G{)dtef1V=!{$_qw+^`#eHvhwF9E5p0K{ z2|I(S>EBDsuAC_yaKy5HKuXsDsubZ@0x=suj{fVN1XODUi#%PR&Oz`z;woVZk&ku#kts0>PDmU{mAsx=%@L330nnobBa} z9sp*DcNk7TWU@&gP*>J7fPP}+E!h^oQ8QD;K%4kG=_(ESWFw9Mm~jvC2GZfhw?lqu zHJuteAVoF}=N;^wKxN;~UBom$T~3M+7n`0My4j$v!UR*)}P&^^_3yab5W@8vjrFOK`CI{X)vPm zcNR}QjPKQ&zdh-9IqXAs?D?Y%a3rwEkua_dL(p9kR-27WY3oWk6N|$~Kppw=tVAGi zD#nA5;#=?ws$ytff#=+CELM>+gd1TFUP!qbo~55W&wunb}v;?N)E49AU9#5k0osuK)&xWtVA+=Y%ER}aA~|U0GzA51Ceep zW?b#-m!7+6x_?*#xo!j1ZfhJW;NygDb&hRfIp2V(N$vTEE@GMc+S^R|;IN(t`}WDU zQ6Q4R!jj1q%$dzQ@mTy(ff;4PUUsC?04A4{mn0`Dwdgz3bS>2?iSWF^Ojs-lo)FmP zvVfs&`*h_hTXJ--%QX4c@00-1xxEvK(Ut2}(rFQmlxwZl%(>nrPi$o1%OX6NPSp-5 zaFF>^k3atBMGgXFmKG;KKs4Zf2d`~Z@=21xqZ*UWBtdH3PDu*rvxgk!u56pA167jH ze`_rHl!E>wt_FrgzhXGsO8m)lExw6~`9K1U53+CdjO(-LA3p}R=~I|zWX@4p5InD+ z)sjv9CAOuUXs_5Xc=ZkpaLqYm`(y^Yk-yq@J5C-la?AE>7ChgH0FGPc2kuIDjGT;CI_?W7^b>22A}Yt49X;BzWoq5Xo|_v*H{* z)Vx}^6(aPm(d@ArC5_Qyjf68o9VB&TL|&t~TEi7wT?wyOnOnl|o&!(qVGqP)exWk- zMZ6sQig&fmRxEy4KEXDla&?nE>m9lGfNyaa>CArFt=&3WLmx?D(kIBY|n~o zH!1rH3eOlU%LoC~dkDzy9pUdP@TzjVJUO~t^HvwQf|&sUrocR<2eKR1d|n6Dg4K%7 zFCd8eBt1uTk!t-tYd-Kl?A};sFB>ess_5MQ(ptV?+J3Q6c0rEl%YnI)NAZ62n`f3W zMFy;qOBQOH=Au#n(T`03xk8lc*3EMFkq!k6w5QKl>JV)|`Dkd329YFO5}tIRh(tC~SNGeA@y!vMF&Actrtv`7KfwkZoeb57EhkPw9(YoI>7Z%AZL>p?O+I#FG6tzVWxjF1f zqdi6O*e|}0=(i@=FfI9KKcnnlwio#)^J19NZtXl4>zO*j|7$SgtJuEem=xC7(L*rO zDOdXzo_&qTA#+}S8IUgd+fmh#hVTnwxHS8THR*X}tit+vXXx$oD;u8HQf?o!-3qlM z$X<2BH|2)qV(h&{sb+{>>yl)@(=OP@8+!4%R)`leSPtm2ga@hb!hqth_X#)WXe) zuT%?p5e$(I>I#P$iO_Rn+NJAuw|89B7}+OQTGd>Vx2Z^qwM_yLF(zuTKO59h* zY#l}5io+cFhNc2*Bt42JyHaQFTcW6?JHj6Y;le6|YT@Ump&tp{F=V6 z#=aUplV`VkiEwz)t~My7yGa~3r(I<4P>kpp|5AO-t>ZmK69$U11QH&E6m%a+JjkoI z31+(;I}QjExaNQ%{Imar(5JAcUfnYk!x<*%F8CCxt?7_}m-AaHH8AS5);x>T%B!3ouan6YR4@5eDLcAbY4X;}wB*!_a@EBK z=?rD!W_p*s1t)b<3~u#=nzLd7YSxj#b!p+gRc5O(4?+%=rF0V9Ho{linGJrAc}&v_ zbR#gzN3YCkZskd$CwG_^;|(_pazsvGA0(YW%a$zWuWo{=>$eLC58E~>h=Y6GfNv9q z{WmXK01A`p+!;LWOm8s!jmXs@84Y^Jb_bS$2*%B9Fe*^oRaOykAe1uj7v^jiTzeU;-X$2tn3=?y+4;3@JUQYuI6HygNHNuhsu5>|k{OKLPQz2m) zeh}Rb1UD0ZFH86Nn+i)bHTQzDpc2%;2557=O%i?zRCYoPmMj+Kp!m?=NA_Q;k@`R$ zZ~b0m-ls54*#z73$V&;}moYCJTOr3UHi;@M+akAYoF-?9b3K@g8fiBHZ4=JgMbDmM zM*pVvPlR@`Z3nLb0x{;7uT4;KE*iR-m7H||#&yMgXSRop8`u5VYG;e_*$vO}&iXk% zY&TCj7qMS%do^Rh>?Z}TB_Fip4m>p`z3Hp=N;J{)D#LK8jdf%}{4D{UsVaxLaiL%i5IF-k}H*F&fnK1HJ3^La>nxu^!(7E~!;3=AdjFhVLT3@ZP{FB!872*l|GXE*AGI2xZ~%iW79KoSEXtW} z+m~X^hn@i_S5=5J5ayES)+S}W`vhdZH(m4)ZYi;Hh!6J5X6}!9A;`}YRh=g$+|L}W zEc{)(OY%7>Oi7DM@*<6CYzfUf6&bWq$L=T>N1A@0DiwP`&r2?Io$PEsor5i%f8d3c zSZzR;*|%F8{tlfzCoaIuCdOapWK=|?ihqP4=BkaNH~_+ALOoU;amk8<@r-20-?$Cg$|nt# zKfYE6W-7TNY4QIO>pIR#*eN29%rKVpTIel!%OH zzg~1MxfD^4mwBXmqg<(rig9ce)pUK7TONC`=lOMtwgXjxez0m2BaCy#lkZK_rZD|< zF|Uo$pW~g$?TnjuhA;l4q-mk#J#vEK`>BWD*uD*XJ)jYxbY}Ua-}l&VW0muMU9X`h z%nV&`b0BICn=}9!#3ohauA+5^2aJ-#Tn>~(zLGEH*gE!!71MZN=|b1!s}MVscQDG6 zqkUti_4h-~0xB@Mph`6V4|rj}RLdt$XdoyFO;ONv*h* zuR=m`%sITFk7|SEPT=cuP0NA^2sKs@rY0Tkom1X6^!2I#kbT0^LH7N@RV?CZZN;XQ z|LN*{P^Hhl(wJ;A+W} zjBR0F&4Gb7X_RT*U6()Q>)1hnU8T?p3nff$vAs4H8q*+`98zb55?EYd4{NAwo4fM5 z5U3#DdFI+*_7YsW;WV9B47yI}wkg4l8^Z+Q==L%wRumq$u?B8|VS7to6C z%AaZOB{qm^@VDTbAL}p7vRdt?Ij@((sFk}ZVt1N{^F_~yoE_5V8~bMoEdO+pk6(o{ z!X_n2-yFSvjuL|=E`UUAGA68P>>8OL`XI6-Bx_E#pa|j_bIK?B9q*<>R0QLdbFSl-$tD_*vUX&7}cI%z+XquA5LM2 z^eqRvFe{~m&@1>b<{bz5&SVGFG}>kF4y5!2vls|Rs*L`of74HDuqQ)j#n||1L+uTI ztync}8ktY1X*v5eiOAP9Po1);7>&UTrqeVCuCPX9uYzV0LD%#9=KYWJ61;iuE$2?Y z>l?6w&O9R?zNIS;q8GS$;b#5!jRF@gbbBz^>^NON8I33@?B0C2&+D-U;d^d{`n$pa z5{TufdlBceGwp}xo^(C(Z);j}-5t&0_-_BdC9oJLsrUm?gO30li*QS8Ei5hL@7V(_ z4=WI&J`5|0UsKpu+Nd?t7Sc2YC}+2PaLaJrhp?CB^%$Q7NP3d!IF{k|I=FPl2|}u- z_%!^Cd3^BEmt^40qITGWE;aLk3Ni%qH!g1MM5LO{{=w@$_A>`V;7Tl0w60`M;9Co3 zxN?<@C6|&nQ4&PY>#}QdGU)tm|0JCA3OSf}){7{EzF7$;ij7cmiUT7S-0-KYe9YGl z=!tuWhcdy1U$T;=pq5FJZxa3vOWQFlK=*s_@af>*&cuLY?D$?tOWLJCaG*ttCOLQ? z0p(&tDP!9K3phyoCaOasEGYQM8{|ThzS+p4K}dl^plIt`;Fxu?FN&q~@%#g2*LJlY z7Sm_X2XP^&oRV1!qZ?`!J^drY1;B&mq`IY7qP3N=bMzF%MUohSmobe=#V%e#{D(Q$ zFtQM|fdg=Mmxn>d8hM_K>9-xf`q+u3BCD2>lnM!o&t$fer1kV^gWn8bjO}K=2k39j zuRbwrK?UQg;LNn(%*mEoDigbn?MUM!4n!Nz!u6m`83w#=DbE+o5 zsGtP`cryNgntb&#uNH%@e+55D_DwGt@Iu2zk>F);z4f-H5y~|h z*nvULs3dq0n$IY8=y_j+su0hk1@ca8;a+5*Wy=p*VAWfZvOBMsjF2OkVAPm?PFEoM zJ;41gjUE1qls6=g%qnBC+Nh~;Z{`QZ(+xlf&PuW>a)Gy`(U?#m<@4g}aXzTeE#sZ2 zfy$}L@&}vTY0H7ywT*j(l-N)b-A}nagyu!ucUt>lmd50)&ajP~7OBX?0_P)uH&qqf>Pjk#)mp<>*V6Tu$ zH?9Dw1ZXp~M2|~PTeh)_$cTz#ZkuDHu;Y67o&VktTZP5Dc!+!)k^#NtSYkOlTqail z>f}WElC+YKz1ZUcGqh-)t&~}F4Ox2cVIPPA&lbS>#lSy$Y<8rT_o{LICrXdv^j1o& z3T-uRm=B+e4vkPN5V>~2K~QhU^pxX}J#9U3z1twnJIr6(Ncz@^@HQg^A;6=_nIU6X z2s1}e^5R|3{rA3DQ-Gnv!sH8*bbP2loQHTx5$q7XA%rKW+NO{jeH>2_cxDn%kL~4e zyr16z|t(+o`(^kW7Qc3P-u<+5fIZ2E#G=@#@> z4e*(pzS1<#PkXRW7*QHN*BwmuFQ_cLj2>6$XM~`{*HcNfnnQA|ck_Xg;+@U}h~#2o zn9AzCjxK$(nylyMNH8Qv4oh$R1*kh4sZ%T>SdE0jV4b|@;T)jhdkL$8v0axvzYnVh zvjzqfMr=h1$Xy0?F*_9jAsMJyURK_ z^{$FjR8O;QRpYSI_JLlA>&|cOzV;Q;i`|CY!!Sv)563HDWfh}|ImB}u+QV9v8pUkP zIK(`Y_Zqoju1Ukw6os~!53HV|_F0gu$b00NijWX7%)^-$U&AXZw?M;#ZUE>2wZ_s4pV^eje z3in;o^VL<$Dmr;#cZ=ru4U4m*pfN*p&C*pTLiYqnh_dZkQ^t_l@B&Yp%s!?rt#jQj z&e_aohjzw19rV%bFFuoq8n?WNC836WYKQ{Fh7~T=)NW3zzvYG=Lt!t_MIV*pqc}sW zvlaz(rqTQlv~n$F9ygkko2G%(E$?FUw-}^o7B599fGAiIrW6}{*TZ}YTW5Ax*~R_B z(rot9@iyT`{Z;VB>AD$p{eEsR&?Dr?fmWK0KcIJ1@tXtk_%~gQIF%y#6WSYIL@83C z#Lb6ve0tI{JQbO8#oEd3!I#H_ws<+EzW7zL_-*pD=LijVj3vxhlH68YU583wt1^C- zYg*IVee()OJWhV9APFR!5LLRL!ElqnPI59>;TRig3ft5v(ldEiY?t~^=reIBwzMcD zYH)~~QP!*6UV~?c#Ix6rySvZ;k|nx5Ire6YN5Prt#1VU<0y+fI`Y0M!Iy|1hYKrg6 zRp_{`J>UU0r3ua#)o=<(%OGt$8Ue{%Ua0NgYSdD8MmK4nZ zIE?$LK-h2_H_?BVBAgp-l2i+f=wP4X+MskG?^g2NTDEMfQGtMN5-renC8jpUrgVW$P=sYVN(tyR|9KV+}K*Ch$SooA^hYNyvamY^n&i# z=}3U_oCwW{@3T1fp2jlE-AE!%eI4+{LVSi}nsk@Nm6XkP9;o2nM1Dzs;CrAlS+M*o zhl_;pOY${43om`|t;1}_p0D;ap;EEy&OqJE`&oZTb(iO!8gd>Y-|KbBaD!(zfDPy- z+pMfCpB0u5q34RV*GHK#lfHkGv)94D!d9-Q^hkL9lrgd`*3&*0o)MGep<-|K`L}F6 zs5&^lSYO~NO%x8xqAA`;3Iov=15Wy#WDYaHoToQAhX6>QPyy&GbKZ6N1DpqNB|dj) z87auBd&wH#3#R$0@6HsiV^PvXfyVo5h0ceru|fy_pEtkg#La!sP-x-ND-NEwt(l?Y z-qRYXHyv6g%6w9ggQ8Ndi`0C}03moJB$*@Eh6y`P3*GnJIcA0@#7Ssqs~y^;xAz+! zIko-MYm4MBx#@wA`*ECn<`x9E_{nxQ@E}ha-ZR6GqmJ0gIynU-g|Z2qGx?g4h66k! zB+)83k@*#y#MabRvEfMdtQ}*rvHksvqna9V*r?nKo#3;lp8H-|2`^FQV_<*+VYh)T z^{Dp^C4$ejv6aI%e2}&C*!qQvbs6e8goF9azyMXE@o$Lq;S9G>iYAvC%=Kx~;*~Vy zG~WZ9Lg3l_Uo4pC9Bt13{Wso^Gkwgf+hh-Zidkavty)hutRVS?1l?FI?`RyPgy z)b+0%2UafR_}@nRpk$PFOf48%YQi$QGIe6NbFf4~Ea(T(|Jdw&ephOzVo(cRg9W0U zoF2a}>}iSC?)7@oqS-DG51JJxU!GHVY;;m{B}-&QOB#xzP?>A{d6H|q73!Ru% zE!3E-8qV$9Oo~Vj_ZJDMIJw)bw}%1`GI#K_X605^Sekjy!bvEAY}_3PQX6F+g(80t zJ_O~Qo}QM4yPpz5g}Cz{K&I{%(|TnLISL4?D)?hdY}`lakKn zK@JW*DO(Ya$SPzk11E27U_Ya9b~hXmG2TAT*N(%bJ$@z?(ZL`Rk>4nq)l@w_SE ztG01t?|zmU1B{yRlQBhrfD2IHm)XAR(nDHZuP&Xa1>{3XEwS&Rr1c#HDzH>@=oAGRz?{y$6J7g$^-W80H)OjwBSnfZ{et?QNYAkbcSkQ$ z#?7=(?;C!c`MP<(;;#Jb;<-WsFG>8JPLUogw31zc?V?c_T=P-a>nff)<< z77kd9neA{e5!uvoknt4~7LoV*!VlnqI7!5L8h3`A4liUGlnt|J4fz|(XQmhHkct^w zkPrPOZF@v0d52oteK}1M4+S(x;wjb$&!XfD3m2mwl{}1O{VyZPN4DIUafG=~!+qZd z>8Pevqp%HnlXkQAZk7}#m0^HWio=00#`4hV1X3Q5;7w>L>be6FA8qI(rJR(?s!_4f zX7XJ*)$!=+3{+RHT7)rfCM&d#fZ#BL#$2{_8g`J&s*aMIuIj==SH~Y-?QWh%ccw zk`>0TKA^IXo-#bpp^F5O_|2-plVSQ;Iy1zr#-2h%eN$1~hGaHMF%aXg9Sd^{LHL{p zK3|S(F5XL-duKo$YupMhS!7cH*~P=8Z$+k9Q4ni}v>*Jh?Wg$TXywDCCcA|M?$ab) ze~G9T|2j_*pEvXJw|-=4m$FWcZPiY*#0GM%x3j`;U0}jL!Cw2h8T>h*bnrz?kDeYy z6oqpLj^TygrSqS=s^mlG7b*iM43dF})6_Z53^FnI9-!21hFe^tH>vY5G`UpgSKo(d z$zMreYX()@hhkYY3r*{wSy=%|`)cm9mh9fWc1ab8xMIIWsRq9wMxc+Ry0AbMpT>bA zS5^qql-{5~ARS-H0BFb)*>g8l?olV&X)t}jx+j4V)O>}Rn%EfD6A9xbDLvZ%xXMTg zMPqS_lyg2mP${1Z-@tw=>vlW@dOwf*yS3yL-5qZM;ZS`;>P-vO^Ba(Zo=dRP)y=8` z^(CG4Z@tO?DMRz-7CNb9)A`PI1UJ!bT#Mgxxo z9@xXX6Y2kglDfixY3)aog+s%6O?=DB}u~0c@px@zgj3uBGG^3~^ zyPjktlXjS|iDP1_XY1pgLO@&|w13?(w}lMx=P-PR5;!Gv1{EYY2d5YVQB;l{X@j0M zlBn`rhkvR03=3EoWJR0s$@75ZOCsJ4krPQ z?T!<04TVA?T{82D@^ls)cxt<(IxKCJ4=u=UA0*m?-LHhl#&I3mPWz+8sXoU>E;0fg zVE|I;(2yZfN(DY(0wlhFlz?J=8sUQg6BPMhL-)SgE>+X$Z>A-p;bb7@2in(tpZugV zb~P-7cU&IV_GnSueupqdpH$E*W22UjiN-0Knj8)Djo+`&w_0OZdh@V}n_>5SnOWt3 z6(Ke{N}i-!sGZ7uS7V`hqIXIQBYGB zXRtubUP(nh@9nb_C;U9t#~}?_Vi$9@I7ZrGi#9@-WirP%x?Uo2(XO&_#Z9E`^~*QJ zd&%i^R}8?+`H~oi=1TqOI8ELl+h*mB(6>Urb}u_{hPwv%>61|JeAMD60{?Lltw#{1 z>8MS1ANJB7n#J_lc|eM9W12*ewfd-ZpW^bPV-u}xiBFhc*g65l(I^(4)tAp5ci{FV z{YPWBIo~2^Fs2yvV3fMV2chu(%Jar9agxK@SQVe0Km=F(569cJ_fUBkV9_JmcyMrp z^ZMRQmv!X}4|jFdFGp8+!*RLOe6@ok3_NoQ;+FJSbQQF+3Ugv(m(H*6Imu*;ahulR z_vyVf3zq;Kmh(@YyywXu;WoyK%xvI}oP3eu(?v?g&Q~mBAwi*%1 z5ue0Q?j^Al3OM9qwS#9S`2uyl5M+>b!Q*H}8}p+GTD1Q11XT=nZSqdrJADNCsE>G8 zRN{}k%wS^?1gO>6u&&J>{xOiH({c2G*DHH8VH1dLJ=UoYzAPbiyTr!7?HpM)I+5*o{n!{DnSrqZ*DLE)Bbr}jbFIywokTfcD& z({{#a%B9%BanDa{mIP)chrb@b28Xgf`gxVD87(JbBNgmqpM1g$dRleJ8sT~Y7Gr%g zQdz^@W!$)x>H&o@MV{9+#NIEBC_Cnyeh{VWhuW2#Qcnh|%1&$+Z9_2s-NTW$cKqo; zE=+{nQb%6!9g1b$>2pXj>Px5~1)4iaA0oCYQ&-hKL%TmI%*bqZx8-hKU-0Q}gyLUX zxt*`-!hzgviICvIi>DxTUz&E{j^ccKx&~`%YGZ9jipK9ifG@YN2#XPNw4W^E5(R*G z&bUwIl^NB4JrX`XZmNAvsr>S{Qs%3p$oUF+frWTk@&SdW$H>(TD_GQzdhYHY_t*aW zbDasOnr{XKvm%(P0;MD09b$p&&x`&z(BAZPm*Y843N zI~iYEn2xY=aR_GkIjJLF;8=KvKWK1N8IX9PF#DEX?I6r0ZAdK(x*uM?6{AzPd5@qb z>Z1>sJ71EwT$S4^}mF-Q%+WnPT2Un{No0()YPwR6JKZ_ zE9eT&Jr8qMaEb)Z!P@lWN8{N=F_v11U!~{xCg8Jus7BfYk(7a#rZIiSMlpTQS6Y@E zI97rRmz>3w@8_}@9!($So>YgGIzq^!tLhwpTQWB3H1)UA8GH|lTKb2%YT%mA8^}yy zS*MAfkx@DRm&!lKuOawd)BNNZYq49IFA*4*c`%rsrVwH;ITnX{@+tr2|MQ~oUqW#qRZVCi1 z+r#oi8JA1@I4@&Hk%EL5^?|v+!!qvq_K_o93~RMkecHvAWHO*wbO&dQwik*n<%qauov7ocQR2$kUdkC2tJu++>sU6f}N2bes6vE zQYDKCYIlqOJH*`r+>>ZFrM>SXHna%L)E4U)9e53HP^$A?0SPC%$ZlZqSn=OmOZ)NYS@ zmE`}^)LU}Z{kf-m(lGV;EW;@)GRR$fT$lQ90|C9jB6Z5UnOfAXmJLU$)L<75G(va$ zg2;pRKq!Je`Bt)Y(=ELpw}gqF8AmYtg}pd0!A^(;My10WkL~DA!ng=!(fPgq7K2b@ zuRhyuVVSMRE}g>$2{KV$I}B6$yAW7RGr^3ouL#u5pR+a?CS9cuy$>5d^dh0}mTF_9 z8;6=J<+6`?>KDsv9%xJM{hr>`b^sYo-8PplPFU*p&r{ z#Ju#w-+Mmz*MGUs{2>i7^4gZ}{!4|kDHr-azDF|(EJV4cxvZI^7{?1*wHyf=_j<6Y z(6S}^)@t_Kuu=E8xHWTaH*~Z?c`GI^j;lZvh+>N#bOm=QprfPULX<<7Uo2oIS7?fM z7D$oWe2O!5z!NjnsPLnNA>W->|?yUTjvXwKdhs;eeXL%5|u=5ac# zdOJ5qI4?kOg(h|;66yZc#o+6i=Uq{c^b$4-lqrIJ*({-UIdCf22&RVosk)&l85Seg zkJ14YJ*Ry`vvZ$qVb@K#%W?wUjx}RCeYs@-q| zlbHuLi;~;?y+mKbZ3N?C_Pk-E$Hxx}J@|rq&dh42XF4M9y?FRVFbabNs~xeefXv z=c`qKtuAu~nwXx~I>3q=mU7t!G||A?sEv7(vy--7){-N?AI*XHK+(d`ERBD8F^Y&Q zSuD3h3)Xm(7eNHidlq}CXLAsKgV`SQ`=MQ63J)nUT_gO&W8(-$0Im97MdVMWP7On!?$0FH5?{qb13pT4Dq%wV#TWbJp$Kn zFD?qh6Iv`he=$bC8@sQ@@79tErg}QPTtka3jh@0Uv?M#`eF@&fn<=1TVfZ_kcp~$G z-8^clW$wYpjqlH2s!o>Gtl^cFoJirPVEP(gZx>Wt1T!*EeH6Amc1=>DUSM2 zFx!zzk;KsQ&mq-523MUAP}%SXk=F9HY6UQ{nw)_yu#_2L< zJuU>6i=5tb6n|?(oTG=r*30 zcgIdSI?13-LNaK6`{&ZieESK!D zmQW5u7#$9r_cV0r4ypqjek zh85gR)BZ}_)dyuF{>5^&=LXI{B(CtVKvUo#qo%+Bysn6~|wN z1SArWLKCLcbikN0ebMZCOn{fA3E476SjsXexz3Z_UKJZF`ZWM$M8#qL`&buz=32rk zH_QVRIt=-Jxg_V+X;?#pp6@zx&p`QMy3}k+l4sHX13Ey(zZ-j;&AAcAnM|B}0vb*`CKee_T!}V6Df<8i zfB2s>zusnz#67dp5OE5Hu)ri-N$+2}P?%A&TP>$y1>Z1UMJBJAT%hBpo^V=U)>6Pe z-hI@pUPbIOLKoH-3;zhUC-wKtILziB`z}KNo@;oBttQDX`ZH7r)m~PQk)rg>Al?{A z=n$2X7g&g!zPlGgY};RO2T@9TLab3=ym~Pv>w7=Yu4CZqqh|xisc_Wr8&yov#uoy4 z`{!NTT2vg@n!>w;PxUA=c)3tMkXasn{t-Ep;Z`&7ejODRWhAa?HyA~bjg+4|!lYPT z5pP2!HxLhaps68eNJHlqFi7URHku|8ohlM22mpxsN5r&JR-L#3aWDS=`G=qb(QV$= z?RkEC9s55Mw$(G%_Bijc{}p|#(LPIv3w9_?bqH4SvycvI{5TB&GBc0jI&83&-{d0S z$z!o_A59>AyHTy6aYrmm1wbs=ePn@$^`-zCH8uhjW^gWQQ@$xUU8^W^_g5yqA-JQ& z$lO6*@BT%xZFZ+x1F6QB(&kEfTH}?Fh6V2?X`6d|WHW2NA1>{YS}Gn=REAcKCAw4m<(45O(@5 zi;xbcx@Qw8&_(-d8hjwhK_gJ{sk#(ef7gUxZ~ev1rh_o9I8)vK z=Ls%EY|xqo*UOSUNt!irXEJAPqqsvXK6F6C@BQHLfB_c{G)}cog~4mAbE;`AM1+*y zdr$hf8F%Ckj1HFYWLvnS01jemS`U8eVAMOVJCEqT3AI5L4qJodR_728N2~=}49te4 zUb}dszQb!3v^9!3sYAH#gz(CS*uEwE)ppeR&Lp9JWBIJx2hLrhOP5exVPW9w#iF_D z5zBxGt|C!#JIc^=3D(6FWobk@Si|2DVl@HY76*{ZgpmE$zu~dntD9gE?`#UACR@9k z|Lbs|Y{RC5$VNTd);$+gy2M;VIOxqochom6kV^QdWju3U3I{8q8q6j3c(zp3e~f?G zN&%tKyA;MBW6w+x=tc}$M=kOa&DwjX$-VJ-N%WJOZyvHnPPX3mZ2=+93L8bDtybc?3|Kf@?W8sfD#TW;<7 zaX_xJCz4eBZis;hQ+(#5&D~i9QO|UI?U@zc z1a4A;wQaT_l$VeTFd+Q4`0&ubtAtfANt9(qbs}<-^XLeV3J=#sJPV^g;ctEV|9N3_ zd+u+n;NOD|y=!b)$0m5FpDd>|(d^gVFB=Q%e$6y0rp1k$`5Dx-0VNm;jW5alFZy>W zG9tn~{@;8!s|aiI`VEKft7WB%#}f=%@G5!in4K^!7fvBlW5j`LTx{^6R!orbr)@hK zs?m#;-XNG(<=pIU>anr!@d2TPm-3^zMXPU@JI)##B6L5`tG@`qGNFDBe1oIe0}gV9 zL3G4;T#POnw~{?>FGU0AleJ0_`OXsAG({A4A>^slK*$IGipK_YH`!sugtxBuV-+@1 zZ#6o*6uY}5Q!($(L9t5vwS*>yaEL@{TQb>Nq>DMDVYOyk2+p}#{p6Ye`loKwkGE{8 zDaJg}P_RmkdCRR(fudT$pb&kRmqo?5%{71E)V?b7R4}1etJ?m(&PtY^@IrACTn#rF zI0-$$+FI>AEb|L#gub)oRb9Q9FncW;ND~2@dSJRkT>tOo<-l;O!!R}7lH<=|N1zn`D@kC*mgF>nJA#G0?* z1fdZ1u@dwg?K(;$HNiYL{T6n@>ry|6IC3}TWEnyrX(Sf#%pFxf_ z@}pdZF^q8DPD9XVjyFgAb&WdQ5DX9Ir`!*B>hw$&NH8cI0Hz>n|6D)F3CHWwtg20z zpw^#>2iCDNx~?Td6_5m|g^0JA4_g+kWs5utn3wl>-lAaFc!I9K%1E>pyh?%$Cv?#q z#X&lJvDhfBAScIRx!!W`h?s8`k#0*YZHQ)kB6;-OLpvEeR;Z!TQ6z!AQ0&?Q4L2o6 z-_-%n%I@x?P zrGVt_+gsbm&IG{vXeuG$qR&=u{B*#++Pf3fj0>PG##?Nm*vK|(D%)Q&k5zK+Y8M#} z=(rCwaff8$;%RFAs31RQ5)PpHRqssr3WxqSus5HaBZsZz0kz&^?yFJ_Lz1#ZxI@q# zU^8^#?m}&wLNTi}^u%%>!UW4*#EmJ2H(Ge8C`Lcjae88B>A0ivh3D%Z@2 zmaQti9QOq9G8+DG$}&+>H%LB)Fc=G9 z(Cf78tx*5n3GwqiN=ciK0Uj8dbq3z*%Q-8k(voO;}GOnvVcZ2PEE z=XNBX$;tH^b<{h`3`>pLhy2(Yb-*$$l}ZS_KEGRxHy1e-pZ49HU8zNH=a!a4jK9nP)3;S= zU80ZHcR&N0so*%L01x-Kk$|Y`Yz%j!n%|rz-)J5mARgBMbok_P{5@QI@U1jIn&Ium z__V8Vk&YL^hvWs9n3jOF34C(?sI&UxQ!fzIqX^40=i56#DfJ4+yxjR`@;_`5^L!}G zFKQRFCsvXHwzn6p>W4Wd<3gi?`>KF*K_SQ0^%p`=R+*1~`S;Pjv%Dx@PIdo3w zryGh(c>5wCpB|oJ*QYEi1qNj<4&JT1)O2qidPv$5Qz$##nB=0P!y&9N*Bj=x?!&wG z4OV4lM5)(`lVOZTO_gf$k2TI)H57x-aVFqt-`SDdeY@nj^d_kQEq%en5;Ors5~Ml{ z%;DxJ5MS0P&L?q?T6Vh-7<_s~NH=w<~zUhe@^QFf}QU%D+VP}V8_ z1ATMcq?HL^F-D+-!*>Wy142y}C?ZnFMw2N!D%0VCl2^5wR*ovLGhL30g4>BC{*YZ|lK2l~M_STG8U>=xDeCcX<R>T9Y)nK4L+#-fj>xPiG}1QK=Xl+Bt51_X z;FwX2k1d`zBzGpS_?%bWkf33ZQMuzW=AQYhq5KeOUH)!+L>geL>D70h9urkx(}`WXwgh`s`ius4S=x7WtNghNf&p zez(T19~X`2eUm9`Ksdae!ERb(tqs^FBo0S4TUiT0pCY4dO}lxcTL@LuWu`iAx(@4+ zUC)ZG5ZpcnHw!_GCxzYKu_KTP8G(@O4kH#8>t6Xo&Cbp-E$u1a5c{lI(A1>W(>dsS zHN?!eWq>N?c^Q`OAd`s|@Tau&G-)mbQ;C5DpV-0vXv#p{weF09$aGwLCwL{wglRCn z=P{nfOyqfaW-x$nGOceYD2GA$6xAjZy%Yi-b#9#qp(;UHe1Upfk{BsrFg1`BOs}~b zoKbypIw-o^usQNN=%{x&Wr0p@yN(QvZT<8^HDj!HzyzMnvVnQ>EeJRkj%~{bn8d{t z@W9te|NA;rs(^S_o2p54c&H>J zv*UmL9Os^=RY7(@y*jE{vP2+Z9}h>yKNW)N-a}v<%(4Dt*3{IsveuCH>(*puO7~dT zA%}H8|6w!NratgFwht|qs{J((M|c~gnI}qfESAGzoEXvLK*q$(+J%u4I7M`>*D8Be}K!NThQvrTmUc#SfnQzlLoWSkt^0> zN=2$_$}5R1_ab`V6#qJH*+h=)Ia~5`;__$Y*t5Mt2Ar0d_o;te8Q-Wd19S_|cbNi* z>6^TG#?m3oce^Xdd=8%9(%WIQmT?aBNA8`38U{*qFgmrm5Of^{|(F>QmWMa z9Ph>~Xoj+iM2dywjoG^jIA+{-8veF^%}@@yAmLp@6rRIv@#(yvM)5+~7RG(Zb9pjTG!Wle!a{($4Y5tJ%jGvS zIL*3vJvYvrzbhg*(1(t*5jwQjL5YAitp5JqlS$J^r<`-g&G5tcE+QOwG5pova=$@l z=uG4F`FUKPpFL6s4HR~c(Gq}1&5kWTV~DA-#f6~M#&qqaX&s7toGclx6Erh?Kw=_H zfCXFg6P<_e#;}a(|CH9nqv9|*dAZv>_k#SI{H+|}Zgw&(axbrPtB*_(jJlO@{!>$B zJA_|5B6%!`-eHpLkxH}CmYvr67)elKN&))RjdpCYP=qtUU9!&y4nlyh4nUt@I#+`k z7?-y**uf3;Ls?GJ&IG`ZBdH&_y^Z4AuyG|#(JEg~<$wgf>*y*)1Uh%}cq#Ez?II}~ zY)XfWLw`21#*BZ((G5;;O|>)`6{XZshvgBmIvINwffNZ-)vO7t1B`F*QhLbHHzw@9OG6$;}!~9T0h^e zXbE?+9sxvWUFRF@X*ag23$j&P8~^|V0I3+R2ZtKZ2)S~|Me#5Gy+{C<90YURKw`dA zi|bVK0h1=`)%OA9JVwZ6lI?Ps#yv+$;g!!WJZSud`Z0y&Y$fjpP$zpZy{~=qb~?D5 zI%!dL1ar$0dNl;|!N<8&1{*v8ibmDt!@95$jYam(Y(6oJ8jDyq<>;9Jq`$9PUvBSt zydOVpWl~OKNW78Ltrnq>izQQ-wGoy3J!TnD)ouRhW_hoPJH*I{Ilba|*KHLh>m9%L z0l#&!^oo|elR{!CiQBWd=lG@+m9O{r_7|Jj39}1jO~B&9M25~npZgU0L*|T-kB{zp zHu`0VEA;AOo)}uu#26lR0SenB8*qhy$+13AdTnj^NiX5*yBWEdiGrgSUSdYn1}bXC zcOYm$D5Yv_w@)!(2_cA6vjBunl%Ky_;8DLxg_@%Ux1VA}cr6&T^^(X}q z8RKC^>VpyZaQ3+jabx6BL*wR0wcs@zeK3JlmLQzu@_6jo9ddE~+z#7iAPi8|(DIm0 ziIVI(Elu(BSpnk3sBlu)@m#c`d6ZExEcx50rgJv6DTO+}ssyjzl-qD(b@~oBvNUZ` z3KCJhDxfTb)BRC8l+a6~tS&ck1fqaDQ(@|%HW4;x0)W7r5i^#R2o065+n(6iQH7+2 zo?-KRv0+|1F@>_7B+@tWnO-~34xd$)H`oyyyb-5Bk$?#6mtNS~NY#s8^-~L&g`B4_ zM0wf&$Z8=tScazd)j8tgp z_HBrFoLrsV-^mcF)Kz$T6Oo@uuH4cw!sKwjk*{L}HtZ!W3YD*Ix6NG~ zaIT3_G)H$=`OR3w&csl|dic4Yj0;f|R4>Vzo$RStQc=rSpW3pO#EW%Ps0Y9v%GnWL z++Q!>x0}qf5YyNnU*015SEfs*!^RNXX7gKzi0$RfYy@w~2wT{R!vZ+Ce;z&w`KL8i zG^YO>mqP}Gzag+=5v{Fwvd*LKc(kr07!o4vYauzk)>T9&P4_=;B*qb94;$A z48c_pg{6sNd>$EP`tDHx;Sua8RuB58=R1jDK#W$vNWuI@gJfyzGJxO&B@r{Tw`I)b zQ;#*h-=O6KDbMq>QHH3zeAI+DUn*146;A?rKciqh{2%qDAOt ztdZ8Vr86n3W2YNz3TZs(Mp@Z{`LqxuQ~CXaUu)ngwQZTxL+fSFhPtt0zm?>5RqN6+ z_9HY|;*r_tg8tDmd0r>rhWEpsm`c8g<;_Q1ALoNjBpm^3jeI(9LdAjY@)rya>A25r z^#k97toGPgxE^~9GMi$<5%+ZR>X7qoY;AEv3k`xN`=S>N=V{`_d>*P6HJ+^3hTjb% z5H>8OH^m2jbMR*x#5qcZrl9BTNQQQ<=#%kiZwnQnmK|uwLxWN{nxg|8f~c-^@0#I5 z5vIsJ*Kw&xMq~L5*VOVOEMS8e+j~m~sLtNqnC#Wb(eDh3FMbib;ckb&v~sAtvdVyi z=DPuT{1Sf*f2XGdXTuFvF(7OGyEiMpRI(x*(|G9&h8RvtpLrWd(WD2a8oR zR=>&Mt?U-vxFO<}+Jk?1QFFkHlq#SFll-gl6bH;r4$K1omQF)WRgXA`*2*o*zf0sq z{$YiMtEQ|TpMm$f8-8J?Qj^7(+g1Hf9wxCJV4&_rtP!l3>4D5F~r0-VvjZ zfbQ?_4B?S5CsI`yEbCBvx4A9^<&8o*)vmTxTG#K}6o^Y&8)3&i`c)QcD3}9memwNg zTD&zY%fCs#Do+T=9Ww<+m(^?xqDHNm@)xePBSdM&zmcUU%5-WL(KHZydIbrqBpiq0 z%QNzM?)ruju)%3E(k?br)S8XpHJ4>}MU>z1T(Mt|IaF&n@jKNbt}Lr{u<1Hw@Yb+& z;>ZUJ6Z@TP!WXuLn0z)`MiMgWw;J`|gDKn)ZH|2eYS@d*G{j;7#Ntd16`_G!v&U~E zVsM#>7IIu9CpoHD=jybVI>=UoY=|LzJg|nOg-jo}CS{|18{iD5#$VAfjMuGtcBDslE6B(W*{+t6MWY#*{8M+oisoHFq@!P+%V z8*OK~*pZX#1$C8xId2cmr|suL6kCBKn6QnJ+7_rXajRKTVmyndYa-5#&IoXm`(wr{ z8Y{(`FkJBJVpjT1X>J&NZXVw-%dj?_@Nl#dyw>kxO-eo)q=B!URPTf-Z-=VXWf)Qy ztBvSK?O2wi8L&he@vzH|Qt}z9KjOW*Wb{SL27T2OPmWxYKSk=ts}&xXua zRFMD^dj360mc+VO7qkp7tj7*%@Obcv^8r*|-UdhB`apdh^%B8Zd|=A^xA79j0}!JC zZ+ul0;dAyE&^XJ|B(Vv6~1ZMp8g62W_U zk4OjJSAz8YD81}1Kr)=GIjxnB!;!Fm+B1yA+H`SK*hr0GrK#wnD4dA&EAm39Vaz$u zUE3B$Q$ed=ptJ?d6#h1e0!|>OqQYd4y4+GJ_wXNvTXP@Xy>sm!Xq^FPZvaAvK}!p| z=xc2}Cmbc#pjD{ACgZekRk<8WR+8=z6PU1)>159R1Ek-3Vo0*tBmxL#T#TCpxs^)1rU!- zQ12Lg0)2V}K0N$NXtl-|rqEV;dsncfsRpIt|iPqn6R)5t~AF@aFEhEkq;Qg<)etb z;CV4&?8A}@ezSl7@!Kd#aB=-TR>m&0!Kvpj+UPXV*l`&M2n`BI8YOJRjZ_AbD-$1Y z6;iyt<6ck=pBwyTQCy>VQn{pB zNwXJTpu?n$;c={+vuj(mUf-1F#Q>=HmdQ_v7;;-X)6cAQ8J|ZaJz0}Nv;Sh+C0}hoQ48L1J?bcYs8)2bO zFJwMDJkBm{4moXD(lsS^%br&%0C8JJbAA_)0836hrA<`U4Qn-&Ug{#FN+*+9r8__X z#)!fV+3^6R;_kNP2?(&;VAqvwbqsi*07m48ZMf(i5#!15qA;Yfdumr4t%-mT0h}Q? zk1sbTh=HlKsq2W$?*iBMmCXS&piAVC*Czy>t81)rCZIx#{HE=|jiu}_fG{PLT3O+n z3A)S%)kT!*NRZVmvIAM76G<%BDcAr1^=pAEb?+yR4{6conuyl(Rt850V1W%K=bLGA zb_Hk2!$}4anKQIQCh)wS;3)-;u4Q%MhM|{$z#=^OK9F)c@tYLnOnDVRMpH6OgP@>iZCv*Ly)W3PsLfDow93`lc>BQe3lQv#F zYU{i$CmC6{D3YpT?EWK!v9}Z{AlEREHHnq!ys@mm161H{p<)AC-A(LNVecl$q9Y=s zXSKJ%6ixH`&I^Sl($x^r30pj}dXsmFR}MWTQik~(2&S|iNfa=u_k8A!FsM$)D=cYoZSl(^vx^vgm5pIDSreOs0=wkT1@zbT&^-~0FR5zJisHh{{R&UF_ zC;5wNN=6rxs+#REr#I@V+dG&5NM{vixIBACOFkuea7$GA4 zv}eoZCZ($A#OONw0B%h5)bIYKh_e&{B&TO*h(s}l!s#&T8U?V6=B)sJSm0?ErX`b% zmawlIs@+NzpOt9){j2}<2$s(_-bU2J8hGTD#x|%HYJPhpme&wRNN7Dj@C2v7O5JPm>wcDl$ zR~hn4+bgf05DE9-;Zs=9EbcX8sHmxqPNFkf0feJ(u{ga_!PM&cxH>^1*c#ArZz1@; z@7idi3aqYJB>+3IKvLrW5bB>>mEjgT$KC&~O?$&xj906J9va!P0&I3alAQ=~>@Emyv5gKH&ny`=7XBw@LZc;Ho3I7z`5J z4UgKR@J@T66N5RZNHa}mZCzo$XLa7WKge7T@qaSowNmH!v)Zsbbmu=00ak4=0Jnkh z!FNS1{VfnDT3k{sgf{N_m6W$m7U_EZ zYKydV8M{o=)AnJ8W#&E<@nAlt?NBB2jYOfO?dx2}D z9p}dRpuzp{TI;L98J&p1)~1QgC~6rI`NF8JXPcz+9w)a#aL$3ikXDlEpy!Zn$5j3( z!2L%zr=NA7)Aq)s3FO|xrtpf~x!88$0yZ#2Fdf%*T|UT&eMyvCfVYH_RChmR|^TG#EMeS+XP7 z=sf|jH4kOnDQ?GKP(5Xk2cX)_onX%@oggbm6ofFsNu5XD2`Zj{2qE`-jcprjj@t7tGd>5ivqKo#d_7mU<~4<7Z-h`&yzbZ>*Say;9jOV9 zJ}nReyc7s$0zy$2_c6$0pcPc=ff1$JCp=OoqZzs$__+!ycGe{@pVXZC?!7UvoZi%K zEuov|HtDdfwF4;2Zz#DO%fB`?1SDgZGuuLXNPObCjKQj2?$+*-oo2On+^O`23#G3) zuHqZ8Yh8MF0s$Qd6v`k5ZnaXgw<@9HfZ%EC-qX`{BC!`%RDYTQ({o~Mq=~F}Y(%(s z55n!;=KBXFvs4~wPp|QTO7{}%g)ByKFT5Rcgf~xSEm4ZLKm>@5+VfLjprIb|b$6rz zrT`DKB}=+v-V4;H<|i=oxTzI(Wrm3%#H>_6L*rkR=h)FnIVaw!4O#Ds703@{=9QU! z3NexgWR9%R*8=`9Fpf0j1Gw)%w5R`LEv>v&JAvP!_m^{5kFAkn2bp<#y3&V1mSuN9 zlWlrTdv?#h1bEcJ+K#8sXB9BDsX^+rvl4luFeBh>`fL!%Iq!t^i$B%q+?~9)xXPmP zvu-UxkUK`M*h;6Am8jZU>pD*` zIfuRF7t!ne0C1FScZL3Bn7*^s15I!a#g70FaPH$n&@oO3%(N@)}hGh+vU? ztD1wTJ!EP1543e+1c?sl$+{jYU^{3XwR4oSr95ULkMsE(e13j1RVv_?a_g(8mGdg# zh7Aq~c+*r&v`m=LexBtSvZnvab9uZyRA+`qw>lQ=ha^q=Z0 z93?w9QAVmv6lfjVsTO?Wyk$g~PJ(?~uury-w?b2xxh`7UTkP38DhTZKUnK_Lw_Lzf zjwwzmv!*==*5aR6#{tzw=iR_fa=h&mh0riE}u%u)_jd+ z#xdL9GMpx*w!sd*-Bf7HxF8SuyG_$UI2g+HHDbrsi*)Tavjp&Zpex9k;4Imlse6BF zWy&w>RD{72P4X_Ht>frcEn^Q%P&Ih zc}1YBS$SNber82zVG9;KK>*Q-Nuam3&h3Oz)5jyLTQ^>UT9CT$0dj&^4d!XtGXOp6 zKNo-nCshz}GWhiY<*QI+1NVju&uLf!&x4$l4Sh%T!V#rt1hEhc-8BAYQnbggj$rX| zCV(ILzn*KHa~IkU7UH9y@&7`-lCV3KKbq_w*I=7Wo_9iNz;bmpSsOy@HqO4*OW#94n`QiT zgYjM!J#FK5*jw;y(!nYMZ~B-*R_`4VX+2Y^NEd3AGa{e&wWZ~A1F2S8z^0ur;^O+kr~kxp8- z{*fIBa(ytufW zxsIZ0yyfqddV>dj{&DIoQnDuJ9HLcwc)gzeT6u}dVD(0`P-PW9OCTHn1K{dnPATOP zNdF@bbWLo3hg(FYg+O&^LCbi8@%_P=zqFw#9)@sD&MtP$zT~_kLLf!mLIwLnDqZ$e znB3Li8vUu?#*o(Jf69Gm`raUrokiR1EqMHkF8$;@FUxv_ld4Pnq9Bn|pDB4Qg#d29 zhH6S9lYZ*)&kJsURm_~ox5%@b>GHT28_R7v86+%dsu5Ry-goL?JP8?cq6OwSDR5Vc z+au90zJpCLZH&=U;$ftjlwJ#gMBQ_?#g8IFczF5rMqr`<@0iXg5%X@$WV#{Zn%^;??cZ0t31otXfEGt#)S{Hkq=3Ir3c2z;b zTNmOH&za=66U0avyp8@1dZ5bB{>0OoA_o#Y8R|l>HEx%cqz5V|5b)mB91ykgv&4`e;@fXS}`>69&8?K3A3e%h}L_ch?2R z_vHl^urBA)1Tvr#ker7(zR=3swE$>c^%}0xQ`7rs3OY+U zByML|9&JCkjJ>}rrq0edEDZ8GU(UvlKVf+80J2gIyUOVW)(XZ~K9QFcRsAgf^O5C^as>QLmg>pRuP3U=!MJ>2 zw^6U}PZj9|uBV?LTO5rccy-yIoN{f$tlICvxPiqB^q#2su^*5^9aAX3(A*_ao11xh z2TK$6p?Tlcc5a3tAf!Wo(BpdHv}D%_=|IRiQD9tJvYMl#zHMvt#C?&B^n?w-C;Eyaa3TShCnOk)`7`aWGGTG~cVATZ*U<+!u^)1mE}m?!sJBm0 zngqUtFCv96RLdg`eGW4l{;U+yWEE}1_2?oelAIYiKkX*r(0ugO3DfR)wT3sys1+xQ zXGK#_hpQC5e;|qi-|c_@=ZHsWT(I zh1;xe8B>Y=M(vZ90Ka#R>W&)xaLwR|TM5fAHHAho>Wmuu)X;!I9(7(Gijl%9^7CbZ9X+}H^6EtiYm_FTk0pck4Q3cH_Qf!NjW=@hikVKda(4BpRQ9wY;1Lbu zY+xBJ0Znb*8@0ihSBSNj^o`URZbX7)V{67Hz>`^dNpO!EfY(xr9vo7_a7;9I>aroi z^_*+wAr{)Q_?|ol#uL~RvQ#W4C!saF;|;h?_a6)TGnh_>!S!mi^hX<*u7j=Rj|_gD zpVTu8&C9=YpqN{^Wgj`Ve}l;sR%Cld|Iq)CJEAK_D`S@eZBto>$915#?*|4NAnPxA zg$;hE+XJPQ$Tcy&RA# zC9-Xtta6Fb7^sH70sSmnh%jO6JFho5f&OS<=gaoTHHr?ELu$wr#&{V-d7W+V{!(t>m&vBvrCryr9`HyxV>)luIe$9KSX_ zbZE34^OBNgAvzxU4Y;B&a099;VJz)*xn_w0ou0X&dQx%;OB&jkzMJ8G*R`v@e8dH{^XDGL#$Du7njA;4durVUiPzH z=EOxKtmYU=QYg*3E8~ky`=UYQBII4OEl8MIF}X+c;6n-lLMRS!0u$kmA?dGs!v8ry zJbRt2OBZ}xq)!jNS`9AqQZK)cPA(E#g^js_^wu80B1uFqfd@HMXHtWFO&B%u`(HwA zkzuyyX2A|N=Sye^65QE*OVpCwGj%;`?R%Vc>`#D~lePw+zB|vN{5qy@ z4&EDPCN&7noF^n+^ex_{Wt|t-ARSnpaKCsq*5I$(7+N&R7z|j!S32M~dYr39-~iqI zjUI26G-<`g0TV2hhG~*7sYW?_=ahPZ=-zQa>k}#!W{R44aB`FVA>!C`wCGiuY9Jes zUF_h|^`!IN&=n46b%)}drAPY0mi@p{E#~+BR(K3MPY?=bK-52O6Ez?u_|#ddQNB~1 zJ*yhtsKFLB8;#;^4=2MbqC1)7*2m6z@0sq(-ZrZp75@EjZ7iu)H)oO`=J~Cs5Ug{9 zvRiiB?rodu3)h2tF;gKurA{RLFz_u!1q|MV46g?@&d*`qi!ky=1T4V#z^Q;Olm*@Y?|ZNdS9Ykz$}-bUmFUqUO# zzakDoUS3MprwzKF8a5tfE(jjm{@glp=q0`n@ynV$?JLBT?M<4PLw#CK3hn%|#3?s; z%Xl|6)}KH)DC!rI62%~UEfY~0o`I(Dj*@B%Z5_GNKG?t?|NAmB zFf)^MH!mFtyN5N+)=`rLa-wpE=qW`=J~5_UoWQ)4;^6P48!FaJ zf)k_~o{k~bFAA&Twlh=3#nVwLpO{Hp@I~1PwEq3t4LD6`q`GZsQXT2ffsgSe$@#E%CAzcy_TM0AH- zJ0ByI*-cnN@6N+tMIMTH-eA#10(pU1mp4bE12grYlrNDeid#GWNnt)KiZoDjOt=SeS>=+hMGx3dWzp|8TDO@rIVYQ$MO-^3oc3X8HPBvsR9ImnV||9F$$JyY zx2#$%aKodCOJIhkGcsezQk=tuE$4H6US(4lU!PCgP_&bTMQMo z0{R5kXpzb1WgjF9KTE&0`ICI#)yEDu*nFN8TbRg>H5+H0S;zQnJXLR0-gTJhSZ3gd zxlG-rI~_Y3pO2yh_sf*)^!xsOYJTe-U}J%^jt%(1*gf(e2DI@=*?_vXu{5FUk_g~i zz+>Ad2vvq>f~uG_39JEc9I2mhUTl_sp?Zw-u(|7wO-(d6J~nOdqB@C9INVOsLCB#a z3Xgloo1QG0D5TF*MUF12(uXKpUM-2}+6lgLKjXeXzZ*27`++y~!-`933`n=8cfoxm zQL5d2KTzw0oEjc~aT;!C_3LALil`bi)N0fz4!~1os0YI$v)pK@>1C%sJ_^9T;i9Hz zofxPcA+n=3x%XS8XeJdO6oSTn*dCP(^*sH~uk@EWDd#WGmxHrEv1xbVE&=&`^06dH z_ztd52pS9IqUYzH|4m)R502wBsJ|WBY3toH_>ek~+*$O}Zpg}hBBB`uQV^4QLz-ET zo|I_uDxUBmH)j~Vs6)zg4t|ve)T+hi} zTA!=*N@bY`%|R9oF>3dN;NF23Ew>avLbn9MZSWqtQs8$2XQCG_ zHe20m#}e77s(aBQ6MP_pz`%=Q_Onsm#rJru3~JZJi^-5A;x-(i`I`|z{c1BHh{`|p zkZs$%QY;I$3^=%8B_kU@UMPkjUPL*}#WjmKDCT=$o<1z- z*nZTlJ39d?BCp@rV>|CMEOhn#c9x@Uwl@oT&`EfM< zmL7aP2i<~Rp6QJ?nDtFVtR#1gg!;Tc5hG@--Ry}GvM-`2sC<4R?YZ%hc)Vi68WPHF zzbo_1Fd&;f62Oyuubt@|L$=F#eqjnhT94x%r@Rj>VfNf+Mvtxn1W`K>GmSxH6Sn|+ zvp;&HlyEEAIy_;~iq8+VMiG}P=El~hjNUwh&$uR=|MfYF#Q-C+P5~(w1|fALXmK-` zUTl`->0Oi1U_2aPIMeXz2?6zIPkJMwIp8!q%p<3b#VCmm@o2?Zq}; z?F<3&nC?HwAw)PPSSr=;!@sA{@qjf`pLL#sj5@2c7ovfd5$R^#A1&rc4!UoA_OEAK zfFW!&AuRFMK6(&gljouy{6sj(#phEL%T@V4IEDxi`^x5uLf5n9I50BFTVa2fgAB09 z9NB|AFB2$bYI>>-%EeaW5K(B9R%Y~e$p{|*iju3CFADEP$@7{8#3@7*wEJsqR~3uY z_btd!5~7A7aMPB*E=ui`0x!i;v;VVu<5lHt7l=lsPz=_pR37HI6ZD9ayOnK3AfV%L z%o`FH`)f6F)>Us*?FOW6rD@vU%SL!zS%{;DKp}+B;5I;U8+(|_aB`aiwUWUeiD~c5 zm$`LHsd{?Qr`|9c17TxIhBRTH_@L%(+BKzg5R#=U9PQ2wc3~-M2x~0PNEbc+M?f62p7Ybs+oNYRV#ZQR`{t z<*J|#$wDxZR3Hdibw#i+t!H>^d60DanADvZ!}wn=A2UBnyx`*(p zvS&8718^qN{hEX*5GF2aNs=A}RL&}GxS)PT>&gHwj=rlfdr-jbqPw6@ulO375#+8j z1L_PEEa23+lc-!Thv%&QCd?3%Cb(BmH0x=-PpVM>EC3C5c?qlbHO zX-UB#*qvN^9)>a>KZ#4|bpQSXg-!E{tSD`gr4QG4K13?3;J4*T7(o|u;9*<76hwdU z-mEFNS_E$MY!YjJrH}<2Cb3crrk#B=UQ(TA;Ox%fk-EPFu1x?zLnp4c!bk+Fxo!6s zh1t)9omIX8{ndGs`T(YV749e2cvlC$p){nGN<86eFXxtM1&e(v;(HRFUlb#uTuGI< zLW{p0x?`wqg~P&mfv|`=!$P%0sw;4dHXH3jVtkw>UkM*gpM9qR+OMnMaRioatbN;= z4>BMW$7iUupP?nZgYu#ECAQflPVG>ofPQsvDMEr>g<$}V3zA4t&ft_Zlf?_wJs6af zIqtf~BYLdHSBr1dcz<)#o#8>25yeeOoMXqF^vsBh0B4k$Tv;?!yp4BTtrceHO(s() zBdb%Q{^@R0v&5kEqtR3VMcT&?1~N!KPYXMM&#(^$Z-3~;Oz5R#ffG2!M{EfI@47Nw z-^xqH9B4s_Xz?HhSXaF=)5HaU?Li8!JCWYW1KM(2L^ctXEz!A$mxM>-Y2$2iY}}74 z4*W{J68c^rks7vzcHOm?LRf^}rX&l_laPZ@R5SgDe!jUW{-rx;u>0MS8Ar=&OwVn$ zlKNb~8jJV@tM!tsm;Pf_+Tw+n1;^+=#m3Xd!7d&CqSP^CYyzj-a5n20uN$q!bZvGo zD(4*Tt;5P_rO0^Mgv8n2~Pj zE$urAjA&~`r5TvsUC>9Dv-&ZQO=7X$u~f5tNEW_0WmD{TXj zy`~$gW>(B4Gd}!Bg)+$e1n9{l0#WE8`J`weZ~Rltg?9u1OhB{03e-PPNPp-~sol-q zTHbwQ^Wb;0x?nNy7k7Cx=~?hEz?CizxB%H^?VWQOO2U_Q0ylE~M!!SS}<wH}O$swQu#VB<)924na&@xWx2}|&1*91|Iur~N(rVT!B;Rd<8ui6Za zj!7;gFPCRjmn)ScU<$wnr8p-s=KmE$c-mB_KU>Rho9ML{so*DYfIR;VJ|u|FuW8)V zDvp8N_^ol-7F^A@i(lUJYx29I|4qa%ir^SRk7kz8YQ)5sPytE!{(HazqoHJ41hCAv zrtegb60*Pd;(J=ikLhAc@q8X^WEahd^6UtL%nB3FXjpR$X43Hq*z|Xs1I?P4*;veE zo>SWK&A-@pLnF&f2{C6*O%>G>WUZ5+)l0H1kmUBz&QS$^mFg?j?M$AF`=SkEdUj1G6v7NJF0ZHdK zbizZVyX?{_pVlF2B=5U^FFvxIIyKWl94%KA@FU{+uv@>Tx|0Yq%NpZiFU+<5w{irt0%zAy`c& z5Nk*IVuCEtiY4jggh_iAXDfNcil*p|^nHt21wqf@`nr~Z5*y*m0RU?o%C{x{<3Y-h z0L_u_hOUPc*jF5Uy8!NvuwprV$RKYZWXaNU&i~s*F1XBK7=gbXj%s!afK{xP6LYf${)%DFE&B1zXm`E zc)3IZV6X5n5{%QGm#QSO@p_{uwZ_rxQJ)=gxJDu_&d*H=Nyfg32>sSuW&rWz0=)4i z^+c#up=n>)wG|fOBClDM^@@#o`O;EgnQ1uZtDtzAN0@jW>{)IjmX5*`LOI0v_xsyj zin;J4ZI?i5(7Kn`?{a6BSyU*sKTOk3Azn{>zfh&TPcNieoykr+yp)< z999R*KSy3~qMI-*|Do0(ZoJf`q@i4rBV-L40N*pxcET8$__&;P8ub&bH=B`H(C*{_ zE?V&g$0*M5fll4whHpn77hMtgJqqM*F?I!yK|OMi)Sv7Mojpa>^h_Aei?vyih`Y`i z4(fx|XfgJ7L3+J=CVH2V8h2C(Lov8Hulw{x$V`bcIPvwYbzv;Q8hAGkBSgw*MPsV0 zOBD#Wmh^WP&T_!s%WO)-Q7Bl#&)0_AQ9-;HZ?fb((k*{vvFTtba*F}IF`n^NS<3_2ISM55*mJ77b`tsbr^&RFybDauDaMS%^uzz~`v z3pW=}#DEp}<Ne%-$#T3PhMdRfGVdcT^ z8d5%--)S0CPh|6iX4`~f9ZvW@G~=9a6QDTJSOwD;9)zo_%5zrA3<_`AAzy<3)U7i` z#|+vk(RE@;FtLFs+Y zSK)+CSm@cG-Jfcl{nxn~EhxNR(X{|hDS=$NkFq(zVof!KxY|XDsU{eup=uJ5cjQ@M zb>7(elhB(H2Sx&YOPkT_5^h7z+Fv0g_f!B7159j3{yu3kiR@T*&Y$ETX?*u0cP$<2 zIy^&DZU{9mq&ie3O zs&NtG;oKUqeYL$`3Vfe-;6u{ShUSug-b^DYvPBRS)VKA<%fKbda#-|VQ~5iniI^O} z*M&i#m9i|Cs$N#q>c?)z0IsT!1`+=kpvhquKWR@po;zPX6Pzx&NsWk;{ih2pZit2_ zL&3v`wHWpMH_r_%91j+Fz?qt6gG2@hMBeDp=SVrK?JN zYsfwBk$&rI%OuDAiBd0dH#wQcdY*kA@kSf}_4&<#W=U%{QFT1ot=L%~q)d+NgrT4U z#V8lIz-EPWUJv-6C5vz^qCV`NhR>3Kmi8IWVU6je?nlzPiie--pto+sg!d_#uww}M0la^MlN8#RthC_!E~c_ik3->4 zvRc3Rn+ia*WK~Zvit>*h3EsEsn>LqFQ`y4*F<(^zRiGR+<=@3f4)>mb6Njm3yefRY zPfK}iTq#*44V6bv90GqQQU5>AhLMQ6Q%V7ljpx}+&86H+F@F!4cfzx5^XL(&LanOV zpYiWFD=lnEz&?3Y-z2sBmh3D1*=%M;zk`Ea=mY@x1# z{&U=3N*YN=DZr7=h|`zzzj!{&tMFMKJin%WpiPo)^?gm0%?e$vpWFU3aR|av@>YJ1 z=X;mZJhlosQE8HdBxyO$Uwq%R()}bTV2K4U>ipPn-t=LAu9jmt=fu2P0=+q=>(M6= zuHPe0VYcP5A&EiSyVlJN0`+Mp_5)|Z24Xw97TgHfUeVf1VZ1YL;i$agHP8z^cv#9X z{89jgyNK4^(>E<$m5k>|iA@lMEmBnnkw-cBa{%u0RObkZ2+s9$6Mau~kdp5Q>_tMR zko1)nptix2nGOaiQCM22UkrWzNIlYkblIv2R*jz}WL;m<^ff&3LcOfkfdwYlCRYWr z!a&<;iO09$JD+A?*(&2Xt>KH&;H$w@-1g?H0YqWAC{pgaa~6eER7n?0Z@IYP4b#@7#aE+7!oc&$yt%B4BWrGh`=h^&P%uHKyV^ zZayoxZr8qq32!#8R3a+st*euckpducOBgkZcnp5GXyj*5ITSY45Pqxm>Hwdr8=YtU zBzSgjKWKKoWNdEpkeIibLrFT*m!4+s_bK$nkf>~-rL$RYS6~N-WA&KMp>^v}99J}s zyR5@)zy~1`;}6|4iL~@gYka=F=i88%TltP;o2U=BCN@vD2<9(VfMBJ*?&X$@pa#XI z6O6&EzNfmn(rKuF+s;;%#FIRWH)(Fi2SI_oZc5SS=LVessip_%vbn)4o>LeHR0X)y zrcOC2>V|Io1uZ;0Zm=kVd^Ct_$68+wh!{|Hk7W2MuG_lfNemoUx0UZ32gnGHvgdR#sbS$zP8F% zo0J~3=(||rhdx?V*5a^$rwpCwE;^Rg*)IOJ0uo+7PT!R}t!weY0Yi~0y~=Z}C);SK z`l)s*q8HF|JdTJJKatxh0$%%=tiCw?N>{u7ksX*qD_Yg4f04F2&e>?zI4G|Z!L5e( ze&}8F#xMV*Uc;xFzYQdh+GocMob1Tw5SO6^f-yYreb<%U$4(M?{ZLqo)yPghu(RO;w|431 zyA93sB}laK*B9<-$t`MP{*V9+r+GxU)(CXdI<2_#-`7~K1x=d_h)8B&;>cQ0O&`RC zls_gp!7vNMO}Hm+9-hYd(xs?2zc~!Gm(fS_?J#yPgn&uHC)*+YTN!i3%_v0)K_r=* z2QZWRK;5i~hp_2&Rj~Wkg6iv7v8Z{Fjn+(o98axY6Z!lq@c@ z*}>h99;=15Gn(mKE9R4j!)$P2c&Tbo`^7&mU$MbQ+B@VwjMiIaW{&{DJ-UMxzoY}7jIa2cTfp0EU zjil<#W=?TvYTYzE@!H-%7n)+KyfIis~)hob~WH1qrLX8 z!QH2m_9ez|S!!e_#mFI>r`4{dlWt(+VI)tEw0xOfdq;c&sHm79%L6r7@nlG4YlGai z3iZA&cM5JU8bKo2w+!`(2-nmdQ04%=giB;hAyPEi;`#qOn-od+UkBk5&AcNRGzH8p z&-(+bR5T0+O0F4p=2?6gb-l0G2~9r@+PZ2|#UhOT;nSl_{Boqq6xaI!VAv+@u*L|B z+r~*s*3kEjgLB4w|G{MFO&p$3)ZkpS*>XZ6Q)wB+R6IMkFk{+fvJ!5l^pfVWi{YlD zpu*$G{;(xCF{eY|T>A=%kiB*}7?t0~Kmm4bo z91othF2ciDgGxCLvYsUkbcht>;g)Ypl0VXonHO$Gz_Q`XB~S*CF9adw(-v%f@}P2E z!oi#RVG{_~FXap?B@8j}eOIBrBEc@#V)NENrZE?}aRVl`Naz&vbTgmQ;%NU&j_ZRj zA9%6S=~t5;z*0?LZT&n{g<@a0IEE%y-a{j6CrD?pllahw3Pg$k;Ee=4Q23rl3aGx+ zeHrtMy5JXgC5e1DWDX&4GKWSl=?(L%-K&vn>h5f}HVWpN80KS+Ig3(nSo_f`W(oZ+ zB}SW28lt&4yXl1Rvt z7}pJHDu)gLhw3AgOuP}{Fx$gGxi%om!jK#8l)!zDr}^NCBM@2Ev>Fc)PSvDTq_Fpi z-h1{L4se*{k$>GHkd8vzGzppL#>04u1vC+n+XFGzRFL1L|R?373k=OF@OZZQa0wmXNbEYU9Q?I*m*$R=z_a30Y;c@ zicyppk3Wy~t`1BaCP(=z%c)BO=E>fP)Q{o4_Qj&*f=s8xidJMZP+HI$3j^=6Lo7Be z3g3$4uD1>8v6xcGBmgbiTZ$A9W;k21;pud+hjgrR>I6(YJB%!nBVPLY8qo)iXLlAn z@RCLUGpRe*{r>LpE_exO8TBR*v&sBrkg12}oVTGBdT&B7CFs~qYRg`cZ5Po;*5|51 zZ%8UyBkDT>2Eb(}IEG9>}_P~_n?L} z+S%sO|fV4vn{5co}0oGF#cqr1q%SeEJihxP`tYxl!c+^GT)- zdzPlS)1CcPkyU}Ip|LAyt&^oKy$(sY(Ko?N8Z+a&q99 z&ZukDmH4lniIYxalsdHzyW!+)G)ucvTOf24_f?*zT8m76a@EgTEVcuOu4uCBmpTdP zIJ}nZrPirwJeT9BiG_rpG`CjLomd7RdNjrjkCc#2{0K{Ea3vO5?q7zqhPg9@#F96^ zvX_jU!5^K&1~5bdZlUitR?gR3j(oP9Y|;0%4ITEZTcIw4EVHugh^IDEtu_Z0c#Q^Y zP=*(9aCJK74rTR4fnbfI*&3K&Q@G{Jo<~XM6T|zTqe8#6#k{!mf%4fs7v{$y+C_g$grQ^QeWnjb#+dY%a zH=*WWJ!pjV0q#d~iI*vs+(DSi&2RLLN-K~~qZ{IPWSIJ)a2M{=_*eCmPpeiFz;=7^ zjR$DdG8uDKa+?fJnD{_ykdAFx5EvF-7SoIm`F&~f4Cve9NmvV8xva-jUc7N#_=ZJ% z(YH6@Oq8S2!unl3c-EbS&$ehCleW^6(6GEThWvTp1($>8-F{qc@NUl++v6laj&_gn z2L&Qr2N8Vwm0W!q!sw0n++uv-Os7PxJe<%FkjU%{r0x0#rpE9*$y5ZDP9qIQ*Mk zjOCruM5Y-GHcUwJGF7~*6G@qHm>X$)$BzC0^f^6Hq%W-KbX0rT83_w+6`SR}ecFXn zF|D0-NgD(5El45?<5R**vQB?y!YA78T&*tW3!4M-4vvR%$snejW^ZQ?8=;qOxFx6p z{Uy_7l};I;J7#@!T)33{${b{wmT0f-jb`A?rme)D)2-p>|C;zA{UJY}CKKE^{011@ zG{rq?}Iju2w%6!R_8#Ws&uklLkuP z^dv`_Yeklq0alVL`THIhERa755RE}=A9Gl+C7un|^sc3#f7+>79 zbg8P5)a`xGhg5&ZM@R{uAM>1$Pe5JhWb`UT0p)g>$*eSxD&VkH`^2%oaY4KI^RqEZ z=u_u$l#_`crQg8~cNlQZ0z4^K+YcR!lcK)F2=3Sjvtr5Y=P$i_+_?S#D81eZnNLDh zz@Lxz7g<{2BHQ9Rh{@JxY{AInI+tPHuak*EMIYUI-KI1|>z2@o8dRx36Un#9Ik=T- z$8!rXhMYZEKT8=(Y!0BJL21H9cc`qXKiJ=tJ?Jbo5kS@u)V)FgTwYi;mqACnoncv+ z-2E<)TB3x4BGd-;&_e~iWBuMF?p~1J$U-X_3#*_oemq>?#Omj9NFqUI;TJ7GkgIdU zu3P7mtF||waqeSfRCEeCrE-@cr?cDWLtH`CoHUv&5s)iz=)|*1S9ilb)mNb;m zQ~%mgT2~ze3-9MotI)Gr02v;Z!q zLjTRc^Y>WeH?C)2jN&@Jc(v|v=RAmp&CR)6k7321WL27{Q zSHjxa8cnTP!V_i_P?HQRQC2Wlg>>d~h047YI^Ax>{~{5~duHCx*B3YK+a&LCK}>vy zU(Q0ek*)YmW;ASTbxlQ(_C@I3}sBp4!g}P#Vi%29A30b-=OQ6+PtY&H=eV`HAgRL z50-OO3Bm3W9vKT##M)^jO%9B%HQY$xIW9p}=q2pI1?%46i%o#Kk8z%{@55+Y;n>>x z%VXenHVgTvn#5K71CI?U>rB9`Qwl>Syvf#uIA9~5k)~Rdw3k1~rj(a6Q(XmZ*=r5o zzix*am|Q$F>}18nt>(vq-k|lo=vZKW^%5Z76j!)F`%_bNG5DB#Y)X@f99KyDWAUN( z5!w_680Ool3uGP8;0Ltoi1Vp!w_`P}nuq6O@y`B^O8RBARM_Fep8G5u5& zl~q~&{j>@f(>iIpeoI0iHUuj)Jv?4A>o>zhylsYD<0C83C(&#o^Y$@r^AMUO80O7w zbt<8R4L_$(t135wfuH)%WSN}g1l0FM z-?sPEZ`vmupigGZhYElc(QcW@x+PY)wfv3=fpu-eU*7+NX`dzK>oj{54}Atv843OM zDq9D$8hshXR$?^aph>}K2TT85y|ZGRr<3d@l$@(z&i~^D4Vz=a!|&y7Fy`ca$Ye`r zRDpu>sdc8H?~6z4qP>(4R2CM~t46ts8A==}Qk@^faRQ>&vY1YO4KjNujg_|3+-PTL zbX%v;fqym9uD_P^;EO}NCG40SRLM#Ude!s{lX}Zh1b>LK=a1x0_Yg;eQ$RG}lku^P z)>BLM^)uzLakHKy!z>>dU+Pvis+9*uelTSL$Y}ck@{N`!kqeFbl#fh_Ys<#UPUZ-XnMG+wiJ;v7m3K?K%nXDK`Aw0xg7r4u?J6C+T->H&M{E zaKJX2mZ$_iyj&SsRf}LL13c~ql~UY~DOlY`PaZz3^_!^GJA00;1{cq|&`4+-(Buq< z+^hGxnd)Icly7basBkUpQHt&u{j*m!>fn^A$`)dJ3ts9CmUcVM=r2+n&#aD$KT&B{ zecuAz$+ChtqP4pg?9&FRD~m~7w(FRQrnEl`JuILK>JE8U^5eAZ1i2i-wY({GBM6O^Wap;-yLD=3k}hu0e1z9{ zGtp{fQu%U&xI2DC8J?NoMTtl~X-HQpo0?DPe~}fHDdZ1WVCQIeGI%v?+D6HXko(xM zNOpI#u#8U&h;s@9@z7{bSzf%Yj)L{n&a(5e|8GFBcG z#hngBv%HK1J3mr%RA=0h28s}OrZ10gC~>Vi=@iPr0WY!6Y5Aj}w)AMtGS-iKE+@?; zahRoG;9IZ=@}-ul5o$T8bx#Y?t}@*MCJ%12`)crbFM~TFSC9CswPMmYwF@is!G>H_ z+Ryqlv%r9uRJ|u`u4NrnLhVZJKGh}uvEZO*p@o8QvL|BY7BbXcUM5$C|0|fKvFKt6 zgfGQlSh*kuWph9If0g3tmZ<|- z2p-bB3g6Rdb3>O!zvs@7RYp02+HxR3ZgGpy3c2IhTub$Q*mvq>^8UcuMj0{Of0?Dvfwg%pq91M1DG;{M=(Y9Y3i61 zRv8&={K{7r4L(Wc(u6WyY{@)L6S8LfTnTC7^7W7MS(dNd=c%Po1I4%L{wiD_Zd_=B z8=iBHklBeH{M}2*>YKf`8^AeJ_<4R-(joxUWO1pS+0qZtee_S&ji{u=A&_cG~g-S1V+DV^YyM~b>oXtXzUXfl9IWD zj61olC%VeVRXEBfpSTu0Bg*|`l9oG}z*;`TFGCKY($)BsKrJ<`M2*-!7|DNP67moS z7-4Vbf&&?1_Q!bu$?aJ)a7yuE`QWT6PrJ>CigW&aTC77B)jBfEKmRR&rG$>O`NuCv zVOh|w>$OMMk(!-RQp%1l7vjS{(P_lM<-*{aF=#UOmzpvWuArK--%;J~U=vPz**UuU zLM=|7^Ew3;R{pevMVQD}5OryLs+wVZL zkc_Vh%HM0`xUlzNe0OqRs+b9KSxR2hdQKuo)Snm6^~&qD0eD7;||~iG$F2MQeE=Sap@akP3$U){1tB640_ksF0<>?WjRzT=+YVxz_tA zbwPLi)RqyiAZ?4I6cnJjMgmbmmQmTEr|!a4EsXj*b2%D;9k6-9J!{VMv6F?kz?u*5 z#wbwu-(PU)2(F`v?9>T4F0#cUbyO5ws&cCm#gbJg9e8-w zwSBt|>(s@AaVNjxsSUN$xhefb_7L*K^LUz_=fDGmeXs)V$}X7)e>$y@PWD@3rO#d#T5O_}y_ z3@f%B%si?~C+db3#KFiEYZ*|`sT%Mpgb|;LR&fK;k$WpdMVM3l)`kcP8<6U9&L}i| z(}j^G~*rilI>UtRXc%s5&GMJgp|NIr~fp43x$|Q<8 zR@N>W|DOO$Pd#7n&X((A==li@6@=85KoOx)wY>%&&?EjofJAPca+ZH^_;mLLC_tX! zY6&@y!UHHxYn-3iKX0H}N+!m9r5Wp-Rs7GPo%R8)mu5s5y0+r-j2$;Exm(t#f52;s zC=Q|i*{QM6i-f>_7Xe=WOEV_4DTAnBZ~{}{BNdF;DG+5!ZlTrLbg0(Bk$ngxf5h?n z9X4xIrX+O4CT;77qlt!c2YYdSK58|U;1^IkCO|N3qjk*etHvN;CXYQq9ttTMM{6>C zHzwfN$(~K>en8>NwYk#}y)K0aL?PRKnp~syHp%4t1&COUrb(l8&8sc2wb|808iSI< zhQT?yg?3h+ZFzm65QI5uzxjdq?aYT#O;!0jz}w20_~S06@bA4(txOY8000T-0kO?m z(~1cj#U*ALK$Y3Es@>mds2NW^4>7=5zEk^H@tAoF5@mhqD>?I0WnqArzGR^(Fa|XX z+n%jkXZA1v00RKm!q+e=rgYKH1E;Lr6fT6t16_;u^y6=X?pBiB-E=*=0#qqI6id8K zrV)nb4@w?m!eanh?!cZlvW=1CYpAv35GcKCP~1F=-Ep9gth>yKWhc2lQWL}*)c!(h zuJPZjZ{LhFae@J0QIvn`77pf5wS)LLB8ia1VKl$#o!I6zLFQKs%ij+nqlMJ?D zeScU^8O3YgCCis;%IO%7J|}X~ty#=#t)#t!`2{RtVvf>n9LmgsM)?cRm4%dl;r^D-K zM2yZterj9=7AkUtb8zK9wEyYiW}x71=xvEqvOd1eN-44u9^dcDnA?*3kK+o+>K^e) zn6q-lvU!$DfSbmqtk6eo;)LLY-e}ZCy$WGk12BQ_Lj~Pah^xZS%y2=l0?n485~WUpra=A_-6s_Dp?~1W5>jWTuCqrocd7E8 zkp`Y3A2eX}=bE+Hct<8wh$4{?Y62an@WJakYE0nTKW}U?5Z!bw1W!$9bxl^S933$L z*{KoB^>4RUYZ+fyTKcppVCuG^uKrS|?DobxB=pODRtqNu9g2CO`o)M}Qfa>rW(ZjO zsf9q)whc4G6C-gONRJCl#%QLPq-77t)`8cbJLZzFrVO3e&#R2i{?mj;VU(a-hPIFZ z`2cHOha!rClb3&E;mkQRiwKO&Hc^agR&lS*Pz796NSuXN|#zdhp_@DZ^{f-kTPze zS4Kq(=SqaRTSHMw|N4<+Apo9p(+S4q={5fa6x)`;7tTHR5PGU(#{ZfC&|3exNgZf` zo~XD2S?K2h?MEO6zctr(~8ql{VVMZ&5D0n>^F<@)%2zERK-TwMm?}*793Z%K2!m?nUU9?S?Bl zSnIM3qqhPnj7HxiS%hEmwP@-Q$Un z-bvm5o55PNfp7;Fmdq(AP);t$0d3PaU!c6-;;#d|VLs(>#t?eC>nM8I_ht~j?9kJW zN#3j0yL)p?El3fx)B!$l^7ym-=hzao?BmG^_2%Z(U?oh-m}?*ANJ-iZuu9(x@I}K= z*`o{s^L#axd0*$P^s;E_W;9+{jwGEkut;-P|CN`&@M9HsuD=0-d>b)GEQccC8WSW4 z1&)(}k-B>8u)6R15C8)^KO+9FXKtWBrSF$uUr(h%Vi!%i!6NDqG%3;+7k{eQmIB@`iI`1pH<%6WVWA=>;)!y;LK`6s-rH z1y3~ZZ?~K-L%(LGr{mWH1&b(E-EC%LM= zPvA^ENlzwv&%>8QPhG?>oT|F!`JvOB11Q;d*31jmy*3Mg47cQ+ zIV}9gBVxl!x(biq$2K)?^uq{Wp-y@V`^_cH+~M`bsm6R@bv@jNyYXbQqqfFa!$T?v zG+zIsCYz4$Wk45!UK8R}FFe$cU{cAh^m-Z=?^ISP61~I=-R2xh`caAGKbC|Un%_&{ zRvAK@z4S(2z!;=5Jy&2GF8`ff34bJWgj&8e9^0=+_p>R4juvN3O*K7T#ecm zUj}Bnck>XExZxFj?Yn0rghRSGt9_-Q7|^DL7q!7DjWI?d!@A$79Rw`6@dF2WdsnXm zkeNLCoA|e_L?f7yIwKUrn4j){_tC(Ub)L)_W#e`1`>uI-g2uxR%vz8KhXwleBD6GG zoVJ!tjH55eC+4U3mUO)(?|HM5eSy=rl{^Esgp-_?Ikpgd=Xod05G?ji&c6X)WP*`- zw{6H=F|T(dd_j3mZjI+AEq*?LLY;*&8t?VQaJm}SIrAPI%;XcWIXoTI1qU6hCOJSg zb?iMHZ_#ONJGPgwT!LDEH#2 zjv9$`gsB@UzKE9`)5l*x{B59lJQhxv-iiZzT65`d`#YxDbvfZ-^3otW zUGLDYaIN2mVD64eq$zAk`g_w(AQX3QK^KuPDqAMpFimHCyL4_w<;?zqdbG5WUC4)_F>HkNhEd zVx`AX^WN3l=!g;~W18l?+FWP@w9)Oupp5gRnyj?BBPyqX8`H;f)-OS@n+U?OM^9tC zA(m)ks8!4;BxvdQMq{P?5q{MxSk*dviw@!js3!G^Ga2b)I=ER!$ZAF0~NO+dj*X_tdk?7%H*^E#o?ezfS*u^9HeUFtN zsKjrEk`-+bZ~wl^?cd~!1Zl7JlhPAsSLPz>Gpj30C`*bR!r9?A@&8Oo!gK#=`;(kr;|Wj7s##Eoknn!SHb71nw=H5ocQ5JBi7;BW=m3G~^Bv)H%;Oj6mY=w&batn7c6D2c?g zgr~s4(s3=gj9lJWgHzE2n@?|iY{gB!!Xo`z6EO8!`G&YrIqhZjIJU8${o|PONuq}W z^I*4F00G=|rPk$$w48T9wXO~ZmZEN)cO)R&YJ|Bc96ZVsWH*S|K~^$ZZw{{d90 zXODoio7Qb9&OaJ5qJfv%z{VeY&63X2E+x_?PK{G4(j!<(Qlq2GT;b`FfpqeKo`)ff z#q;EBEZZ*lBru(e@qx_I2lQD1=!mRGQXp)k1u8JiNweVOW${@a0booOjjWKe*(fG) z>|EyITO@1=!&U+k>Yw>M8EZrqP^NTEHj|osvlfSEmu-wL{*?W;(7KVbY5=!_Ge zOanO*euRVt;9@-_fG2uxRU14ERu`lo^t*U`h-G}jYS~7w4CMC#6tgmldfwK&LoJzY zknP>Cu3esU@J`8Og zCMJTVnBb9+c(gyC3dI)oOWfUabOMM-mX&*pCPZvqh@*yc% z=1_r4=&P9AXmh6A)Z2K(*Gh(LgtyF;5!51y1Dx*j_^m{&dN` zS6TLIF)=VWI({2rfcxpm@|}gMTf$^X_`J`vZ4I6~H59{`B+0kxa>07)jm1D?Z-OpK zc%)yQfsXh6+>*|5JjM0;MQTIn@9T&hd%bXIvkCXjo2_ujK%sa+SSS>`^~)Rzn+*`I ztm>zX*?=6RRHj~9K(#J*0l^{pogBQ4gX&GNDTdKi-njw^Ra)Z$l5L+jHBu=N44R!~ zG)42z-vR!5ZUe#h0?)K2t8^M|2qW5eUJE7~GiUjdKHy}xtsgVMRX0t8j}d6H%+`hF z<$;_e-@*z1()Gswb|e07b}Vv_G+fk9bYynlocfSnPoz`TZ#BP+V7-Q>-7%g8goFv% zt#!0j4=1VZljk`=Vwi=9>{Ax%Rx+96*4|=zF|+X_g$GK!)+h`=+N7mIr)7eP-OHVg zcl>QoDeN&&L0thpZvTI+F6KOG31PtR({ZJvAEl4Ud3<{dp&z3fMKTXRTl}Vt>d_0- zesBYy4Q4JtFBNm{e22%kS4LME8&Ggg#=83qs$C^9mzT9YEMP2sn%2EUyb~sNXKu^C zAymIHP%S|cl7a-E3dIJ5$9b@FSa7sCfOn*z8Y_fdyL85jC+Oh<yvJy$DAtJrE(P|{v)H;F#h`pVyEvc(8r?XPe9Xx!$GbckmhK;O?FK1yBuuH zo0byM&VTcG0Wb)RzQ&KEZkj|+CYH@=*-`mr6hsQ`e>H+gB(@T)GNCfbitEN5msPCVW z9D(`VJ6eQ?EGYF=-m7vg@L$zoeft+D$ zQ$zv*x#oef_e}d>LMWeBf>Xdy9gp!f@z6TE9cxRMW0h6i51eeV`+~?Y|9=ry{ffh! ziLO=X3@a-=3R;kXYkuV#LIU<@L` z8A#q=IHzX%EBee{@psjfDW@p9w@=JL3+@3MdUK=agP~XMKPW|<^qhSLk#s@taNhtq!2|W49T?mP57MFMFSHz?uEfWh0T`R zF@7?i9zfDsjEVE6P~Vkimnv#F@`K&|48tdH@}0zjwi#%Ew` zo9ykNg!-a#1@6>pmd#yLr9d4-f_PbIjirUJF4LLDnRquqZ{4kJ#>9KCnf)IjW zcr2_#s>hLZBwGtN=mW3cmaX(Xz}g(-&g-~#wj$*csSo8F5XyX#2RpGI?UhO#;&VtH zK5wqG*VjSq0LC5~oknNo?ZJ2HQvQum3UjtscWE8KRv>6FfE%P+{kttJ`h*Gp1@;Z-=9iu++5 zoSWV0KlSirV|$f4!=3E*@7{n+MV^;qq}Iw{nZwmoHX*CDemW7XHT2lV)0!pq6(26A zlkiyx01OT-?pNOF9ixG9fB*v~(|`adL2v*XHP@{ErodZsu!Feashr|G3pBNUWu5>< zi5?470thZwWVNm+`=nOk*E+T=KliDINhR}g4Fr;&WVno!FBr3<84qimIz?=BrR=SQ=iMn3V+>V$XrXg za8SIrx1cX@Z9rikT2W_ z3GjvfIib=5?AM9BBPw{TeEl3(!LI2tHYgN;J~(f0<|_!!*uTcz6oh})h1O<5IxIAfx47W*Qc78f^jZdN` zN*lu`ywgf8Kmr3K@F^Hb7`wR~+U7ue$IBw-7+o*t!Q9a`)G*j@Wt~{~FFsfzg(+x;EtTrJ-!4|Xjv(MBXkbxuB@75(~~h<%5psOcE3bxDT1w#eB|xPrc{3Rz!%AZ`WnqlcmuH zs1CK)pNmN`8H8_|_aOs+K|L4iUa4=F96t&cE@X;a@Kjtq5knf~ocq^j1mRMl-N{aM zF0y_7#PRl{r~m$Tm?%b90{6BH_OD?ECTAgZTaN^wk4KH+=Ui(0zbPCWK%kDz<`tiw zmbv?sy&vsWzn_dpTem&{e!7q6-2ZMti}PXbjv6_NrL6B|{mEIbrW{eR?APSJH} zcsMA?WdwCl^#dHs$i}vTqEK2?U()U&U?qFJ_2GeH9HJwL>Pg4DuqCYIZ5m<)+ULX& zTZNF}^*%04Sx}eU|EN2rXVB!V22^qU&NM9Y%D(T;N0k~i-;b8vMq@M%yG+>H@$WNp zPOWM4KH+-fZbI*Jar)rz_uph%{wXcAJCBcgWC*{AslQ{;Plf)-K8SjDBD%v6Bx~_o z%$LLYW&s%#Q@y#B{?|DR8!yc}*`kN$*)usCjU30mB4{uM;V(usR&#APa9i#ptg(ey za`4|=vWP^wxzb47pcfQZbKz&-oR>@SS2?;Q@C@tM4O*Zqit-p9>N`QiP5KAlx9je& zh5>@+ny*_aQKl-SdnlQUe}RkAN;w9u7JHU3!7TYWpx!Sr%$p!4zR-`w_o~<`n4Rf? zA}gRJi5isuf+6g|_l(MOagvNt-4Y=B(ibWkzzz~_nwwBSZ-mU5Z?n|HfmVf$K<|-!{t( z?Wy1I$+-Kqqn8qKR@|Ui6$?*JF|780S8uy+@A;YaFK^CSgDBn>D@d~Z{>dj zoO7EfLzhG$EhB+cNm}EBKCu=cebELK%O+Pz$#qyyQ*SgZR@(He_kx_yhD5=n;2rl1 zk~cPxM_x!1%$X&A$`^u0KXK(6jqG*;fR!&T(-wIBJyYK=fHFR2ODr6(ci>aYAsWGO z@rYS8@j%{YDCnhb4vfiDQ}03o7vkP&6Yd8rD_5J&jeA+wg={V`h-po5IfNymyIk0I z7f1PaT_d;FpOZ)mkNdB<=F=+wYom6BCOaE==CHBS@nOgCe4=QXv0M|&_RE;43-wTO zSgWm13wKVB7tpz{lL_D1gkGW<$iX2BvTw{f6!ZmEpb|4r-Li&JOqH@)C+!NP=$~}t z^tOqJU)T+VP@)iz(6`C@?R|AvRL%SM*HU z2zx|*#-SrYP=B9+*b^iE+@Y(ZUN`xuaYNWTYoc`!iRt&49%2c%3oOHMh>U9OYBGH7 z_@j(F4EB5&L)VveEAKMGKX(mW>68i|+h@|vp6?aXFls5BQ5O-N3*^Jp>!dVe$!W|G zBV^;j-FgQVQW$Tn@Fh@baWufF(cF|Yy!!2OZy%o)$s$(&XPz^PeTum~`&1Hg3iWp6 zUuO!6>q6C^hC@CWWM{W=^*$EHHT4j!Es6^2S60{IB*W|ygBTlhi;-m;s04RmZxBAn zFxib=uI_bJ(2Uo)Akrh_SED~lSjA< zOvPwE*@q2DCgOhV0VQASFXHly0EM&RH&5J3Ehb=QA2m`#@1f%Mv{)}!iqp!11ll^h@>0Q1jlSp= zcW1o+lWl`q(NtAf1g|YgH#2407ukuDr%FLK`l~AOt2NJ5NpWLpFHj_5Ep2IbIcQ+A zJiM(VT233kE_*G{Zk<~B*(kIExUor%CRvuB+1cDN%E2>&Mf{Svez2)BfnBSb&)@$e z+hUgm(J&L9pbIyQ$7P~XT0fJ;T>c||;4>@B?78S^x!4ZNVoGS^dT2(R$3pG0?xjn0 z2ynM7e6<!9 zct&`ToZnrpJrihWqqeFl3`bB{o zM8SFNodhg0*#IXC!DG~O^-|me#oz)nkuRn^);w0`4Q>85Ipph{ZC2d-8>|D$Z|+mY zBk5xHx4bX(v>D0Y`Gm{Y^r5boCx$w@s*8MerE`w>F?Tc$Zs3?18suj6;*8TrA2yFe1K&RMzY&=vIswzs$#}!_Zb<=wJXiS6lsgzXb+uPk%NdWXQBa zUp!?ZU~(b&z3d}a(pvlO7;%28O_>py?$qw#A}5La+S=5ST6g-Hrp(_joMIvazhm0_ z_@I+#bH(Db74S*?vew&~iW;_F<(k0@7+r1WNdZES$9?h`_p zDtxSuA6K3_d^>Bh)Ei!JI*Z)8Po$@>Hrhh)cIs45bu|Xj;v<3vKkF3-Id^dx-$_e@ zv9i`Y8(puG+qcRRj3wJ_dE#ufcm-z!y7?u}%x%wni)i9(3!775epoQmVa%g+Se~%$ zz(V<4YrZkT(}eSqEwRbC2}Si6tNu^lI=D=S5w&=RKmneze2gw$XyX-}SHX!6iv@0e z9$$lrNn<+N*QpKh6lT%1dA4!iWy=jkKrBcXqG}p7u{DI+?U>MzGMP&sj z?!J)}NfFZ5d{cHwTqf;OZXn;LXNXAk9aY$BPQYL@ZCCJp7n>M8usXfMiNJKE9OD0{hqR9h@9YQ4o5OFEadY}lYHEf zKD_5Ht+($g^N`2zzYyNmz;Yji5pU>)@0ELxlbt@y5E}1O^E%|f@cR0$gsqBKGU-Y5 zw2XL0IJaP!OW0&{-Q`)$#asSNxk7^`_xbnC9Y5K0g?x0K)e6}Kx_H4bZ9O8j~#Pzpu!|7{8p_U7RWpA32ucQ^pkwL^e35_NXBydGvcIdSv z4CP(i(!&|E4p17oMP#BnX~<0tZkoB;k?4>^Hoz?Ber}HWNv4z&^(LdRypk@Ro1Sztwxq4UC&uJZXk;6Mc8)%ooS&i`ZL)}jkM5^+nD1V zI&(q@P5)DrVxMZi@Zw9qi8dUxb9c0&3Z_b9$8Q(g9h4;811{g?r}gqrD4=hAm%P`} zWC1JiqJ9FaUcVh^zhXT3sK8yCl6at&(Loxi(WagDF&T2dglE?z;obwK5bMkEGmiSJ zj&WhLu=xg!0B`ZpnS1I^Ddfw6nQcv*-KW{1bFDIGp7#n;pEHwX$1CO76;Zks@gUn| z)dD&vMWwjk94V79*}T2D^cHXOy??s=6I){y7W%I={kra+yG;>@ZVR8h*lWps))Rpr zIbE6yF(%(F#d}i)so^|aW1U?@4=m=NFXpvirnrNTGgd;@HB0{`_moBT9`U>L5*L+N zoD&a{CIKG13@mNz>s~J-yAXwVhGls;e8vSYzvbj2i+$s*^tjnDV&u}9%`nrQaag(Zr;m4(C= zo52Y?iI!{onEPUNxV{GIG)!gW5f)KJju~?C^Wa8TO4<*sPM5CMwOF+o*f1y!?R06C z!0*w9cNE^=L$rVKtX`g9#jzn`P=fW9(uMCFzPGVSdLrV~-?qUnSGb6` zN|%xgdHJ~Z2t?kvnj$jr?=v~)mTP4_TzMBcmF>QU^UAQ#ZTUfu0&fdllc-q&VU>5 zc#Ep6=|aTmg)vgs^qBAvg+cV83l#Fq2&}gE+7&cO&ZC+yUJG>i1t*nWbs-{_JQb9v z*`I1TEO01zII+!nn^fw?c(7Px7`2Lmr!8xUOy-Mh-jb;5-u*E*v_dMr%J>2X#Osl^ z^I;)zeynzpzTxbu@B2cFr5S80FH7IC7d&sY(IUO+Yr-8=m!qmtfA-AkexaqB2rGh> zAN*5i=Nslwf&)^kNx5)7H1u`bXEk51kp#19nz8k6mY6!R*Ebxsm zpZ}ulm0zjoF-A*FHX4(H$rrZw$+mFcMUYWxY9dR6l5F8p_+C)yjM-Elw$=H}CLnw+ z@sn7h|8<2>iM2~z(>f(HP8VbRb|p56cv+?^6pU$?PdZRE+<6mmVPp;hSHgp zEawe6n26zcuT7w5&A8-HX@7jIw{YF_tP`ip#FRB(R&754;WwGd&y9hjQ)EzI{IC$o z0mE_6U?R@B1JwZ>MR(-I$Pm?rHx-$Z+KHwuhru z-P$I6gFxxRP#2+F($JNHR)$8u+&N4O0h-ryI4d1ddKdBs&jir8yHl^#tM)bySnpxs z*999(`h>|1Zyr$RE~+;{u5o1;tT)rqSM@Wuk2t^mRvOlb8mXX(u2rYm?@p)#eTctxnOJF_Kc=AOZ4&$MG`KN z9vHTZ5AZ^nZ;UO|dSSp^hCb@_f#9Q9FV&Hjo!tYhMET~YanQ(-S^nJm;&aWfXU00p zf%7(Vn!JNt;C9F?l(SB)c2wA28}C&}ofbq{J0&zz+#^**dqUhDCLJ!J! ziiUK}{n%!vV;AQ9G%?~&>xx+9?R{Eh#qGGQpyp0@Z5We&w)qOW)E&Y5&Pc3V?%S_IUx#y( zEm9r| zDY|r@DX@bxHz7#;+7q!(`A#5qV})JD8EW?=?3cgFx;BW?v38K>s*m3HALPv1d1N|I z{`M{<#wY0#BHX8!W((NEkc{sUXTvKbulU>)t9mZ8kvwo4NAfc3NT3Z|C8Z+z;S)EH z_y&Tp&cyY?H%$T09pm+vbgZE1tm&3YjE#@@!)yf7{0`a$%RgSI3wZjvaY=TeJOeMT z$ND_yKIe?XH#BStU%$5qQrS$7od-}Z^P~coU|-v|8~_S#NCu?BGZZ{x4nh3_D^ICn z({hhwXiex8s`otr04LT_Z46qjLfvN{wACunV0Eb&upSt4kgRjmFv>H58O`?v40ny7Zl(e*AN{rTD24gN&2Eu~| zFOsK5Vl!ABp7(z|T70G+&8lu>{<-cwhbz;vGYpnU&Y5)@h|9SHeE)C}oc>Tc97!xW z6K#CK*mZ|XsZHqTvsW4N9|OP>Bsdv)NQR{yYBDg0&-|x&52{C)%E^n<*s%2Ipja~i z;M%g4+K{JPok^TMR zfC27JAtcfoyQ*0x+@o4@f@h!Cz5-I{0Cv7jLt}L6O3w@RSd%|E0XVBeJP3#U?O)wo z12Emde#9UdxktPL+!J}5jV2Y-1%>xk6;>?tDeUOaua^BQGsFid3^$(WjDMT9u_y{8cF^`>(2-J6fv zH!5wWpaKAN6zohHp3$L9dbCJP7ay}XT0d1{WC_`ZbvG`+Qz5`Mm!mE%2U}_#%4=_TrkFn5S zi)kJT1pj|E04yRwDPc&)^3ThlM7sk}nO6GAH=>_9mZP(KsOxPVI}QF8${Q5pgQu?j zyacGLA2-`5Ui?@_x#qf_?`kr@Hw8+_!ZT0*6=y)1OKt}eI7Oar;YmS20j{veAb?v0 zU`zr~$ZInGFk=WVfYpYEx{LTBnY`ZcBdd$y?Q8=8q{X0!03?(D&*+eO6WS12 z2SdDf1jiyUsC8MxBkmahpa)k4;DV=#{2jFK_x8~pesp{R4vKvaVP z*D!%YqIeoT3DYn0w0jb$6M5P{3DjSC3OvFz`D>Vtc*>wV3DYm~lp%8xByiD=uK3@0 zLMIjL%a3^ai;4OT(=YP$OBU+S zdhgc^)REph;^|-e^sjyT+vkXX?bE;Z35t4-;5>3TQTEB;@SC{&v5ojy??K1F#xf{3 z?av;_f2;O>bIJba%o7Od*y;MukWfw)_!RN8Q}r99Uw(@C6>sYQ$vT0Mj-9Cg4C#oa ze}Zx1{M~u_&ybE-`X~5KAf#j8=|4mI!O}m#bpjzByH5WZ(hrvY0iF{G>DY5pIXsv; zN#OxFoJSh(AK*A~z*CujI8N{P3{OO)4{CQn8lC|^Nr&5#kLu0nEh+-QnRX;BwH{*N z1jtv#L#7VndI38m8@N~~f5GCv@ayWq@1f=#u3raVHURbx?lx9n`iB5p008{^00cmN zeh+^tfTw>d%m3c;?{ZLZOafPD^J`$GiR-oRV}kbVxA3DkaNM86PviXExagn^wD-Fp zi?x-j3z(y^ws&#;(G}POs$aApbH)gBu(CA=>lhrYe#R~Yo^Av5UwxP4wXw2y{+VGrh;TwGm_2|=|fHFviZ1XEP_W1rtoprc@`f>~5V1qkUmu)U}dpP(S0 zpnw2_y{(0Zkf_K}<@Y1SA^0{NkN{gLLaYG#!zqB~Dwr0G$ZMlNhuA@>(2%H$6+BR4 z-lhOIOTw^Op6O(&KiC0Z?*RaTlevo%n4+eFiX;}U&PVGEdGir0qXLDC>vs-Nc~lz^ z4oE-P7KDc41{HqD!#FH|Z)dx_K)7xyvO+d zf7gS*00HE|h*~R9_gP*CQ&d3lCjdrsH!D|jFg0?p{+@zafbowu2eebp=1xv1v+!Hs hu{|J$K#{}oXA3!`-Z{{cwC3^o7& literal 0 HcmV?d00001 diff --git a/citation-provenance-readiness/docs/demo.svg b/citation-provenance-readiness/docs/demo.svg new file mode 100644 index 0000000..0b1cf16 --- /dev/null +++ b/citation-provenance-readiness/docs/demo.svg @@ -0,0 +1,21 @@ + + + + + Citation provenance readiness + Protocol-Guided Perturbation Screening + + Status + hold + + Score + 22/100 + + Recommended citation + @nguyen2025 + Top reviewer action + Add at least one source-backed citation or mark the claim as internal preliminary evidence. + Missing bibliography + missing2026 + Digest 12a467b7c532f4c9b6e6c8a3 + diff --git a/citation-provenance-readiness/examples/project.json b/citation-provenance-readiness/examples/project.json new file mode 100644 index 0000000..066a0fb --- /dev/null +++ b/citation-provenance-readiness/examples/project.json @@ -0,0 +1,123 @@ +{ + "id": "scibase-protocol-screening-demo", + "title": "Protocol-Guided Perturbation Screening", + "manuscript": { + "sections": [ + { + "id": "abstract", + "title": "Abstract", + "text": "We describe a CRISPR perturbation screen for MAPK stress response signatures and a reproducible notebook workflow for follow-up analysis." + }, + { + "id": "methods", + "title": "Methods", + "text": "The analysis pipeline uses notebooks, a small synthetic dataset, and a protocol checklist to compare perturbation response clusters." + }, + { + "id": "results", + "title": "Results", + "text": "CRISPR perturbation reduced MAPK activation in the treated cohort. The workflow is fully reproducible across all reviewer machines. A secondary endpoint reached p < 0.049 in a small exploratory subset." + }, + { + "id": "limitations", + "title": "Limitations", + "text": "The dataset is synthetic for this demo and should be replaced with controlled-access evidence before external review." + } + ] + }, + "claims": [ + { + "id": "c1", + "sectionId": "results", + "text": "CRISPR perturbation reduced MAPK activation in the treated cohort.", + "citationKeys": ["doe2024"], + "evidenceIds": ["e1"], + "tags": ["CRISPR", "MAPK", "single-cell"] + }, + { + "id": "c2", + "sectionId": "results", + "text": "The workflow is fully reproducible across all reviewer machines.", + "citationKeys": [], + "evidenceIds": ["e2"], + "tags": ["reproducibility", "notebook", "workflow"] + }, + { + "id": "c3", + "sectionId": "results", + "text": "A secondary endpoint reached p < 0.049 in a small exploratory subset.", + "citationKeys": ["missing2026"], + "evidenceIds": [], + "tags": ["statistics", "exploratory", "endpoint"] + } + ], + "bibliography": [ + { + "key": "doe2024", + "title": "Single-cell perturbation screens for pathway response mapping", + "authors": ["Doe, J.", "Ibrahim, S."], + "year": 2024, + "venue": "Open Molecular Systems", + "doi": "10.5555/omols.2024.1001", + "url": "https://example.org/omols-2024-1001" + } + ], + "candidateCitations": [ + { + "key": "nguyen2025", + "title": "Reproducible notebook capsules for computational biology review", + "authors": ["Nguyen, A.", "Patel, R."], + "year": 2025, + "venue": "Journal of Open Reproducibility", + "doi": "10.5555/jor.2025.021", + "url": "https://example.org/jor-2025-021", + "tags": ["reproducibility", "notebook", "workflow", "review"], + "openAccess": true + }, + { + "key": "chen2023", + "title": "Confidence interval reporting for exploratory omics endpoints", + "authors": ["Chen, L.", "Morrison, T."], + "year": 2023, + "venue": "Statistical Methods in Biology", + "doi": "10.5555/smb.2023.44", + "url": "https://example.org/smb-2023-44", + "tags": ["statistics", "exploratory", "confidence interval", "endpoint"], + "openAccess": true + }, + { + "key": "alvarez2022", + "title": "MAPK pathway annotations in single-cell stress models", + "authors": ["Alvarez, M."], + "year": 2022, + "venue": "Cell Systems Notes", + "doi": "", + "url": "https://example.org/cell-systems-notes-mapk", + "tags": ["MAPK", "single-cell", "stress response"], + "openAccess": false + } + ], + "evidenceArtifacts": [ + { + "id": "e1", + "label": "Synthetic perturbation response table", + "type": "dataset", + "checksum": "sha256:2b5cf2c7b8a6d0e5e01b6a5dce0afac0", + "access": "reviewer-visible", + "supports": ["c1"] + }, + { + "id": "e2", + "label": "Notebook replay manifest", + "type": "notebook-manifest", + "checksum": "sha256:7c8e4b1b326b2a4d7efbf2a79d1bb0a5", + "access": "reviewer-visible", + "supports": ["c2"] + } + ], + "compliance": { + "dataAvailability": false, + "ethicsStatement": true, + "codeAvailability": false + } +} diff --git a/citation-provenance-readiness/package.json b/citation-provenance-readiness/package.json new file mode 100644 index 0000000..c4af289 --- /dev/null +++ b/citation-provenance-readiness/package.json @@ -0,0 +1,16 @@ +{ + "name": "citation-provenance-readiness", + "version": "1.0.0", + "description": "Dependency-free citation provenance readiness module for SCIBASE AI-assisted research tools.", + "type": "module", + "scripts": { + "check": "node --check src/citation-provenance-readiness.mjs && node --check tests/citation-provenance-readiness.test.mjs && node --check scripts/render-demo.mjs", + "test": "node --test tests/citation-provenance-readiness.test.mjs", + "demo": "node src/citation-provenance-readiness.mjs examples/project.json", + "render-demo": "node scripts/render-demo.mjs" + }, + "engines": { + "node": ">=20" + }, + "license": "MIT" +} diff --git a/citation-provenance-readiness/requirement-map.md b/citation-provenance-readiness/requirement-map.md new file mode 100644 index 0000000..28119f8 --- /dev/null +++ b/citation-provenance-readiness/requirement-map.md @@ -0,0 +1,34 @@ +# Requirement Map + +Issue `#13` asks for AI-assisted research tools at MVP level. This module covers a narrow, reviewer-ready slice of that workflow. + +## AI Paper Summarizer + +- Abstract-style summary: `buildSummaryModes().abstract` +- Executive summary: `buildSummaryModes().executive` +- Layperson explanation: `buildSummaryModes().layperson` +- Key findings and next actions: CLI output and `diagnostics[].action` + +## AI Peer Review Aid + +- Missing citation and bibliography checks: `buildClaimDiagnostics` +- Statistical context checks for p-value claims without confidence intervals: `buildClaimDiagnostics` +- Data/code/ethics availability checks: `buildProjectDiagnostics` +- Reviewer-ready actions: `diagnostics[].action` +- Stable audit digest: `digest` + +## AI Citation Tool + +- Citation candidate ranking: `rankCandidateCitations` +- Transparent reasons for each recommendation: `recommendedCitations[].reasons` +- APA and Nature formatting: `formatCitation` +- One-click style insertion text: `citationInsertions[].insertionText` +- Similar-source style tags: candidate and claim tag overlap scoring + +## Safety And Reviewability + +- No external API calls +- No live credentials, private data, payment data, or user identity data +- Synthetic demo data only +- Deterministic test coverage in `tests/citation-provenance-readiness.test.mjs` +- Demo artifact generated by `npm run render-demo` diff --git a/citation-provenance-readiness/scripts/render-demo.mjs b/citation-provenance-readiness/scripts/render-demo.mjs new file mode 100644 index 0000000..c251463 --- /dev/null +++ b/citation-provenance-readiness/scripts/render-demo.mjs @@ -0,0 +1,46 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import project from "../examples/project.json" with { type: "json" }; +import { buildCitationProvenanceReadiness } from "../src/citation-provenance-readiness.mjs"; + +const root = dirname(dirname(fileURLToPath(import.meta.url))); +const docsDir = join(root, "docs"); +mkdirSync(docsDir, { recursive: true }); + +const report = buildCitationProvenanceReadiness(project); +const topInsertion = report.citationInsertions[0]; +const topDiagnostic = report.diagnostics[0]; +const svg = ` + + + + Citation provenance readiness + ${escapeXml(report.title)} + + Status + ${escapeXml(report.readinessStatus)} + + Score + ${report.readinessScore}/100 + + Recommended citation + @${escapeXml(topInsertion.recommendedCitations[0].key)} + Top reviewer action + ${escapeXml(topDiagnostic.action)} + Missing bibliography + ${escapeXml(report.missingBibliography.join(", ") || "none")} + Digest ${report.digest.slice(0, 24)} + +`; + +writeFileSync(join(docsDir, "demo.svg"), svg); +console.log(`Wrote ${join(docsDir, "demo.svg")}`); + +function escapeXml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); +} diff --git a/citation-provenance-readiness/src/citation-provenance-readiness.mjs b/citation-provenance-readiness/src/citation-provenance-readiness.mjs new file mode 100644 index 0000000..d952576 --- /dev/null +++ b/citation-provenance-readiness/src/citation-provenance-readiness.mjs @@ -0,0 +1,404 @@ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +const STOP_WORDS = new Set([ + "about", + "after", + "against", + "between", + "because", + "before", + "could", + "during", + "every", + "fully", + "their", + "there", + "these", + "those", + "through", + "under", + "using", + "where", + "which", + "while", + "without" +]); + +export function buildCitationProvenanceReadiness(project) { + const normalized = normalizeProject(project); + const claims = normalized.claims.map((claim) => enrichClaim(claim, normalized)); + const bibliography = buildBibliographyIndex(normalized.bibliography); + const citedKeys = unique(claims.flatMap((claim) => claim.citationKeys)); + const missingBibliography = citedKeys.filter((key) => !bibliography.has(key)); + const unusedBibliography = normalized.bibliography + .map((entry) => entry.key) + .filter((key) => !citedKeys.includes(key)); + + const citationInsertions = claims + .filter((claim) => claim.citationKeys.length === 0 || claim.citationKeys.some((key) => !bibliography.has(key))) + .map((claim) => buildInsertionPlan(claim, normalized.candidateCitations)); + + const diagnostics = [ + ...buildClaimDiagnostics(claims, bibliography), + ...buildProjectDiagnostics(normalized) + ]; + + const severityCounts = countSeverities(diagnostics); + const readinessScore = Math.max( + 0, + 100 - severityCounts.high * 18 - severityCounts.medium * 8 - severityCounts.low * 3 + ); + + const readinessStatus = + severityCounts.high > 0 ? "hold" : severityCounts.medium > 0 ? "review-needed" : "ready"; + + const summaries = buildSummaryModes(normalized, claims, diagnostics); + const reportCore = { + projectId: normalized.id, + title: normalized.title, + readinessStatus, + readinessScore, + claimCount: claims.length, + evidenceArtifactCount: normalized.evidenceArtifacts.length, + citedKeys, + missingBibliography, + unusedBibliography, + diagnostics, + citationInsertions, + summaries + }; + + return { + ...reportCore, + digest: digest(reportCore) + }; +} + +export function normalizeProject(project) { + if (!project || typeof project !== "object") { + throw new TypeError("Project input must be an object."); + } + + return { + id: requiredString(project.id, "id"), + title: requiredString(project.title, "title"), + manuscript: { + sections: array(project.manuscript?.sections, "manuscript.sections").map((section) => ({ + id: requiredString(section.id, "section.id"), + title: requiredString(section.title, "section.title"), + text: requiredString(section.text, "section.text") + })) + }, + claims: array(project.claims, "claims").map((claim) => ({ + id: requiredString(claim.id, "claim.id"), + sectionId: requiredString(claim.sectionId, "claim.sectionId"), + text: requiredString(claim.text, "claim.text"), + citationKeys: array(claim.citationKeys ?? [], "claim.citationKeys"), + evidenceIds: array(claim.evidenceIds ?? [], "claim.evidenceIds"), + tags: array(claim.tags ?? [], "claim.tags") + })), + bibliography: array(project.bibliography ?? [], "bibliography").map((entry) => ({ + key: requiredString(entry.key, "bibliography.key"), + title: requiredString(entry.title, "bibliography.title"), + authors: array(entry.authors ?? [], "bibliography.authors"), + year: Number(entry.year), + venue: entry.venue ?? "", + doi: entry.doi ?? "", + url: entry.url ?? "" + })), + candidateCitations: array(project.candidateCitations ?? [], "candidateCitations").map((entry) => ({ + key: requiredString(entry.key, "candidateCitations.key"), + title: requiredString(entry.title, "candidateCitations.title"), + authors: array(entry.authors ?? [], "candidateCitations.authors"), + year: Number(entry.year), + venue: entry.venue ?? "", + doi: entry.doi ?? "", + url: entry.url ?? "", + tags: array(entry.tags ?? [], "candidateCitations.tags"), + openAccess: Boolean(entry.openAccess) + })), + evidenceArtifacts: array(project.evidenceArtifacts ?? [], "evidenceArtifacts").map((artifact) => ({ + id: requiredString(artifact.id, "evidenceArtifacts.id"), + label: requiredString(artifact.label, "evidenceArtifacts.label"), + type: requiredString(artifact.type, "evidenceArtifacts.type"), + checksum: artifact.checksum ?? "", + access: artifact.access ?? "unknown", + supports: array(artifact.supports ?? [], "evidenceArtifacts.supports") + })), + compliance: { + dataAvailability: Boolean(project.compliance?.dataAvailability), + ethicsStatement: Boolean(project.compliance?.ethicsStatement), + codeAvailability: Boolean(project.compliance?.codeAvailability) + } + }; +} + +export function rankCandidateCitations(claim, candidateCitations) { + const claimTerms = new Set([...extractTerms(claim.text), ...claim.tags.map(normalizeTerm)]); + + return candidateCitations + .map((candidate) => { + const candidateTerms = new Set([ + ...extractTerms(candidate.title), + ...candidate.tags.map(normalizeTerm), + ...extractTerms(candidate.venue) + ]); + const overlap = [...claimTerms].filter((term) => candidateTerms.has(term)); + const recencyBoost = Number.isFinite(candidate.year) ? Math.max(0, Math.min(8, candidate.year - 2018)) : 0; + const doiBoost = candidate.doi ? 4 : 0; + const openAccessBoost = candidate.openAccess ? 3 : 0; + const score = overlap.length * 8 + recencyBoost + doiBoost + openAccessBoost; + + return { + key: candidate.key, + title: candidate.title, + score, + reasons: [ + overlap.length ? `matches ${overlap.slice(0, 5).join(", ")}` : "low lexical overlap", + candidate.openAccess ? "open access" : "access needs review", + candidate.doi ? "has DOI" : "missing DOI" + ], + formatted: { + apa: formatCitation(candidate, "apa"), + nature: formatCitation(candidate, "nature") + } + }; + }) + .sort((a, b) => b.score - a.score || a.key.localeCompare(b.key)); +} + +export function formatCitation(entry, style = "apa") { + const authors = entry.authors?.length ? entry.authors.join(", ") : "Unknown"; + const year = Number.isFinite(entry.year) ? entry.year : "n.d."; + + if (style === "nature") { + return `${authors}. ${entry.title}. ${entry.venue || "Unpublished"} (${year}).`; + } + + const doi = entry.doi ? ` https://doi.org/${entry.doi}` : entry.url ? ` ${entry.url}` : ""; + return `${authors} (${year}). ${entry.title}. ${entry.venue || "Unpublished"}.${doi}`; +} + +function enrichClaim(claim, project) { + const section = project.manuscript.sections.find((candidate) => candidate.id === claim.sectionId); + const evidence = claim.evidenceIds + .map((id) => project.evidenceArtifacts.find((artifact) => artifact.id === id)) + .filter(Boolean); + + return { + ...claim, + sectionTitle: section?.title ?? "Unknown section", + evidence, + hasStatisticalClaim: /\bp\s*[<=>]\s*0?\.\d+|\bstatistically significant\b/i.test(claim.text), + hasConfidenceInterval: /\bconfidence interval\b|\bci\b|95%/i.test(claim.text) + }; +} + +function buildBibliographyIndex(entries) { + return new Map(entries.map((entry) => [entry.key, entry])); +} + +function buildInsertionPlan(claim, candidateCitations) { + const ranked = rankCandidateCitations(claim, candidateCitations).slice(0, 3); + const top = ranked[0] ?? null; + + return { + claimId: claim.id, + claimText: claim.text, + sectionTitle: claim.sectionTitle, + currentCitationKeys: claim.citationKeys, + recommendedCitations: ranked, + insertionText: top + ? `${claim.text} @${top.key}` + : claim.text, + reviewerNote: top + ? `Add @${top.key} or another source that supports the claim before release.` + : "No candidate citation scored above zero; request a domain reviewer source." + }; +} + +function buildClaimDiagnostics(claims, bibliography) { + const diagnostics = []; + + for (const claim of claims) { + if (claim.citationKeys.length === 0) { + diagnostics.push({ + id: `claim-${claim.id}-citation-gap`, + severity: "high", + claimId: claim.id, + area: "citation", + message: "Claim has no supporting citation key.", + action: "Add at least one source-backed citation or mark the claim as internal preliminary evidence." + }); + } + + for (const key of claim.citationKeys) { + if (!bibliography.has(key)) { + diagnostics.push({ + id: `claim-${claim.id}-missing-${key}`, + severity: "high", + claimId: claim.id, + area: "bibliography", + message: `Citation key @${key} is used but missing from the bibliography.`, + action: `Add a bibliography record for @${key} or replace it with a verified source.` + }); + } + } + + if (claim.evidence.length === 0) { + diagnostics.push({ + id: `claim-${claim.id}-evidence-gap`, + severity: "medium", + claimId: claim.id, + area: "evidence", + message: "Claim is not linked to a local evidence artifact.", + action: "Attach a dataset, notebook, protocol, or reviewer note with a stable checksum." + }); + } + + if (claim.hasStatisticalClaim && !claim.hasConfidenceInterval) { + diagnostics.push({ + id: `claim-${claim.id}-statistical-context`, + severity: "medium", + claimId: claim.id, + area: "statistics", + message: "Statistical claim mentions significance without confidence interval context.", + action: "Add effect size and confidence interval context before pre-review." + }); + } + } + + return diagnostics; +} + +function buildProjectDiagnostics(project) { + const diagnostics = []; + const fullText = project.manuscript.sections.map((section) => section.text).join("\n"); + + if (!project.compliance.dataAvailability && /dataset|data|cohort|sample/i.test(fullText)) { + diagnostics.push({ + id: "project-data-availability", + severity: "high", + area: "compliance", + message: "Manuscript discusses data but the data availability statement is not ready.", + action: "Add a data availability statement or explain controlled-access restrictions." + }); + } + + if (!project.compliance.codeAvailability && /notebook|pipeline|script|model/i.test(fullText)) { + diagnostics.push({ + id: "project-code-availability", + severity: "medium", + area: "compliance", + message: "Manuscript discusses executable work but code availability is not ready.", + action: "Publish or attach the relevant notebook, script, or reproducibility runbook." + }); + } + + if (/patient|human subject|clinical/i.test(fullText) && !project.compliance.ethicsStatement) { + diagnostics.push({ + id: "project-ethics-statement", + severity: "medium", + area: "compliance", + message: "Human-subject language appears without an ethics statement.", + action: "Add IRB, consent, or non-human-subject justification before external review." + }); + } + + return diagnostics; +} + +function buildSummaryModes(project, claims, diagnostics) { + const topTags = unique(claims.flatMap((claim) => claim.tags)).slice(0, 5); + const highCount = diagnostics.filter((diagnostic) => diagnostic.severity === "high").length; + const supportedClaims = claims.filter((claim) => claim.citationKeys.length > 0 && claim.evidence.length > 0).length; + + return { + abstract: `${project.title} was screened for citation provenance across ${claims.length} claims. ${supportedClaims} claims already link both citations and local evidence, while ${highCount} high-priority provenance gaps need review.`, + executive: `Readiness depends on closing ${highCount} high-priority gaps before release. Main topics checked: ${topTags.join(", ")}.`, + layperson: `This report checks whether important research claims have trustworthy sources and evidence files before the paper is shared.` + }; +} + +function countSeverities(diagnostics) { + return diagnostics.reduce( + (counts, diagnostic) => { + counts[diagnostic.severity] += 1; + return counts; + }, + { high: 0, medium: 0, low: 0 } + ); +} + +function extractTerms(text) { + return unique( + String(text) + .toLowerCase() + .replace(/[^a-z0-9\s-]/g, " ") + .split(/\s+/) + .map(normalizeTerm) + .filter((term) => term.length > 4 && !STOP_WORDS.has(term)) + ); +} + +function normalizeTerm(value) { + return String(value).toLowerCase().replace(/[^a-z0-9-]/g, ""); +} + +function digest(value) { + return createHash("sha256").update(stableStringify(value)).digest("hex"); +} + +function stableStringify(value) { + if (Array.isArray(value)) { + return `[${value.map(stableStringify).join(",")}]`; + } + + if (value && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`) + .join(",")}}`; + } + + return JSON.stringify(value); +} + +function unique(values) { + return [...new Set(values.filter((value) => value !== undefined && value !== null && value !== ""))]; +} + +function requiredString(value, fieldName) { + if (typeof value !== "string" || value.trim() === "") { + throw new TypeError(`${fieldName} must be a non-empty string.`); + } + + return value; +} + +function array(value, fieldName) { + if (!Array.isArray(value)) { + throw new TypeError(`${fieldName} must be an array.`); + } + + return value; +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + const inputPath = process.argv[2] ?? new URL("../examples/project.json", import.meta.url); + const project = JSON.parse(readFileSync(inputPath, "utf8")); + const report = buildCitationProvenanceReadiness(project); + const topInsertion = report.citationInsertions[0]; + const topDiagnostic = report.diagnostics[0]; + + console.log(`Project: ${report.title}`); + console.log(`Status: ${report.readinessStatus}`); + console.log(`Readiness score: ${report.readinessScore}`); + console.log(`Claims reviewed: ${report.claimCount}`); + console.log(`Missing bibliography keys: ${report.missingBibliography.join(", ") || "none"}`); + console.log(`Top citation insertion: ${topInsertion?.recommendedCitations[0]?.key ?? "none"}`); + console.log(`Top reviewer action: ${topDiagnostic?.action ?? "No action needed."}`); + console.log(`Digest: ${report.digest}`); +} diff --git a/citation-provenance-readiness/tests/citation-provenance-readiness.test.mjs b/citation-provenance-readiness/tests/citation-provenance-readiness.test.mjs new file mode 100644 index 0000000..6ab11ce --- /dev/null +++ b/citation-provenance-readiness/tests/citation-provenance-readiness.test.mjs @@ -0,0 +1,49 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + buildCitationProvenanceReadiness, + formatCitation, + normalizeProject, + rankCandidateCitations +} from "../src/citation-provenance-readiness.mjs"; +import sampleProject from "../examples/project.json" with { type: "json" }; + +describe("citation provenance readiness", () => { + it("builds a hold report when bibliography and citation coverage are incomplete", () => { + const report = buildCitationProvenanceReadiness(sampleProject); + + assert.equal(report.readinessStatus, "hold"); + assert.ok(report.readinessScore < 80); + assert.deepEqual(report.missingBibliography, ["missing2026"]); + assert.ok(report.diagnostics.some((diagnostic) => diagnostic.id === "claim-c2-citation-gap")); + assert.ok(report.diagnostics.some((diagnostic) => diagnostic.id === "claim-c3-statistical-context")); + }); + + it("ranks open citation candidates with explainable reasons", () => { + const project = normalizeProject(sampleProject); + const claim = project.claims.find((candidate) => candidate.id === "c2"); + const ranked = rankCandidateCitations(claim, project.candidateCitations); + + assert.equal(ranked[0].key, "nguyen2025"); + assert.ok(ranked[0].score > ranked[1].score); + assert.ok(ranked[0].reasons.some((reason) => reason.includes("open access"))); + }); + + it("produces stable summaries, insertions, and digest", () => { + const first = buildCitationProvenanceReadiness(sampleProject); + const second = buildCitationProvenanceReadiness(sampleProject); + + assert.equal(first.digest, second.digest); + assert.match(first.summaries.abstract, /citation provenance/); + assert.equal(first.citationInsertions[0].claimId, "c2"); + assert.match(first.citationInsertions[0].insertionText, /@nguyen2025/); + }); + + it("formats citation suggestions for reviewer-ready insertion plans", () => { + const project = normalizeProject(sampleProject); + const citation = project.candidateCitations.find((entry) => entry.key === "nguyen2025"); + + assert.match(formatCitation(citation, "apa"), /Nguyen, A\./); + assert.match(formatCitation(citation, "nature"), /Reproducible notebook capsules/); + }); +});