From f579912e54b4f2c3fc8e3cf57ff5b149b7a87155 Mon Sep 17 00:00:00 2001 From: tuanadr Date: Wed, 20 May 2026 18:20:56 +0700 Subject: [PATCH] Add knowledge graph measurement harmonization guard --- .../README.md | 13 ++ .../acceptance-notes.md | 6 + .../demo-output/curator-actions.md | 11 + .../demo-output/demo.mp4 | Bin 0 -> 46114 bytes .../demo-output/demo.svg | 12 + .../demo-output/harmonization-packet.json | 148 ++++++++++++ .../demo.js | 72 ++++++ .../index.js | 215 ++++++++++++++++++ .../requirements-map.md | 11 + .../sample-data.js | 134 +++++++++++ .../test.js | 64 ++++++ 11 files changed, 686 insertions(+) create mode 100644 knowledge-graph-measurement-harmonization-guard/README.md create mode 100644 knowledge-graph-measurement-harmonization-guard/acceptance-notes.md create mode 100644 knowledge-graph-measurement-harmonization-guard/demo-output/curator-actions.md create mode 100644 knowledge-graph-measurement-harmonization-guard/demo-output/demo.mp4 create mode 100644 knowledge-graph-measurement-harmonization-guard/demo-output/demo.svg create mode 100644 knowledge-graph-measurement-harmonization-guard/demo-output/harmonization-packet.json create mode 100644 knowledge-graph-measurement-harmonization-guard/demo.js create mode 100644 knowledge-graph-measurement-harmonization-guard/index.js create mode 100644 knowledge-graph-measurement-harmonization-guard/requirements-map.md create mode 100644 knowledge-graph-measurement-harmonization-guard/sample-data.js create mode 100644 knowledge-graph-measurement-harmonization-guard/test.js diff --git a/knowledge-graph-measurement-harmonization-guard/README.md b/knowledge-graph-measurement-harmonization-guard/README.md new file mode 100644 index 0000000..d492e1e --- /dev/null +++ b/knowledge-graph-measurement-harmonization-guard/README.md @@ -0,0 +1,13 @@ +# Knowledge Graph Measurement Harmonization Guard + +This self-contained slice reviews scientific measurement edges before the knowledge graph treats them as comparable. +It normalizes units, checks biological context, compares statistical endpoints, and blocks low-provenance recommendations. + +## Run locally + +```bash +node knowledge-graph-measurement-harmonization-guard/test.js +node knowledge-graph-measurement-harmonization-guard/demo.js +``` + +Demo outputs are written to `knowledge-graph-measurement-harmonization-guard/demo-output/`. diff --git a/knowledge-graph-measurement-harmonization-guard/acceptance-notes.md b/knowledge-graph-measurement-harmonization-guard/acceptance-notes.md new file mode 100644 index 0000000..e12f0bd --- /dev/null +++ b/knowledge-graph-measurement-harmonization-guard/acceptance-notes.md @@ -0,0 +1,6 @@ +# Acceptance Notes + +- The module is dependency-free and uses synthetic graph data only. +- Tests cover blocked graph edges, deterministic unit normalization, JSON-LD review output, and a ready graph path. +- Demo artifacts include JSON, Markdown, SVG, and MP4 output. +- The slice is distinct from prior #17 submissions around broad extractors, link audit, ontology drift, relationship conflict, author-affiliation disambiguation, artifact lineage, evidence freshness, reproducibility routes, visibility guards, and negative-result replication signals. diff --git a/knowledge-graph-measurement-harmonization-guard/demo-output/curator-actions.md b/knowledge-graph-measurement-harmonization-guard/demo-output/curator-actions.md new file mode 100644 index 0000000..5264f7e --- /dev/null +++ b/knowledge-graph-measurement-harmonization-guard/demo-output/curator-actions.md @@ -0,0 +1,11 @@ +# Measurement harmonization curator actions + +Graph: kg-cardio-measurement-review +Status: curation_required + +- edge-rat-human-pressure: Split the graph edge or add a cross-context translation model before recommending reuse. +- edge-low-provenance: Attach DOI, protocol, or dataset provenance before surfacing this recommendation. +- edge-mean-vs-median: Normalize the statistical endpoint or require curator approval for the comparison. +- edge-glucose-rfu: Add an ontology-backed conversion or mark the relationship as non-comparable. + +Audit digest: 6b18c9efd5c2126643baa9b6c35532fd305fc889aca712635386ff4dcc70a58b diff --git a/knowledge-graph-measurement-harmonization-guard/demo-output/demo.mp4 b/knowledge-graph-measurement-harmonization-guard/demo-output/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..a6eccd490ff104b76ab7b6fa4fd64ab60de20f4b GIT binary patch literal 46114 zcmYIv19T=o^mc9A?bf#2x3+EDwr$(CZQI;-%dOq5&9A@zr*meKtLNt4BxlY{k^uq& zA~JLFbg*=`w*>+M0s620xtR^!jG1g5Sebx;fS}BrOih7+Dxz&o3|)R~>LI|tzpJ-I z&w7v7CEC+y*NE0huC6_p*;$F`h>Y!>Oo^D-*@&E2n3-9K7+E0xT3|DQ*uAB4V>p`E!YfQg;R*uu%))=>Wk%0%SiWNKq$>HI@+dT^Q;yZjKwj2&hL(0eE`AK14F9t+b279wb^hrsBYg+YAKKCc!2ClSnix9# zmxhtPk)@&Ye~nl=nf^~=?xvRJ7A{6VI(r9GJAHF|hac_#7CQVutxY|Dd;^%-82=Bc zZ)<7y!xA|go7$NgySe~a8UG8@$?(5Ebux9f_<=ha>;J!W|I<4e1B{)_h-{61I`+S` zei#5V3q2!|1Rg#4C0S3L&u*9 z@ZWF(1pong{WXmU0pj`oI;eg@Tbs~vbWT@|#ag}}CEMf;nH{J34&0H|00IL2|Cb7` zq6m$Ane17?|Hxn~fM6}U29>Hfr_N$N_N2Nep;wy~Q1ZSuOJ4#fW)c}3PA9KH zdlxiwiTvenqh+sp-yQ}@g}OK7Pdmb4C~MZ5 z2ZEcg`d=<{?>Iuy?Dur>70x^Ok<{9*#~WnA;eJiQ=PVVGKS9HU%a9=31yqi1>?qxN zE4}cA^l*H5-J|0FlKF>6X@jc{I-|Ty$Ra!?AKzjuAcrlQPtgqa;-4oU&8?ev0FihF z9*|O>QBQ(l{JTd$h&{WRClXt(a2DXt zVutbLrm?iei-s~}O`f^-sUSu_e_4jd(r;sE-mneszpYyb+=+iWUR+VdeT}!O_>N?o zb;qiXNRCM4ha!BI%u3^n6dGeRoeqx%UDEE`GklJufMtQSWHKDERX~K6ykhEi`Y2k2 z>FW4#!%4+YPMvzC!e+%5l@i|7DRDZi^2Zl320WQ?1dc$oCGO1X@$EMHFe|m;d zc=&0qSgW5anf!XMiso|2jbKZbMQ8Q}U1F-_5aXt{GYfxbwh98zH3aN}Km-(gMOIAK z^UTtAX1IX1Uvoe|v#rCMnFkyT$&Bde`+6dRbjuvxy13|3tPa@)p*63zq=NdIsZRe@ z>JJV6&`iuLKmK`R6coLpkfC~3s_~fuu}F)+I(3*{1|=4W-is?#e=F&|5D48;ZqpjYw|crhHsW&)SDG9G#z;z$v;G|p^6M#d&AwSFViW&uGXg*4g1nJN(e^erI-41PRkjcD)63~0{?gZV8STTZ~s z2**S6flcO*vFCTZ{_8?u>vT)Mjq_H z%K8h9{p9`ec&?j3oOJ0=taVv3d>(;^Xc&V*X+Sb|}4;GgLQkN28;kUACjRg2NwRsI|u69+Y+6rWNtd{ko*tn%PA;_1S zQ*N1uuFwtAyWXcM1$q>o#r!FLhUyClw#y}jrc^PK3Gtt4RL%*ftMONn0-=ru+DmkK zUxKTKmWsz*A7=U8{T=)Rd&$;6PJc86n^Qhb%8kX^8D#(w+@W98NNzo~_aiJm^x(CrU*3Oxf1q@;R^0O$UM}{swJ&O6 zZrdvONY#~nbqPT0m8-a=R-e2#6H5$Ly_(5FY~_m@HyX-LSy;E>s!XZ$oa;lp@s*gJ zeQvd?ZB9Px+b!QnjiTzT)lO_b+3T0Z?OWnUlv6DxIR_(0`RWaVanYJL-7I?uOb{W;KH;w{n&%Ow zFB?i{W|1MSZYZbGPN!dO9K5CoN(>!30c`lw>o@jHVun=B&1DtWoXN+&ttV^xG7dwP z=7um@w6g9riD7_~75HRaGYnobNDaZ79d++TA^dr!8{Wj^+KT*&T?y*vu8(n1%mg`` z7C_-SnH_qcKOEL1T>l=n{-KW!i`oP$-2d~X0~s3OtRWvT*nQaI^Uzwi#8Z0e3%xjZ z;6rn*hz;yMeU4FLc}kpZUtvXe0v;;D5^Ht1(@#2Q$$E9@fx88DjXe#En{W(>VGH5O z3P%SpyDk$3HjNyv&$XAh{vFfKDC4st_|mI`=ueN4N3Nv3#PKTVD;Rmg_y=|$c9Q3J zNw@NVG{Rx@x=^zOX(9u!?sRtTS}*z^E;DTRuq*#N+_2uNL zE@IO{4>QqlTY!{H>O6b@jIRJDa)Xrne2Eh#8bYW?1*=bE0_jjkrA4ltn7<>%N_b`h zrzV@h-3V;zaBd&gqQZ3sStp?o(r->#-~b4ACGkukIS*yp^3FOr;bT2H6L7o8;J|e@%xj02W@x zv<9S2GADk+80H167h?*dMupaC6u*dzskzPTPab5MlNZz~twJ=b2ou-h!(N{8e6){8 za@Gw8q<<7*CMs0tu=B#wWDX)k|4MAH?w{g<{K6F+%xM@*w4tLvtECd*F>|s|mFj!= zK2PxAqmj|wrBom~BwW6Ob*WxkT?>_IyD>Oz<$1{8OQ8cUl){UIlSL2WD-x7{UNFQi zrV;+~Di*RBX2DK(ZUJUksMb-0yfhDex9BiYx>xV)%FK84;9h3R#lK)5D^7wu`IhPD z41A-b=VuMHXGY>3Pw0>ouyycZnDlN|;S(9djKX<^0uGQmC>7mD(-+PCYNv!%%Lqgw z%)gx8^ZW!mI`Sp4*_(YXe-GF})%fk|(H>JmpUB;ephneMQHLu5>8u_m@atBqGZ!9V zE-NAm0=|jQ>JzzS#;P^X3~T0nemQQ*O}2nb&_DqJDOCYUufAz!=xL&EWjv;`>5@jn z%YizIAh;z>klbz0r$N&U9`I=Uz^t<8p7Ta`W=cSXG@|jqmj4S}#xSd+86R~o>Shge z%x<^}jPg6KII$b_mT;BJ!#`O+2&`anwCwS4OOK`FjA%C2Osz5x<4-E6moJ0lHv#dM zlu=Q+f8>Eo$IUq>m()K;cdVCA_q=r9PI_UGHNPT0{G=1j!54M>ewk5^Q_g8+%Z3wa zN{e+^Hrf~gbt_K|&7QClOSDq>WTk}>Gq4~U_9yP%2KX7Pn4!18k>koe%aARN4$Zb| z{0-1$jH>t-%QN7_UMa25f}}+?dmA;j$m)_Mk%lzaM>2oZAnnd zw`4d9sh3&fp9gx0{u#yeg2RwcZ)ct_deF>#l>RJC2haH(cen=qsT_Ea6zq@xasgj8 z>~L9f^asp|iErMe%f#z!EY||uJ|hvngLYy-VAYrxF&vqZxp5rK4Aq$jbAht&I>oxZ z**G2STe0bE3WiGv9U8RKK(nPYRI=?cc>jsgNW8e^FZ#=N zb$3MqU16H9v#@yKV?xdNlBT4M4)~wt=y3FQ_ zlG?1m?Yn%s_6LPNq?4BABW8ViL*jZ0*H;+-829kxO9v#o@U~`XGxq86A&rV=HZ9@j zdI@3(W(#~9MzIxjA>+jsv^Y|N%y9SSV<(sB?87U4sqbz!+{Up|@HDFp+m*+tL?Uzx z;9Eu_GaPwG8^G7g@iyynxD`rdVe$ld1o z!rl1%TKnXmoYc`{>g^!&QZmd1gR`KU!@2v5t}YEV#NraO?4o}+WZ0Dc0g>C<%vlnpC}>34M2?m`ZmXG^LQ` zeyfke=BH|V*$iKr>+<&2M@3Um5uPziVI+4Z^xMhRAL$>W@{*VgA|dk9J5&Di({tMq zEkh+`arNZSa|@0}Xmu@+=Rb9TN|X1oK}QXNOjO9@04weZDkl=#Gz}`Oa?_E!Fy}8} zK47tP$WHQ<%4@+}a*A|S<22gg4cuF=m6PxPl)5bz=3E_YmnCNxT*yk1IWp;=>0PTo zr4@V6K7OS(3M=U1Acb_V*J1T$(F)y{1-Jvwj*A&mN1F6;;A4D-%cf z{1P5K6Q^-9r~UHUD@d?Yt6kY1q|X?ns|*w+QM6MuBs#)#iabvc%}QATY)(f5hv*I> zQh+Sxf%@b};CT&bJ7?FfSDR>WaDRSJ-E$T-LZ9Yd^SJCpJ}c#}g-LEoeee>2WLy@n z3d-_H>j5^)7e9@K5MQ34bV2&OLmjV*U=J*<)iL|(OoTRP`D<#Po6JU$rtY>1QiVi( zNdhUK?DxMJc~iuP238N)=K12H=*kaWSZmKbv|)Lcb~dO!6YKzc1+;j|&xX8nXWalgW@4k-G<=*N8S!pxDfwzffNy=!6O*kg`mh?A{B zOrjcgg+|2$D=PwbakxdAQwrM59QA{ciQis?d*jjLriVK(g#gBE4Y?{huvrhGBn9;! zauB9wZDTWzD}XzkH6)n@JKhRO zHu)~v_q}$+t3xOhct)BsQrrtDr{A64$&PoRoo*7`0FE=xCpBoJv^3FP3LJR`q$ccE zM$MK%;vM?<-{mK*qnms(Vesb*NogL|vKIWWzeAX4<*Mn+Q32c4=-vPe*xx;NITcwD zFs+@6pA=%#H()!r=pe}T(8F_zd%s!-JrfJw9%~<_Q~bAX9zT-y%oSAdu%!wcE7eFQ zC6R#pcftwxDO#qEZNvTVZi^Z7*rn5XHseJuITmhgxsAy9V54*s$aD}nsh=Hzb1Xm}Yme@Y zIwee52{mOhQ|A7p05jjp1F!3iiUHxy(EI*_rPGfD?ISQ4O=yYnY9g-BGf;9OT|2u- zO&DhMdzC|p+y90UDYF`#{x^}|tKQtsqEfT~?vo|Ek9+9ZM%kf5q+UL%iZ=aesSkwl zxsqOl$0huy9X=&&#FOJ!yO?3;fLP{+?ORk*JFd}urM{|JLMZ4h zTpDGKJ}qN8_%#BH@hvKRen7VX7=Y8~4mZi$VY%D|A;BW1>3O%y2kkB8`f+_P$B7f#OU5ub4f3s? z(+MiIMy3w!x8%MXDoKbJ+dZ1r5ua^pN&m4|;^;4dnj^raiaQ1D%jY6hF6=%0leRQ( zL~eaCWInQe*_o#c?Q7H}>+uGu7txz^Z|4gcSk^(L&&#R^tQ)=?!kTo%1xG>0PMb}+ zZ-C}>k;qB%qHLoJHhSlx6YmfdXa@T!6E0Qd_#KcbOMV<38XLReIg;E=~(Ls55W7YT?H5rl|)l(nIr5N$O`2XS> zanXOwiKV$nr1w07Jn_s}%={7dQo7^=i49@J{_E9c?C0pC1sYF_>Ypx%(dFsjclgM@ ziNnoO)5OR_N8g?Zy`S&;$+N!`gUQs|#L)Knu{aai5vAOPl2L=ZOzzPR z!Uh4I@Om;WkM!JW%t7ZCrF4sH3;cQpq%MtW-J*L-!{CoF8;>(N>VvZ{Et`c+Dqo#j z)m&=?G?K*;rzig5{aD?SOSsGtn^T$fLDUv3m1SKQzvWI?;^!EpVbscW#Ec+aH`6bt z1+$IvR>eP~y7ja~aBms}lkiX@Gs@^Mi|#(6$5~nTv!-BAS_G$6gCf{mxpua&W$L9? z^Bm0CP5`6)Zie)3e>`fmW8r0nb>cKtqT2z+g*P^gXnih7Wjph7Q7kADot$=E%CK_) zB}*8TO@Q?Z)ayZ=uJiisn7GUx)zDBum=Oa}Ae)oSM4XDS=NHz|0D^u-GA$}=Z8mJl z1kF#srD>Y-R})JQ2Gf!%$>7_uV_MPQZDWhLe6(PM+EQeaKgEZWvvYSQ6li~#N7~-) zO8)N$6lIOxnx3LaKI?!kxG5>=e(-&4;9<^Jo$mgSFUTR@=nO0_8|jzM_af(dH6^Ek zKC|{cK`UXCb`ni0{X5i0`J1gjdv73Ly`w4dluvmFF$)G3|gD@*8sm8+zjwBQ`^v*iby6vctdLxJs&^JQn?1RsNC z$#*|){2hz~Z7;J0yhx$D7vQ|P~ghrC)UmVDS@ zjW<}r07Bx@eekvkj9H$~#1f<$U<2MA~nVfnBp04zx#V&gv$2xELFh0laKtKoc$13~P{7a+Hf zqhnv}NtDuMWDpL@x<<3a!s|4UK|u|qIogkHM7TKL)l?-zUPob8F31qO+B+8Lpef&$ z%oted<%rgJ_x$L)l0N^orZ7A1%LHExI@&021bxms*)!wRq!!}fKPcLtYPBYZH->l z@*BQiwh{`y4DiPV=F1e@pE#D_a+=N+9Y_lO1IBwWV z;Wu4i^Pj&(8;f6-+G1UX;&jL)@l~Hsbw~C~F;sv50-Mx$hbYn+>n>&=Jw9{nh})f%`EtN+@s8ei$-AE{(mB;)%!KR) zAd_;0*4K{a9o8GDf&VydF( z$+_-L#ml9Ajb+6XssEz>_Y4>|2N#s#CZ%bFQMlgQ$k!99SyoUN1Pi7~yborpgD}U-koi999;NG)N69OVl^huQI$s?v__x@ZBMd8k51q5O=w6 z467S)Zx8qV6@k#aV$(V@q6yk?$NAFC3z^nf`bCT5vtG^~sLuxL^Y1*g zE2cH4ugZD%cE^l1peBDmJT~Gg|6cx?EsQ_}QBY!U4%B?$NawP?*Z`q?==Sp6sX&ZN zL?_;J)}2|Sx_ZeL&{+2RnsibbDztLbJA|*vT(UERpm<&>YIc&rLWc`XL?`0CDyxMC zmImh)%e^Q3E%Yf(ekHtRpAn4lt|3U}t617tVzC;|;Bcc!+B_bR6}PVlW842!Ou{%@ zgEWonIX*iI0ueG&lMl8YT>c;iESW2H6NchL#p5%xIb)TeA;oP^@hxL=9wa4I^~DD*&u z7T^TyIQXUiV1`U7Mw9k#q(YMZiSMQ&mWZo+$s+v|A4ed3D6-N3fH8NRSro0zM{Ngh z&*CUiu;{4yp?WbqZGT(&d83z`HiM#IByGg0_ZTivBfi5kAU(~YNu_|AxM||orAa6u zBMir?YL*&E8IPCsyE!}$8S=@*_0wbC^P!pEf^Ced92maV;ogM6Xk57O1lmHn??Oh| z#}FlV#Q6Gi8!|{NcovfUeqc3l=goD#FJKPd ztt9ka4s7l{iR?J43fiRqJXZ&xDeV8~GiFa|%3R`3yZqo~hS{5M6jQ6aUhYYvb7M?E z-MqZ<(m+-fb?(&b8qvV`&27?%+aF`#d97woAy3L%9lGZ=)<%Ye-9M&T9BfA*pI1&@ z0Ihmc%+j)aW)$l6RhkCc?B+|B99otOlW&qWIv7$1(4#tQ2L2?woxGv)`UC+xIzBjpMiduc(#0*Op9HFjEa()~xX_q|#qZv-oii8pB10Q|F-(Hg(+_xHW)Ub?46_;_(tPSEFtw*f@ z2R#c7ff$1{7&EalQbl6sagq<~6Lr(lYWO?5^@LHDX~|EnpL!DI4y*X~~TaRvS8Ngq>p0U<~?U zye%o{7BIofhRR_(xHNL?MGiVCS)3}bQ8Dc<<|OOC`Orb&?X!3|r$}ukl@hEoRvk{5 zUrK$wc5Jik(1%gk{3ojoTIBfsX}4QqE*hGk>IA&?>!WTgFP%kP4zt!5T-a2|1PB3o zG)!?)a+dpum5cjoG#zEU|2(LcvlvD-Fv>e)+Ek#CyTcXgceb!saN(}0{v~?e?!x}7<;EIRXmyz_TB-Ov-CQd@fh;kaRL1|-Pb)}ej6LGICqCob9VgxhR}IVsyG zqP35+EC~|>9^s$&x+Bnq>nY7$L6~wJPmNhzte19Qk}S8^zh)Bm3BM;?r$W$m&*>QG zhlT=nh?^osG!vH!LZ)In^r={QOQSTwYu&Ds|o>y%h#L%g3ME z;|k0sWW}1$V!X3uab&-dX=|p>qV$z@xMy*2nvWL%Jk{a)^v=^;!NzlK|CaS&aoaGo&5hx}kDzH96Cx->D|xO!Wo%>7alvCLUEN>xifAM*`^KRqSjil~~!Rx^wGWV^uVyS@%<=@_mn-8^@} z{miP+uO5tF^+w}hpEl7o+#uE8#A5n6kmdDJE+qSqBuk8~d^7a~))NWaTx`3DFB+!I zAOs3ev*3cIPyy2Wgi`e)vSDJ7kfS(upvQ*WB6C2e^l=RzR4anMBH5b+lav*|9jM~< zMhUW?L+!E0CmL(UHX2x6hWKVpqtu)`m8tZK?FPsplK(li1P|pt(X~^RRB~#5rx1q0 zEr}bK#K5sCUWl(5H+V5SAI~7+ium4=@Gu9i^IX4&+d#-NIb-PCWh)ZWwzQHb0Z7uo zj?=*ePnZD*LeuTeS}TlywUs}|k-tY&{}LXib7O&yF}A!!<$}>kUjKDxZ{0mkm9eU` zwU2lr98;#u>UJ*@CDOVFQknrYyIP~js=^DSwaQ(rlB96S*%*==fftQC(58hvR0^t< zdQ62g89b6}HSN4xQlfS}3fBskBfxQVD14auKFrSWw}`DewOPdSHP@2z8RWHqSHlQs z^G08pmv$U}qjreapai0@?`c!ZLT zZ!Mz%l6GXU7gIm|Olp3t%>bhQ@GQB1k}w_SlW{B>q_}p?-gTNVbjH*dNC{NW>kx8t zm3Gz{ufGo4m@@fTm2`r-PpPd%Xz{*m3ER}vZ&78o#vyO zr6z@C6sS^YF>@hIUJDDb4vwud*@m_XXi+`O9dolZ66GNqjN0`CM#Yz2RyT=72d8@- zV29iS$`wQi_gvLFw9ClK*1#2jOh}L%+DE;*+=eGz&kZzY!y~rYz+>@K66E3}?yCG`&itMq3HX^jI}kBJ}~So>ny!_b7<%#0XHCHh_b> zDo;2GWOzmU?(|gTNtNfnBstT{P9w-Yp(Vc72;%bM*(dhl=hfe{a#-OL=nH({x zhB3W|yskJ>q&p4FwarvXQMbAX*Xd*+C1*+L*!{%>*4h_;USvN@Y!S=kGeI1aAjPsB zG*%WT3x?!8Pt`V)K#AAYjWRnofVWZ0;T5C%4V|{te||HmGcbE=R^(@DgUscZ6^EKk z;k@PWkNe+iS%oU$n2rxWF&C*AoxmnbfbRy{VFUK5QKlA2NGPuU(%!8(lMJkvYONRU z0u;vX*C!>sON}7y+Dz>Iyn(e`f1vE>8uI+%@`jnIS1%b;<#GV>)6{(rTVN(5_Fnx= z;)b~Fgh57R99|5gvfZDIXINeVJcr+;h7!ekPe$yARUdeicwfv!Db$EN+b1fWB47fF zENkk^x`~SG%bnN7lHbx7v;5l&-}9C=Q7yGj5qZn`)W$a)9ORjrcEe~l4 zUDoHHWc>XZ+M?Tl@k~*Z5i$pE=0!AkkiVIK8dKu*jBr&wPHa*t!!yLwnhUUdw&_tY?$c2QJ5(_HT) zZR|#mI2)&Ha_@Dx_*i+9)fHNcMAIa*ux5vUoGJ&4s>dfnTmlxLhsC?#Jn66?q-%wF zMU@i>D)>{Q6bOVDuIFdPMArZa2z^%g>V2*ak^s_yL}}RPBQ-(6oQgctO5^y*p4DrH zcpX0uwPyOUG_4-@g7fnG#h7C>%q@#4&C)>wW>H2G^dfkx92CMOB9y~OdpN@DfXVJC z4#gZj3FDb=Y>U63BK%q!3)w40+*%G$Ioh0INn4P3f(P}j!lP17^l;g9db*kkwS;n} z*411;Dv5o(-LT9!W3DITmM5ta9;dy0`th0txt@?swRv3hGR4kPtqv+;I7T`$T|+`$ zuywb9X{c_S5TNgZ2>T7WhKH_ZbW@OPp#BuP=%syg^n z3o1=|-7qU3bCzJOz60?)%DLqwJ&@aETnhZ?NOx`OOHnH!T5!j4RMX=j3l4^`I4~h=sX&!lljI-Xni;ehbyc>o(iCrw z51Sg-m(rE2pxQSrKM7187dHJ83$B-=PC^Up{=PKPTd^o74G2UENxmd5uA0$W=0R&=e8!0zEx1>^Ja;XRrMF<=ahYyPnVpG8=(Cyk zYU^qy1Z%-~i!bxE=Zw&Cpvb}2R|-}m^{=xNA!k%z@45W};rGYN8-YH^&Cz6AO;}ZW z1eyy!-^eEPIH)6=XGs-*;Nwq5tlg}thj?@u`E@Iwk=(^k`;JC+XJ)84Yp*3{8r!3T zS+U|u6u^+=1N8mwMDHMEZ{+3~;qvUv-r!2GNIR_~j+wP|N5w+SS43Sn`%9-_gh~8i z+NTS5j9c`0voDEJUIEn0Og#~@ims_7;91kt2?IWvip@TbyLCR_#?+t}*sqH`C`peJ z$IMZ|jPgP?TSBJX$~VEmXer-#q=j|Xm>J5NI$fX)WDdP249jd&myiLVqhdPGc!caJO%0 z%$s9nWz{_rgZ-vycma6ZhpvXQPVY$v(T)x{HRfuZ2{Xrf_tqWaCf}Ur+T-ET4#H2gUh2{^T8Jl@|`QjM7xAesN<;SCf25Cu*%vi@sJeyW= z>jm;1F&GRa=(st7&8bQ0a(K{*1OQO`=_~bcc#3^;!y|lw@^a#vE*;A!q2FG&@bHL{ zTGRizk!bjb`iR)Z18OCP%3A$5OK$AJSI z=e3{3z6DTR+Yj4o3X_|%BP)j3+11v}?>~5W1+wKhz2Ad%n*Xhx!GyY$OvNn9B~ zRkd*9m=nK&FOb(#ABQbDthG%R)vR&%hC~Xn$BmCpJ>|r)ZF*jX=v_L#2j`pwL4ktP z@OpL+1)9E)$)T``eiK9C1qBjkE0SR_Khwi|9aYO zeXUaOj0p)Mp*1(XO((Jf5+?AA{2s0>#8BLBZ~py_!SX<35EL1#)(p41e9;Fn`b9I{ zsXZ?z_(V;#evtBrN=8v1LYy=?LHezR2Oj}!k?QaBT8&LM(W`FRN0|XpP9!0!j}*hK zieEp&XZ<@qH~PCHdgt%+SQj{(g>6aC^t4H)LT}y&iJ^8t=T2P z&$i5BoSZSwu*ZR%%g2o& zFys&qgd)YPwH!T+8RZSUnv`l#s-Z6*64o%7XOb~AwXXVYe`HK3LnRIYE73WUr8Y@! zl+agpMPP#Tf~U?}2?er{-R)SEiq{iQ>neA58M=BmoU*SiN)U%@kQ_AnY-J>-n$>Dg z&IyOzhC%)_U(p`vt=;!o*BoqYsD-ScUwh$eOU= zc`zbszGZFqqBQB~SneETJ5@geut;bVk-$8$#HRREXdj=%Q2!VWPuaeE!0J zH)YX}8!BnVrTgivrU2IQ_s8USo()i0c`zYo+*BZYpVN%ZfrlKVbaG!61Za}5*pMK+ zdm4l5X&1P5c=aeJ8DK%g{n83^?dco2LU&!PTPZiT-eyZ1wqUxils2|>E>02R9i`01 zMcJTkyHmjti>IeYvs%jRGxIx^SPJDk2V;U*R}0IBKW)f{nqc3y5$_%7@NgsDg3Wd8 z-_`G57*4xn2ddl=olMGjA>U(gaAr-ts5X&mz@dEcsz(j4o ze&V6wcTX8Gh$gM}+-5G{_IAy4=w6kROX|!-{BtLNYUg@t%%Gsi$4$bBh!RF|eaI>+ zY(atVgsK$8Xa;6i848N_s02Bp{<3+}q_Ike(;;8zyRYWKO&pl-7vXt$ML+`#D@B%m z`%=|bfj7s@(W-XVnsj0I(M2@iS1bJi@1lE`@ZyrPw(kXGtzNX+!p%-8(2Xkrj%YHB zY8~&^#B?8x4I!>NY_Ni`H6Z6rBq}@*KEO=i)fQE(M=5>sm%I@g*_%|XmL*&M;0&tzWiG}20~{sPU}+M248Hn!47hvYQnewG@d}jI=U%r@Nh6IV7c;23!^Rjsuru+@ z;e6&s%g$0XV^AP!baaNnIz7j>aqv8z*&%EnbiTTsY zKjySDfgHQ}IHEPR-&f4H8kz4=#32Nbz~^NoxO@s(!U@scAHmgXGZ%A?Vf*>Ds^+E~5g~WE(^d&WsquLQs9XmM zEwi~e=IslcOb{IrIgO!C+qW=@H6-PF^yE z3_!jok`+Zbx|6)wqnx@?0ev<1(g*cCL4;-WrCs+O5+u4qh)L)%!Txo^)J;=e6q-C4 zG+^fiJ!k#9oi|1-roP>E9b;qXHi3BMK~z&Y3c5z^3WHg607wk>%3Jqq3;L2 z*a^(l`X~NA@p{m1A5x*V(YD4Py-Vb!ZtP?Dk-#zu92m=unK*yVpHdVwMr|5!d;6>3 zJ4gBhb@os*zw3o54=OHhWGDzzh@XI;t(%RO1flWdorhlBe_fkUgC?{WN8*K}f2W{1 zMhef;D@V8<5v;qG*x|OBBTgH^t*jCMj#yPXk1p0<^pP^|HY;pwb?|F*t%(SwOm@5x z$v`%KLB>RS$T3%5aeAMh4jb9y;`cx1gQwfyJp2vb?uQ1g86|!Fbuf}G3+3@@_g`kB z+G3`nvcFkVOASb_?WIel6aF2OF5?HnRIb<;Xmue3zx?kq30?UU9+?is6uxfrbFLH0w>f5dKk#2XEjPWnv(iF zBY3d7;HfC+8ru;iv63v}bs$J?DDg-k`(|m^Wq1 zA?ZZmNG7DYxbuV|H0yB*;(vG=1eVCt%Uc?kVvaugr8|bl-|W18Mfh~Y^Uij0(LBs$ zq>c1{W^m9x$do1S1{dA{Z|t=9WW&I6&fm)73b}|EEi0R$PYPNSIH;xHi`e9ljDA|u z3jf2uvEmO@Gp2{Njz`j*feV1P04#HOwTH^$wj-Xe5=X8|*81WlOW@J1uUVD0W&WDs z)+S&@OdP~Q>l`Llx4mQo5#$?rL;@EDYSNZkQMGWZ-^t=!^}gL(5C5KjDvS86Vtc(3 z{I}$Dsvw;$)QtWt&vH=`Si$Vo{jex2t%RJ7^D(%f@L(cDs$8RwhpYo`m^ud_Ntfa9 zCu!XU0zy&?WZ-H|;+7QhN!|D*2t8*X(P9fTU<>{k(Q-A5;KrM5x9>C{!?M!#%L}N& zHx0#(*H&~@SJQ*_iC*lM&dG;VZ+{I+hk4JI`j3eYIhfFjo-TNh94tky;F*BGg{(!oB3bgcWc5uF88eXW$NBmhVCwOR)u~j=v5uSp zr6sl>8B z$6d8<4cWkQ*n8ZA-O>DQQV4}L0#6eCCBnB=R&q{7%=5ik&f%_Q>)cJ!USl=5>?HnJ z%s6R3Z%eSI@+A=C_SbMwmBfE~nf&~rG4;d`vS_)fZ~cXiJ4&ArE#;5TI<@;a^zOL@ zmGSg>2>;7lCm?nAqQ9gU)f_D2uDbYUbt1mlqk6og4HP$_W>7cp7Z{i>xEk9G#y3D* zhAWyy4$!#Wy)bTtyE{$h;@6Q~(MKD1ZgJeZr!LiytHM?j2Xgu{Lx z>D<+|4`g`_Nr^Oo5xCILzH#e_y!kKS12P7qeKm?cO=F4hfB-TI$X<`kn%q!drVRkeDgD zX;a81V&Yw7FFOWn#P2LpgaDNx2+hCM(#-@V0X3?J;BBK%a}_~5NB2CyayxuIWc#Z& zp~e3~{2%eGk4{ZkGggfXn!dPDiEZ;jKyD}8^EK-LK_h;mwdF`<-;+}X8|_^{kksY1 zI-z9QBPS^KQbd~?BK|(_{wH+sJ~Zd-e?_CJIL>ag0%Lme-Bke4NRcUKf&bo8E<~iU zq_t+Hc7@`x{0jty?~oA0Gk?GK_ds$&K?;^Ax>GW(o=vnZu7X++U=G#bPk-fvZDlAC z1<%c@A%lqK!eWiLnG?Cts*Su;`z=Nrt@r>(D5W8e9MHLLh?Knq0^Or2X>;nh%;in1 zj9w~v;Esc{ELc2no#}3e-1cV^Hq&BHD~TK&I0b3LyO5%+O~3zW|Crb-$c#lst~224-$w2lF?oqlo2RtAeJQ9j(LJ$=j_Z zGVa&u`G%cj*ps)tE;NReRt0RFe5!3f%mqC!>%!Il$23#HT3R4FsD(%e9C+ugMHjyu z3%99lh91o4-JQTnpMY<2`9-s6W~xsunBq?4`i}F67jW_;+~Rwk{rRx}hf_Fe9MxGS zn%%TA|DD#j9DXB$OG&aOW^;GD?dcMxsM?KJf*ExS#eBa)j_TvtHf+Du;c5U z>ER`#g9M$PG-o)vZGHej0PYCRYnY0_kD%fS`uI`t8JhvYFkkBL*;3t~nPS)V{_C@f z;?I*;2f&?Ht{5O*AUyv!FpPK@JO88iGW2z}e>2*gX*%r9k@FzB-=+9#KQj_s5b-^`ob$Pkhfcvz#+yu6r*)%8Lai9Q7-VF&qD<21(K zsabbUSRsEw^I8u1`4#$kKqOvh-w2%bzfo$Swlc<8pVjY@N94P^h~#rbkK=xdfQxoc zZxX;=>LIihkLhC_HwGDgI5`xN+YnpJE49aCmRb8E?BQ<{z-v0MUeiuF-CvNF*FdAV#{Ee){&RbNuA$Sz7ij`Zk7UULZrGnA=+mDOOwi*clK-UIR1j$` zi%lj^*y0Zg76<v`NG^d5pb{qevbAiEB&QN zes}(IxN;gijER_)i4|uu)bl8jrJ+hMvz0VR5e$zuq7i<5IItnvW9Z|wEX%h1lCUO2 z#wXvUO6@ta&gGgBP@0r)+-MaDf!wQU0x){V{=Qw1)b8UM5OTj9vgdx zA#X2IYAKrh7~9qQF~9p62Ag`m8Fo(RrWW`#{f)R`#NjgPI!rcETC+~tTj5(I1@`Ie zdr~XYZZErA)!{>u-X@9erZUl!wR&{%G+!b;~s?dQ8`E;Xk^Fy!}C0B;lxu2Eb!zW8dY2rU>Ae5 zU=s+o=Jn`N4jMh3CNaHpAtx<=h6?PAhmK5Wsr^0ephdwhUUUY$r=`s}0p+0}+>gz| zJ%&O{j=zE}UnqfyRmo^O-6F6K^QN&H(AkkTDb+Bm*ra{?!P@H zyS%z=AO0>5tP(bS#6W@1qtG5%NDY(N&|I4{K_iUao@e>wcZe1?%4uoml*&jz=GI6n zu&+sHF8_V#JPKU7qf_59YR6Y|zK`)uoKSgkIn&gTW^|vA744YM9&-oqM z@9Rg4@U;0(&(>IX!aK_&UAY#|PWv^6@Ex@@P_Jj*`d@-^P%w_I;j>RDf#OuX1AUX> zr^ztp58x^3fpvlYEab(a9~l|P)GGJq8M#3@;=X1phQfJ z2!DPPJLPcKw>2p2x&ulKc3|og!(K?Td@+glP76!k-c^-!#^*L=%L4t^4d9$f-Jq=d z4(NpS0$OUUVYEpJdf3Y(C^8KQhtov>++`Co$}qI4U$*4B=x=}> z!*kNo4pxOfe9VFQI-fGkAH)c0Rz2guqYk~<5Le>>-?_UhjZFAPq9rPPd?-#KxL;9( zGSn*)np0k;iqbCr!dSl(5l)Yxds1KmOSi4#;VLoLw*M$>L&8vQ8tc zw!o^{FaWw*fLe@h)?hKDxm#gT|H%nGu)*d8O6-H=uWm9~D82LQn5wnp1v zt-W|tXb!+p3yz=g)f2P+f|`Zn4IS}`=>EUQ9GrSOr!NJ-Q9(6m18ZQN7G!3r)UC^_ zhf%3{^NH^nTDTgzRp7k5QSXz?J}2**Pj$yiR1S$Q?ceqf9gNK#0Cf=uHD^_@jf4MB z63B+T`SZjt^YE6$a@2|cW% z=2{a2tM4&tlfsEu6OhBH2*1!uFLdfpbq|asTj%MVv_p}??8});IicmUTJPQ?b>wZ7 zzL!eLG$^8Yvo-C#uv_wudLw^-!-Hx8So__zwL-paiwV4HyijlX{q1Ljw0D|MGf?0< zoB{T7n2sHrRW3q!7p-`3^d4E)6Dez=S+?_C1#1G`GqG9$+4zEK7=D3S*iKxa`7IAT zI)pn~TB?qF>4rH~XhVueV_VpME}= zEEVA`gp~*`H@OBA^4+h4-Edg~V|1}p|J3s4 zNywz64{|f&yjsDr@{z~L`JU(nBgc-RY5 ztJNgDW0=I(BD2rrGmJryRs{wWem{5{gLL#l3QDo-s7@&^)?-p2Jrwe0b3!&N-S ziMD@?iX&pOGn19#eut)A#*=9XTp|$EV-!lwjWSU^^RHznOODG-vSxM%vmgRWCa4E6 z7*#k5F2+oJro*z=OhhV>y zUR!SQ;Pc~9g&)%W6p!!c#KZ=}YY?F%D9^{+R94P2A8cKZh&)1Kf*d%TW_+)ORslYS z^L5ew==v%KB$SN3#$Z$R*?EbWb4g$aX)-5XNMUJ_N#a5uyPqVJ`3S4Ja;8%)Jz+JG4Z;+G4fvn^$Mu`Wo7!qA#~rS)TX`q75#ggI1pFIh{-7q=it}W&6?N zlI4bsJ)M@@!oci!b7B2iVVD(XbfU~qU;G5W5AB<;-bM$v&G&$&$gtT z^gDx58{(n1b=V)YEC$uU!S$0ofAp43S4@U`$Y)(VojD37AOMm8o5PGLiCnhxmuU&2 z+eXx&UbL^qozAQ@$$MDG@Q@A2I{||@=^~5r?sDCYNd#FI`>KmeMUh5RQr`R`ycsXi zb^vK4cQ(B30EyK2Xb@(A>L^)1=|#TY2Y>^(&&3L&A^d&$6TeU52@_wpLboTz+4;TC5q1Mw2PsDNQ4a65NslnPQW zdHi}wd$L<`)o$Ly=deQM_3tA2B&Ov-`*5H=SBh|KnC)DwCPx(F*)H z1nyk+abBNqgbK0@FY8zzLfHzTR%U>FiO{OAlr6d8!c4ENTA;AinO$3riZd*vsvNm6 z^SD>rAF}{^L6sFTQ=855@&uzDl&cPD6F~@OCj3BHtDyS#uj&k?OIecKijBeu%+)LS z*(kH9ju<-j*!dy&P8tJNK{*iU{{%|AVJxRX_MND_*Uvq)t`i)W-I*A2Q~0nVWW#*F zcVGlsxth#>>;q78-R*F!Nbylu!Vfxvj<9MKscodf!Hj^BfWor*`8KjHts6rRbiK9X z1r=e77jEodm|?& z7ynD3F8M|DtjvLFPx2nAy?Zz`TE=Q7QQ*9Gs;K%YNLFCbr@VNc0A86au}W^6AhzAU z=??!4``+fg+zbAvqNYVZWY^I?Yb$xxsX>+<5V#FY$^*GazxIsy{pUQCr34a zD^WmhEhcy3t_0Uxi9qK7o(N)-QJ8sHLC2_C0qVwe39Zk>C3HdTQdH1LXpbq36%##p zZiKopyH=)HN}NLg{XEfYQQIL>rM9jYLD|as_56ypJ73$p30ca5xB+q}C7W$kGGNN= z04M|v&@6loorqb|nVRMgNe0apFqQuTi=^>~vuMeN+Au9B%P4~~wvn4aC;$KjE59mq zA3|?N<>8}mKmZReqn;6O;5E`_dY>eWa@|}qZtKTn`B1@qPykwmBu+)9E}N~tQ=HIJ zQ_iiw3sk!l%;q8bCDruxL;0xT8P~d7P|WJ2MhasoUcrES(_)+};miO}<7+N`Ngf+4 z-Q06g{}k!>P`l5*l$aBDeCIIjI3qZs_i?SZ*9laDGrf@nl?T7fv2I!^b(fsNqF9BE zw7^5Gh!gBQCc3vbx6KU9L`v~Gh%W!Ue_T6It9o@7I`|bnL)pImAP?z0XbNxb8H~SD z&qUoiRo*A6azhb@H@9{(5i#j;4MNg*g&WXQ{YgSh@JKysYj-JeE=kZOKPtp`2{)c(NDSwB@=5B$a`-l%Axa z-m`2l@vCqT7X+P63=b`ZQll&_lIf&tBuOUnPVzlW!(z*Gf}tl(N3@gikviCKk^Sf3 z#>#xbFYK$DN55A%2tua^LsAdwiiHrSX#G8e^0|5_pof+*xJn2+$f!4I=@7?sER?cozL7SV2 zOGw-`faKpXXGKtE&J7RW!D_TN95WX6ZP{$t`u|#VI`&F#kf4-iAyPj zImR#6?i0a5u&X)h9@@e{xNGkX5qn?Gk(+RbxX7_?js)t#QYcgv&ntgJ-Jl-mZ>` zRGPOnl7N>Aw=L@@1J@Rsg!c$2mZB_p?KAFUU{HpOT}4pZ6Eo&`TdLv9)&y)HNw52R zqPU$zWK7rJWl#O6CJ(j_kj?1H0JeztMk1{W#EJjC*Py`MbGKv*P>{+7+NTkO0Dm>!ONFIC`4RJl* zg?&p17%tMYIv7eb+eWcV>Yf)|vu+@8vsYS45iZ`U8yP=sLq-|k9`m;WLGi91FAP_yBX6> z{lHC5O+9qxyI6E)Z!={M1%rNOw8PO_H-H?`%5*JhAdAu{iTecs?Ey&KSHo(o_GoMj z8_rmf_?Gps9ds#WQvC!VwEhVSa4yb%SWi)EDKC;{@VJb^HIiL@pr@w-`R-M~AiMzK4T7zNfnd-4B5H4EZOVy)+uC^;`f_r$**OfaAfZ6(8gAHg+kMbnb$Vm}>$`Q`;!vomR>gOH zV0&j^ChHk8u6mR{f^wlltzzObKkb~oK@*nr+>EexSEM)1;AV>*3YK}eEk>2pgqjR} z{4t}txlL2lXSb&;n~u;BnADfoI;C!+qJyvco&i}gvg

jyA^NhWGS~N032{y}MZj zbH0`kd!gQe=JFV*vU7`<&^(fvn4jL6SSa#@;qh|pDXuY!t@vQT4LY+Kp7PKmB~6wU zEoADs^wQv`Cs3}fB2WHj?Nt((`|mJpxm+vmKl2+n{56aGVM8sj=H45>WR1Cvpy)6Y zk{{MJu~g`n>DFbUuy$){xYf)!G#;PxPx0c}?K-Am4>;#(vEoBWj;Em;TU7#8XupL- z^bER2Fy+YZUb1cu6xy|l1Zkx-WEBD1JI6(`>4L|D?F%$WsgI{$|-C31LVsHMu-ph@Qt zacd@hQ)ON8@9x?dZ6{YH*yj-(f-2lT4Z@gDp`H#sAxy)Zi%5~hG&{e5%Bj~P4nx{W#hY< zCO`6*Pf4@3PQr+L)W0Mth-Zopf=e^{3bB)YOpuIwZ7O{$A_!_C3I)6x6ph;TJ|I!X zH`LIB)6dS64xXa8P^vVGXqCymSCQC1(bt=^yN{N?FQ0BiDQEr>1L2O=pi0A~Y4)x= ziV|~9KN7`Yzc$3iWeIMXja#I>XObh&KF#I~Is;3V5zpD(4{1v%;uX*6h9^UeKPyO~ z&jts3;h9$LT>G=J6nHX?!RkiF*0B_}DTJmL4(I9LR7A>Z%m953 zLO#Q-h1g+^ZGvyud^d4lc(xNCl9;GHdc=%2xQU;MJe0RBj-)fHCmY=*ZY2AH#tTVx z#dyYelqM2@JnLT_1)peP1)&}iq}-KI+-k)M3Q$T=mo2y(k^Cz=RIb=a<_(4!2d-2u zAdMDUYx3+OuLobbCbLBYRL>be`IYBd-!b}t(AM3?ZeC1$`&>_}OzT63 z=9U%I%Q3-`0V3J$)0FV9t@>TDn^(Yz>u8l^s# zwPM0nymddpVh&f|H74W=9_cLsyz)hbi})WA)qJf`Lzzw=1)cOL_BnhRTuZxLA12dJ zMH9bxYp1mpd9;E*8!7xS4EL}|6XZC8n6n#zn=wH!_T*+t>CJY6^_NzdM(&?KV3WkH zFF|voC$4hty?`R8b6n$3W`_{00*W@?*F+;WA>!?nI%O{LN5sIo-VR1KzKL00ln94gkg0yp>gwg=q=2g!O|LKig(v0vW zVc&H()(~fZx-DA`PD=c}wo{SKtr56cIG93l;UE*rKz3V=bh_u!eaP%GfB~Nvk)UiG z<-;*LGUW1|(<9-vXZBL-Y>xhg+C>f|$9v3x0009340>QDgU4?CVa?dMkY>0hU`#7l z>n&d%LY$v9D@F;H_kwTS4PLW1%6Pkko`{7c;JZ%j^C`~O(Qn2n|g$d2A# zAaD=G-gZtLtAWYAu(>sErMFO_HlWK}(feC>CD?%cy|B(ipAZT$h1J|Qe)oXb$D&WD3u#A1~$t88O1Qq2)tFWpL ztP*>F71Wgg-;mi@WL8TYx(lv{$wk99Q>qxrwv@>glpL~mj)ZoGsCW3}6a=P%bfoUI zsJfFtE*$-tf;L=;V3;igxqmI&CYNEvA}8I?_I+yCcjE5KTi_%##di$BToP|03kz-4 zQ7n4+RiE1Hb29$vMjXXuC`^@5z!Em%(fcA2K9Og)l>IePFgm^n*9DwYrh>9ES1m5U zYYlCq4up@_y^F??SZ%@DgK&%qVR)=rXmkIt=Xiv^^eFAE`pyV#!p4m?W^b(r)Knz_l7%jAJ2kSrbjH5 zQSZ5yKwxq?Xaa2nMO6w-BYhr>jj4F33eHa9xfO$0C{UNh7szPdLDv64ym z$yzL>;m8$tqvCmOW21}m*F~kVVE&kDLEv8x`TKn`P*ViNdR<~~@ztgZO2ls<2Q4n^ zL!|x!u%uXt@IV2S|1dw{M0Si|y^cW^uWA3bvBz*}H{^02M_5=u2`Z$`p^G%(jC8#k z^r#OlCpE8I)z5pk1P^|_se1rs9T$FC5onAytSH>ez#rL} z({v`vh<@+rhD*&O#3X?6Gv2)}%D3J%4o128mcJ}G1w0BHJgpRC`DYEhM{Rl};k{ho zo`W%MTDsN@6r6tVxL<;3sY65etW!I=WcvrMlYE0z@R~uB^&P26Z{eQPNO<9Bys!nw zx;JN@S48Qzy^gndl@EUV2)N!Q>m?p!Ygs)F-z!>gKVZGI&q|mhTHKMP;;qCP3Pmje zj}UmB|6gk8pfOL8ReB-H@e86hn|NN}7s3c@xgf3NLEX;gw3xlPr&$ZSE>uRj9|FA$E6!dh6gMgZuS$D*+0WY9cH zWPf)ZFe5yZMeCFt7biuBc7g&9dH|pTJ|c!a&txx#i@*Q?WfFh@6x?f+3dM&vX6xYd zlG7?kxEgkn{keLzE!eU5%KzCdDlh43m*%Zl(Iec%Z)H0WVc}OML%`rPla>bzlqH1F zPgP8_|2&ivdVe3?{nC9@>E0dKA(aUK$V^Lrs=*};6?=J|{0X1daHK4I@Lak#Fm4$Z zko&wXA%|{%UZJUQx9^sJFRtU1Z1gsGs^H{#V{!$Z+i!(4IB&F#l2vAL3nSj0kM6}) zf6V!B7B8F0V1==127Yk3oQt1zb@gdznmytO;Cj(3LIcyx#Gu}RTxt#M>hTYZja(X; zUxTE|m7MXDqXVSiq3H-j_YR0GT_G?`7Of#^U>G@*DVk!M=6-7BB?n>=N&(W6qN5zK ztU#sPsuxz5`0E4C+P;xNhTMU?7g{ZW8G&`wd|^%u40cuAF$hu1U?J)*k{^LMQdu!T zOAOfxg?-;OJk#i7gC_Qs0zmnjKn-Z<4vgxb`Lcp)UMtG+kYq54)f}l!NCqPUlImWT z%jy1JP4h00rY4o##UMc^bMn1bJS+ahpzTijB{M>N*P2oAkgY%aKf%7REdVbAEe?eF zHZf@dSdQ^XvHEjE;x~ZCjx7pl0lahI1}{Y9v7;pFdjt*Qm^DtzYm=DmLGk73&HhAuO0rmQg%_D=gTxEc%TX> zPMRM1;ZDGRPo(uud3_jQgF@3dgXt!QK)s|NwO3!$xu&QK2Ek*TXoP;IZ0`<$ zYOESHhdF5on%G2Q$T?|1D3WOM0uItR5TDS0n|MH%^;}~nAL(X%FF`S7~X z<~b8p01*Zz@RI`>mb-dfcwv#RQ*BYBWNe(qa#v-9b=WgT{XIqpio#* zeOp!hm0dCs#QXo2d2ZEA?UYfUJwTv8j5kW~6a79GL4cJP(Pdt87B)xi4M@ZQR(i~7 zg68^OhRNL@s>{>{k?SR#V$V2p^S+0=5*y+~6PuY6J5 zBp2{C*|A#K{Ryp2cZyxLBf}1in2v}s^=jUF>}E}o3?|Xk1Y7hI>Ubdmqy#6_{X}uR zW=+1YMre^*X!Qg|g6m)Nb8U2n0khc*qpavkpt#Qe#)r)5=~Hq#3?fWtf)+d8b~-WN ze63%3Fa}Y>EVNCoX`#ls3S5Rv)c0#yyPRPF00RJM)p7>b>LE9Djd$V))Ki7o6mDgh z(ZAB6BB4?zFhIV>q>-{2WR^fO3us88*z`FO!vC)8}L)b2>$@-p?XQ5 zxq{3m6*;K=Oo{d|6vM|GhEZ#~7J|P+Iu4nM|1nc;S8@Je2!bkM-52IBq)%^!VH?I< zZ+B12%TX4nisUv~=?>LN#}Y|jVmX#78xBGpB9MQ5$bwaWk_uT+2r)Q&+;Ab#6URg2 zLrJE`1S)(wPMAqWq7{*xhPepJzr$u9gUB8eJm~wG`@R;OteYq0DbPTg!iPS2AV}-R zvm_vD@N-DJ_TF2CdYxn%wjbG?aKY&)S{QX6sS#4V=tw-nj-YGG$4dG?GNVfZ-tYgp zxteM4`EHKQK`HHhla2B^y)P;_Z=L8sut1u3cI!Oit|8niG9cd{q8`ZhNdP46#VZ*% zB!&HpsIn@}YF;l&`6L(D$jORD`vMfwLaMS^dLZ)}w{tyCWrK*Wh(e`(ml)z(xqu8+ zF32q11XFyWbdPR+0!;n|2bfJZNaiB~uqNX0T6SY3P#%3!5c1_b?8nt*3!fhixXDd3 z`4i7OwCa~|Fkk|&hoaE$gq@`hU91_`W`2@60Mx$(zYCX z;hSFeAHa?tG(IeVp$%NWn-)5;Fv-)Dg@Z&#NT{&HJJZT)w^kF?bpPUz(tJ+;uVE3Y4(1PD0Ma+QdIDEGB8p>QMOdcu`NT%GD?? zfdO_xc=|3ZIPSjZ-j*!Up25DiI1j_E4A*T!nl8niD-K8NXWjE14BDZG3R%A-BQ0ia z{$^Ms%{)JURaPixSeY)@pHYD7=_TrSVht_5Di9CXJA`XHZ(^4e6}h~$ZBgj4Nu&t( zquJ&r1=a-LXBx8vHd?#3jrbcYS|O^qxK+NqC*vLZ{L#fogc=d26Xvb^<%fJa>|Zpxi8y`tyN`R>{2#d1>$HL(NO1g?2Na{HXHS>ls^=3Cs)sX&}pxr_zRvUo@^XR zef^WT`KG!N1+u5lp9)|uY~EgC{45W^`^BqgJzbk=;=K6p0+p0X2xBN5Hz87Z5K_ge zC~tp9V%c_>VxnB!@o1mE$JVAOSdRxu1viqK;&g~(83w9JdnoV+YV1;vT{S!ls2fSs z{PIDw=>k*gG)hWHU=sDl!0iyI-$YRb3-y0Nby0wbogArFpC+!$_8^i2IucG44<*tY zkI*(DWB|3)D5X7FzqFgjs_uqP6>sfTrWXp zD-(<>2lxyiXsnMG{_Qp(DjHYSx5aVAt3*MSKh1_3Bi?Fr)@q3=rHejaDq0r~w|yOr z?h%RNp|xL;LO@N1!HTrGg2aJsSNlKP)4)bE&F^%M2%(+PeS_r)MY7h*8QZFEn0~#b zAwnD1EEF(sCnMZ5BhD?v+4Eg@#~)EjOyFlUAV=20Ki7muBS(P=d=DHTme-Dl!n<;n z=g~q-PURP?DW_wZPOU)JU7>iUC<7q}P-^yxG&B{?h1G?|@FF z+*QQa18>c4cv|&?4;gwTWc%yPKXf0{2aGa>8a$1b+>dz@4`g>Wipt`xtI`~S(>gd_ z6nk+Y4eaec*?tiqqfE$OX6TR@*FZv#pD%zB32;fr!B{!eBeo{VPndKZ9g67+TQ(R9 z%?b$x>emI1tLAWc9hetY8VSNJSR7~H_w5?KrN~t4v2d2na|XE-sS9C z?^p5clXml}W6f?bQvRf^epr=CgLRXM$p|HqM{6zK~nX^FZ@E|MK|ElMW4H#qJXEEy7*fq6Wr^1z>x)KsA zJ`cY8A^8*txZ78!rr%(F`m`KKteMj|6K2RK@6WmYhz3K_1FY#1e7kOILP$MvFj0XZ zQ^h`uAjzuYxE2UNYjHHhl%m|}eZT`;f<*v3z13ES{%nw#$RMD`!1@Dh^@Q0j@M^qf zW|2)!*mj-6vocDx`WlPy~{F|uP(1oTpI<1rsM^mhhDmT^R`p6Zq zo_(?xxHV7JLKnccN~)*W?WIhggzj#I{@kc$mc2EjKd<(m@y0}h*kf)Lqt}YdZSTte zoOJt74{$A2p$0DfjU3sO;-;Ac_iKBLp9f+IPC7BoY!ZLEMU>4o8k3CsDU2ZbvmFU1 zb`CC+$8oo@L%F7QI#m7W02m+u8VXnd01>zV3orlx z0{{R60009300RI30{{R60009300RI30{{R6000930dr`pV97Wh1p66TXcc4QL+wpRnrYOlRr@_4Sh%X0a#b$eYwzIT!*fUnw)yPc zxBT7u3k+Cd_07i~t(v4jKt6L)_jIlJMhcnau@xsfNL7>XZzIhzzN4O-I9vfL5zPm? zZbL^^{e3=~TwFR_7T-CRX0)+0BbTqyocv=~OD)EzzN()9;1saE7fISjA-s#icDa5n-d#dhd8vIEAqN0i+3~y>$Oy5wtja|4sdX9xbymx+>4~@h`aQ3C zm15Q*Uqzpibm><&e1V;=^)IT}d>;=Mvbdiz99Xy8m17IX&Sw{rO)HH+D@Z1WNQpws zRqh8d#;=~ZW&3-Jbd`XudM_;~HEegRlE~rDoO|N4ysy4*w~k(YEDp=10#WGhwN0;b zYAX;w>4}Q{wZK>CHya(#`a8I3jSB?UFR;T4Go`IZj?_ym*yg5!gu*McKZUcdOXBnO zN9ls)3MiijStYtz2l4wJr;^av*+I=NXCc>?LW;ng(eb zvQGq(eb|5aPO+=$6V*f>W6=lDQiGmWW-iE6vFXZVT&}*Pjd$->r?K_5X_SX&UF|TE zY;=WQHX_%RP>ZhdZ8JV!sE$(|=a%UlWSOO7SGWl9qwKyr`_p5qYgVk|rc*`jI$8}D z9Idh4OfxvjKmg}AMSMVl!0kgLEiczh!I$Wu6e0%2ZD@d_S%Rrt{QUjg&m$Yx9kJ0B z4MG1!2O~T5eN{REg-mIZj%G`}Xk*B8Q{Vhh9~fCjGA<6fhjoDhZ1tv-Gy6!LMcoL% zLJ!J1+CdADZmK2~`kxrmYsT1Jt8=c_7$L|FmhODR^91-!V{qUffx%I|qVdtcODOfX z3mjmbt*{rpfdE|h+XO=Lcol9ZIR62z^8|yr7ZP5*j1xEqurN$KK8JP_>ZqZ;XKnqm z+Q2^|37mv~MLnDheT7P@PhF!|(xsvSgMAiRU+nYD*U+RbL)K+7G@tM9e>#R<%|7senoKfCel-iuse`?ieTHBk10Gruxu5pz=0Ox#lBfa8R&e z>^&@W`>!}<)ciBAtH%HC_%WUDYgAGqotPO;$@B}`@tBBlAi?&{?mr;k!?=x4Q|!)v znhGu}x1fp*WAv+F5|NP*7Q6#oqj>f)u*GGrr@G6cYtKC`CI{pt+t>o`HT}YDBtdx< z@qKv?J%w~-p&-HDEHic!+>^EvLxpH=fs2bkQtGwkD zj~r%Vc*c?C!>a>J#k=s6}DfsCU;`w4TlMCRg(VfT1G?z*>Ln$C4nM zI%D|KJyWkXc}w6s$u;)BFSNJJQO4!vFQb-susy<{eK8KSbu^Zdb?M(p+Qr!-gwVJl zu{YYkUcF0-C}r(ulG8hF%`8rDWxVgynf?1wVgSI98U%$-XF?b5dGI+$?HK^(l2}vR zSD}j-r5{@Rjd#kAq!^HDzhLPGv4mlK-MGpB*ur6ls<)T=5b^QT+t#6JPJA_+a@}Tz z#7pTP1U4+8sHSu80y`C*y1mBtqYBT4-OP=VD@+zHUE97lvi7eFh5u0AFjG6oOg69# zw@BA@sCoz9q0xX!WQjlteHkv&g57C4jNu!0?w_J1eyJ)f1Zsy^(-~3J{Ph9(ah=kQ zlQbZ88%}Go5Og|9ahoNcqw?@VF6P_KSKi0rXlKT5OlhO=yJFF(8f$mHFdfkCP$|JS zRZzw&Dwi7gc-G81ku#gGky2;-BH?A|)gvi!S-to*bwJ2d!31_Jy%v{dX`&m>ZI*Ld z)NJ=?*wnNB{adGdNjofb0rAqHTo+{@2^x`DD}ig}fc98dS9o_L)t<_(!#S8}y^@dv z*%>1(8{4+toB68@DksZ(!S_4S%^;!k4S^c+TCZ)O3!D8H&F~uJ9D!VqGJeSiM;k=` z6LN{V@-P;V(Dk=F6bbZ_{ot*!QENI$7;BgRifAu(^dAOYKO);hO`%8Bobo`QtJM2@ zxE6BlL-gPjo_~P%3&jnrYnf z;VQX!^e~TxntViu5I{bk0qJT|H)r8f19Q2Gi@bDJcDt;Vw&luM>?A`7loN|n9jl2r zjl!5%A~bCb7wK13@H>pFWE@aTbMk2@Kg&}=d_}u)gFej=p3jE>x{{K@*FK#q`5YMy zic2k`K9=n_oqVa>D7ZiU&Cv89g3Qx#wBc7}t{B(!Ql`uCzXAtP1(XSQFx zU_%1_rZReC>dq-4D7LOEz(jhcjn`!@%`Q6nb903hFL75*b;f9NTSp_u`-#i?R8?fkClsi1z zLNP`ptR({R)zc`45||O~1eg#F^g~_(e~7!HE>k^?xtW7T{q2&K&qcj9F6!EQUmbx^ z%3R$staKl!rZ_{etHooW$TW93q(YSNLC2~*FLX%eD>B3;EPp=u>rU%UF3W%&q>GNtx0Q77(QE74{fRBgZAcm0)bJ3AN8qfZ4UzoTn*j z?H{zOZ#N~KKgqy7HLoMI8prlaKR%pkl0GItJeC(STz}j}xb5*(A@O>p2j>tIv7HMHz&%h-}atN&9()aSd&^`w2(#TljW{ET~%uRiA#y`ERuE$PH75LXVjG=|N zYx2Nds&&9lwsnLQsfI$sxsQfAKPj=^0K<@RH(s?)cEG*-vDW2~TWTVX%;r+uYsHC{($^{XK^$RnDLPmo z=_fgjFDLOCfpQX)ud1*Hsw)OGSOT4AkX5|_ZK-R{x+QOWkpVbR0ra0ZRUT5uq)bM_ zGLAiQ-UsU;xLvE3hshuD%9V4^LZ|ppu`0LVRqa#RCXv!{4u|z%y2Ah8f+|Gs)7AK& zARQ5)yH$;sd!kO-AWd2EfW~faq=kU~RDmmsM3xn&|HI}+n>0lRxfp#-%6MbaTCd@K zp|uY3QjH4%%;w2CNhVc&I{M(-mkhZ9f*aW099<;U zHqlNO|AWL#+5W3of2jTH2yh2fZG+81Ut65@EFW4)gr2L>q`x{7kEkU({TUg}4GMHb z&^O1(u?3TM>sU2*Wf!hx<$f1&WKBZ3a?c)$R|*|y=r}Lp-!0ghMxWx)$Wj-A^S=ZF zYJxcdM8JJHz2wXRm4}0?B~n9yzK|@3^{W+$^nuks9q$;pgJm<}!(iIQKHzECzZ%#G zP)UH|sEKAbZT5_E`2pDoPM?rRN{Yu&f)sXldlT4aFynbWW}|cbU-mQZyJ_Oigf;~_ zJ0o52Ut}R9DND0c-Z1*m6^(o3GliYJ>6fupc^9ueypdZ4Amp9gZ{MRnBuc>Ff?@|W zNTO3=5-Bw=w=LN+Ibu(86-kLLe0?#T9$%1w6yA}u>Myryfv1I3%6OW#-t>{WG7D@D z%sdWShG4zvlrP1J;_%oTRTw1`vL%FTRVPNjjWV!`-ot+aYu2c0g8q_kAkL7LBOu&J zJBNPWO_QBlNYhl#e4a+|m881z6ZR5h5rz0khvy?Z(l~TnTYsOE!DxCLH+$?Vgp&ko z0q>zUt!0!vR>KrN4S1xNOAG{ObwUQX3d#&?Jm|!ZsLesTac_YzHCTn^4F^pw46TCv zZge?+zplt|DaQ1!bHQTL<~9cqG@~G^eOUOB0Zrp$%`T|^qMcv?iYy&1IvA>?C4c}+ z000tz0C|82c>n+b>Oq<$Y(u~Psg?i$1R-S90OoOJU16;4It1~~D!kHC_vgDuQ$l3X zCHgBx1r=Z#Gtkwto4zGlsKteN6<(n{vBw{t(p$DxD9PW zhf2Tc(iZO-vQ*G@ec2_N0OnJ&0F+%!DyPtSn7MWc+=KevX^dV?qc;pB;PK+oC_t zc5aXEb+u}T8~;9dBj5WTp>^)YBQCn*%CSMcl*d9TFuni6K4>b89--)V0>oDVZhgiX zaOUS-mZf+76}mC(7?e9U!|+O6i*G9sLR_PVx1q)&9AP80&gYTq8nQ*QM7+d>KyDIb z+cow{kd`&6c?GvJeRWpq0&|h9i~{!MX4&<0Gv1#uTDdW7wEphjJWm>n4%x<_9DLb& zfY6{IV*Puf{)k&DQLF(WuLyH%@s}`cw8T1oh+T9IG%=yxnKq0xpOyP;Pc9u-?T~rG zh2#%~S!68+khSArCM4p@1SJZ)jay~yhuo#WHLzJ+($YTid4(otA_34|Zw2apaSL8; zUF;O#i`Z_uj&7w<`w+fG z|EH=#$&dkVR4$b8nnOG~5h8_Rr``}4)$~kgC`ScD^AiNJKnsgkvw2zUSfsg?i$22+8afEH~~!_RMk=O=(vKs-9L&^|UdxNki4c0ELBoWjuY zP!B#rb<)hjU}xYCo45hS2iTcbkK*QOIDXLhU)~k0C=v7j3)G%fjH-5kK_V>T}eapqC#ywvRv#NN_ zWY>;m-8)*`_+J6|<2}dq+NFc2UE7uz$B+ds4H*Zz~I`L?GiQsuhSb{a}ZRTrBsa#xP`mb4r7v_+w|{#8I@N_sk`%mJJM(mF1k!` zAG;!OpUeqv4~HAc3+}KzxEgm~%@1xGJqCC8=LA>d?t8hxJ%XlJw2gdZ53a`DWBI|= zCi5RT!RHZqaSd@`#FJWCrHWHOhFnq-C^wy%jSCG*1S*Ay;ayS9|C30zGP znJV9GxEgmCqj;_1+R?j~+!cB^%e1oOuF$(#Ga1WQx2lBRwKSS7*KSJa-MJehQ^~9; zBB6Kfh$^9XaaLv~r$|%@y-TMXm)qZ4iz>0-wJoYrV!vx&REhnrnY7Y(L}I_Yc&ke6 zcjp~9n_5+3zq^Rj3~|o9pj9Q#nXPY4Ospz?5-HA^?T9MA0Tk!V+1G$3XI0t;P@FTH zYy)+xN}Mz2U{#58W*ZwoZdHl4V}4P|CVpEKbr zKbusa-n8Sr*A=<&izBlaH|+SMfv~S@AhS8lBJg1orrU||M~mf^Zc{`CJYxQj1`B1p->E@0>N0ng3#a`r|}AF6O-^J*6?+10vU5M8EZ9` z&5jo^R+NZ_Yh^ecg|MuWxticgPI_&Cy`wemJ4;G;5y9|!&0 zwjxp%3#vqo-L6)6UWF3&Q5kZSf9+5>;O6nd?U86NUKz53k!VC`2$gwPj(O=l4ml%D z^@EdOuSJwRIjMX-$~QIC*4Nk8*VVZL{@&q+COn#)b*>EFUb1^oszXxPEt!+-rapw@ z-?nx-CnR;9(T~IZ+p{1PP5(VTO%zH8<;WmH>InI=-e_1G<@75^lXFF) zDhDV|W!?dAC|(9Ip0edi0_OJz}xzJp6@Sfqt|nv#^S(n2Wm`FM!&;p0HlL(C7c zK7@SW>tG@IxYW2_Dj=J^^Z`zj%_O(g^)!XNr_~((gYVlR>!tHZ()W$<9{4`^N3{3@ z2%$>Z8u%7?O4q_aB+_C$uHt;?K-;iGydi|-Xs*}6%R@?3Mz}fXQ$s{qmC^cx#Tb?c j2gyL2b$9u>iCH}s4H3Z#MOG^lQ3f+?E6lK!WQF|$o=UL4 literal 0 HcmV?d00001 diff --git a/knowledge-graph-measurement-harmonization-guard/demo-output/demo.svg b/knowledge-graph-measurement-harmonization-guard/demo-output/demo.svg new file mode 100644 index 0000000..fbf2570 --- /dev/null +++ b/knowledge-graph-measurement-harmonization-guard/demo-output/demo.svg @@ -0,0 +1,12 @@ + + + + Knowledge Graph Measurement Harmonization + kg-cardio-measurement-review + 4 comparable-edge blockers + 1. edge-rat-human-pressure: biological_context_mismatch + 2. edge-low-provenance: provenance_confidence_low + 3. edge-mean-vs-median: statistical_endpoint_mismatch + 4. edge-glucose-rfu: unit_not_convertible + audit digest: 6b18c9efd5c2126643baa9b6... + diff --git a/knowledge-graph-measurement-harmonization-guard/demo-output/harmonization-packet.json b/knowledge-graph-measurement-harmonization-guard/demo-output/harmonization-packet.json new file mode 100644 index 0000000..66ecc78 --- /dev/null +++ b/knowledge-graph-measurement-harmonization-guard/demo-output/harmonization-packet.json @@ -0,0 +1,148 @@ +{ + "packetType": "knowledge-graph-measurement-harmonization-guard", + "graphId": "kg-cardio-measurement-review", + "overallStatus": "curation_required", + "edgeDecisions": [ + { + "edgeId": "edge-rat-pressure-pa", + "sourceMeasurementId": "rat-pressure-mmhg", + "targetMeasurementId": "rat-pressure-pa", + "decision": "allow_comparison", + "normalizedSource": { + "normalizedValue": 15998.64, + "normalizedUnit": "Pa", + "conversion": "mmHg_to_Pa" + }, + "normalizedTarget": { + "normalizedValue": 15999, + "normalizedUnit": "Pa", + "conversion": "identity" + } + }, + { + "edgeId": "edge-rat-human-pressure", + "sourceMeasurementId": "rat-pressure-mmhg", + "targetMeasurementId": "human-pressure-pa", + "decision": "suppress_recommendation", + "normalizedSource": { + "normalizedValue": 15998.64, + "normalizedUnit": "Pa", + "conversion": "mmHg_to_Pa" + }, + "normalizedTarget": { + "normalizedValue": 15999, + "normalizedUnit": "Pa", + "conversion": "identity" + } + }, + { + "edgeId": "edge-glucose-rfu", + "sourceMeasurementId": "glucose-mgdl", + "targetMeasurementId": "glucose-rfu", + "decision": "suppress_recommendation", + "normalizedSource": { + "normalizedValue": 5.27, + "normalizedUnit": "mmol/L", + "conversion": "mg_dL_to_mmol_L" + }, + "normalizedTarget": { + "normalizedValue": null, + "normalizedUnit": null, + "conversion": "unsupported" + } + }, + { + "edgeId": "edge-mean-vs-median", + "sourceMeasurementId": "yield-mean", + "targetMeasurementId": "yield-median", + "decision": "suppress_recommendation", + "normalizedSource": { + "normalizedValue": 81, + "normalizedUnit": "percent", + "conversion": "identity" + }, + "normalizedTarget": { + "normalizedValue": 79, + "normalizedUnit": "percent", + "conversion": "identity" + } + }, + { + "edgeId": "edge-low-provenance", + "sourceMeasurementId": "rat-pressure-mmhg", + "targetMeasurementId": "rat-pressure-pa", + "decision": "suppress_recommendation", + "normalizedSource": { + "normalizedValue": 15998.64, + "normalizedUnit": "Pa", + "conversion": "mmHg_to_Pa" + }, + "normalizedTarget": { + "normalizedValue": 15999, + "normalizedUnit": "Pa", + "conversion": "identity" + } + } + ], + "findings": [ + { + "edgeId": "edge-rat-human-pressure", + "code": "biological_context_mismatch", + "severity": "blocker", + "evidence": "Context mismatch in species.", + "curatorAction": "Split the graph edge or add a cross-context translation model before recommending reuse." + }, + { + "edgeId": "edge-low-provenance", + "code": "provenance_confidence_low", + "severity": "blocker", + "evidence": "Evidence confidence 0.42 is below 0.75.", + "curatorAction": "Attach DOI, protocol, or dataset provenance before surfacing this recommendation." + }, + { + "edgeId": "edge-mean-vs-median", + "code": "statistical_endpoint_mismatch", + "severity": "blocker", + "evidence": "mean cannot be compared directly with median.", + "curatorAction": "Normalize the statistical endpoint or require curator approval for the comparison." + }, + { + "edgeId": "edge-glucose-rfu", + "code": "unit_not_convertible", + "severity": "blocker", + "evidence": "mg/dL cannot be safely compared with RFU for glucose_concentration.", + "curatorAction": "Add an ontology-backed conversion or mark the relationship as non-comparable." + } + ], + "curatorActions": [ + { + "edgeId": "edge-rat-human-pressure", + "action": "Split the graph edge or add a cross-context translation model before recommending reuse.", + "evidence": "Context mismatch in species." + }, + { + "edgeId": "edge-low-provenance", + "action": "Attach DOI, protocol, or dataset provenance before surfacing this recommendation.", + "evidence": "Evidence confidence 0.42 is below 0.75." + }, + { + "edgeId": "edge-mean-vs-median", + "action": "Normalize the statistical endpoint or require curator approval for the comparison.", + "evidence": "mean cannot be compared directly with median." + }, + { + "edgeId": "edge-glucose-rfu", + "action": "Add an ontology-backed conversion or mark the relationship as non-comparable.", + "evidence": "mg/dL cannot be safely compared with RFU for glucose_concentration." + } + ], + "jsonLd": { + "@context": "https://schema.org", + "@type": "MeasurementHarmonizationReview", + "identifier": "kg-cardio-measurement-review", + "reviewStatus": "curation_required", + "measurementCount": 7, + "edgeCount": 5 + }, + "auditDigest": "6b18c9efd5c2126643baa9b6c35532fd305fc889aca712635386ff4dcc70a58b" +} diff --git a/knowledge-graph-measurement-harmonization-guard/demo.js b/knowledge-graph-measurement-harmonization-guard/demo.js new file mode 100644 index 0000000..6c3eb75 --- /dev/null +++ b/knowledge-graph-measurement-harmonization-guard/demo.js @@ -0,0 +1,72 @@ +const fs = require('fs'); +const path = require('path'); +const { execFileSync } = require('child_process'); +const { buildGraphHarmonizationPacket } = require('./index'); +const { sampleGraph } = require('./sample-data'); + +const outputDir = path.join(__dirname, 'demo-output'); +fs.mkdirSync(outputDir, { recursive: true }); + +const packet = buildGraphHarmonizationPacket(sampleGraph); + +fs.writeFileSync(path.join(outputDir, 'harmonization-packet.json'), `${JSON.stringify(packet, null, 2)}\n`); + +const rows = packet.findings + .map((finding, index) => `${index + 1}. ${finding.edgeId}: ${finding.code}`) + .join('\n '); + +fs.writeFileSync(path.join(outputDir, 'demo.svg'), ` + + + Knowledge Graph Measurement Harmonization + ${packet.graphId} + ${packet.findings.length} comparable-edge blockers + ${rows} + audit digest: ${packet.auditDigest.slice(0, 24)}... + +`); + +fs.writeFileSync(path.join(outputDir, 'curator-actions.md'), [ + '# Measurement harmonization curator actions', + '', + `Graph: ${packet.graphId}`, + `Status: ${packet.overallStatus}`, + '', + ...packet.curatorActions.map((item) => `- ${item.edgeId}: ${item.action}`), + '', + `Audit digest: ${packet.auditDigest}`, + '', +].join('\n')); + +function renderMp4() { + const videoPath = path.join(outputDir, 'demo.mp4'); + const font = 'C\\:/Windows/Fonts/arial.ttf'; + const escapeText = (value) => String(value).replace(/\\/g, '\\\\').replace(/:/g, '\\:').replace(/'/g, "\\'"); + const filters = [ + `drawtext=fontfile='${font}':text='${escapeText('KG Measurement Harmonization Guard')}':x=70:y=80:fontsize=42:fontcolor=white`, + `drawtext=fontfile='${font}':text='${escapeText(`${packet.findings.length} graph edge blockers found`)}':x=70:y=155:fontsize=32:fontcolor=0xffd166`, + ...packet.findings.map((finding, index) => + `drawtext=fontfile='${font}':text='${escapeText(`${finding.edgeId}: ${finding.code}`)}':x=90:y=${235 + index * 58}:fontsize=25:fontcolor=white`, + ), + `drawtext=fontfile='${font}':text='${escapeText(`audit ${packet.auditDigest.slice(0, 20)}...`)}':x=70:y=630:fontsize=24:fontcolor=0x93c5fd`, + ].join(','); + + execFileSync('ffmpeg', [ + '-y', + '-f', + 'lavfi', + '-i', + 'color=c=0x0f172a:s=1280x720:d=7', + '-vf', + filters, + '-c:v', + 'libx264', + '-pix_fmt', + 'yuv420p', + videoPath, + ], { stdio: 'inherit' }); +} + +renderMp4(); + +console.log(`Wrote demo artifacts to ${outputDir}`); diff --git a/knowledge-graph-measurement-harmonization-guard/index.js b/knowledge-graph-measurement-harmonization-guard/index.js new file mode 100644 index 0000000..6763d53 --- /dev/null +++ b/knowledge-graph-measurement-harmonization-guard/index.js @@ -0,0 +1,215 @@ +const crypto = require('crypto'); + +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 digest(value) { + return crypto.createHash('sha256').update(stableStringify(value)).digest('hex'); +} + +function round2(value) { + return Math.round(value * 100) / 100; +} + +function normalizeMeasurement(measurement) { + if (measurement.endpoint.includes('pressure')) { + if (measurement.unit === 'mmHg') { + return { + normalizedValue: round2(measurement.value * 133.322), + normalizedUnit: 'Pa', + conversion: 'mmHg_to_Pa', + }; + } + if (measurement.unit === 'kPa') { + return { + normalizedValue: round2(measurement.value * 1000), + normalizedUnit: 'Pa', + conversion: 'kPa_to_Pa', + }; + } + if (measurement.unit === 'Pa') { + return { + normalizedValue: round2(measurement.value), + normalizedUnit: 'Pa', + conversion: 'identity', + }; + } + } + + if (measurement.endpoint.includes('glucose')) { + if (measurement.unit === 'mg/dL') { + return { + normalizedValue: round2(measurement.value * 0.0555), + normalizedUnit: 'mmol/L', + conversion: 'mg_dL_to_mmol_L', + }; + } + if (measurement.unit === 'mmol/L') { + return { + normalizedValue: round2(measurement.value), + normalizedUnit: 'mmol/L', + conversion: 'identity', + }; + } + } + + if (measurement.unit === 'percent' || measurement.unit === 'ratio') { + return { + normalizedValue: round2(measurement.value), + normalizedUnit: measurement.unit, + conversion: 'identity', + }; + } + + return { + normalizedValue: null, + normalizedUnit: null, + conversion: 'unsupported', + }; +} + +function addFinding(findings, edge, code, evidence, curatorAction) { + findings.push({ + edgeId: edge.edgeId, + code, + severity: 'blocker', + evidence, + curatorAction, + }); +} + +function evaluateEdge(edge, measurementsById, graph) { + const source = measurementsById.get(edge.sourceMeasurementId); + const target = measurementsById.get(edge.targetMeasurementId); + const findings = []; + const normalizedSource = normalizeMeasurement(source); + const normalizedTarget = normalizeMeasurement(target); + + if (!normalizedSource.normalizedUnit || !normalizedTarget.normalizedUnit || normalizedSource.normalizedUnit !== normalizedTarget.normalizedUnit) { + addFinding( + findings, + edge, + 'unit_not_convertible', + `${source.unit} cannot be safely compared with ${target.unit} for ${source.endpoint}.`, + 'Add an ontology-backed conversion or mark the relationship as non-comparable.', + ); + } + + const contextFields = ['species', 'tissue', 'assayContext']; + const mismatchedContext = contextFields.filter((field) => source.context[field] !== target.context[field]); + if (mismatchedContext.length > 0) { + addFinding( + findings, + edge, + 'biological_context_mismatch', + `Context mismatch in ${mismatchedContext.join(', ')}.`, + 'Split the graph edge or add a cross-context translation model before recommending reuse.', + ); + } + + if (source.statistic !== target.statistic) { + addFinding( + findings, + edge, + 'statistical_endpoint_mismatch', + `${source.statistic} cannot be compared directly with ${target.statistic}.`, + 'Normalize the statistical endpoint or require curator approval for the comparison.', + ); + } + + if (edge.evidenceConfidence < graph.policy.minimumEvidenceConfidence) { + addFinding( + findings, + edge, + 'provenance_confidence_low', + `Evidence confidence ${edge.evidenceConfidence} is below ${graph.policy.minimumEvidenceConfidence}.`, + 'Attach DOI, protocol, or dataset provenance before surfacing this recommendation.', + ); + } + + return { + decision: { + edgeId: edge.edgeId, + sourceMeasurementId: source.measurementId, + targetMeasurementId: target.measurementId, + decision: findings.length > 0 ? 'suppress_recommendation' : 'allow_comparison', + normalizedSource, + normalizedTarget, + }, + findings, + }; +} + +function evaluateMeasurementHarmonization(graph) { + const measurementsById = new Map(graph.measurements.map((measurement) => [measurement.measurementId, measurement])); + const edgeDecisions = []; + const findings = []; + + for (const edge of graph.candidateEdges) { + const result = evaluateEdge(edge, measurementsById, graph); + edgeDecisions.push(result.decision); + findings.push(...result.findings); + } + + const sortedFindings = findings.sort((left, right) => left.code.localeCompare(right.code)); + + return { + graphId: graph.graphId, + overallStatus: sortedFindings.length > 0 ? 'curation_required' : 'ready', + summary: { + measurementsReviewed: graph.measurements.length, + edgesReviewed: graph.candidateEdges.length, + blockingFindings: sortedFindings.length, + allowedComparisons: edgeDecisions.filter((edge) => edge.decision === 'allow_comparison').length, + }, + edgeDecisions, + findings: sortedFindings, + }; +} + +function buildGraphHarmonizationPacket(graph) { + const review = evaluateMeasurementHarmonization(graph); + const packet = { + packetType: 'knowledge-graph-measurement-harmonization-guard', + graphId: review.graphId, + overallStatus: review.overallStatus, + edgeDecisions: review.edgeDecisions, + findings: review.findings, + curatorActions: review.findings.map((finding) => ({ + edgeId: finding.edgeId, + action: finding.curatorAction, + evidence: finding.evidence, + })), + jsonLd: { + '@context': 'https://schema.org', + '@type': 'MeasurementHarmonizationReview', + identifier: review.graphId, + reviewStatus: review.overallStatus, + measurementCount: review.summary.measurementsReviewed, + edgeCount: review.summary.edgesReviewed, + }, + }; + + return { + ...packet, + auditDigest: digest(packet), + }; +} + +module.exports = { + evaluateMeasurementHarmonization, + buildGraphHarmonizationPacket, + normalizeMeasurement, + stableStringify, + digest, +}; diff --git a/knowledge-graph-measurement-harmonization-guard/requirements-map.md b/knowledge-graph-measurement-harmonization-guard/requirements-map.md new file mode 100644 index 0000000..e0f0daf --- /dev/null +++ b/knowledge-graph-measurement-harmonization-guard/requirements-map.md @@ -0,0 +1,11 @@ +# Requirements Map + +Issue #17 asks for scientific knowledge graph integration across concepts, datasets, protocols, papers, authors, and recommendations. + +| Requirement area | Coverage in this slice | +| --- | --- | +| Entity normalization | Normalizes measurement units and endpoints before graph edge creation. | +| Relationship quality | Suppresses recommendations when biological context, units, statistics, or provenance are unsafe. | +| Evidence provenance | Enforces a minimum evidence-confidence threshold for comparable edges. | +| Curator workflow | Emits explicit curator actions for blocked graph recommendations. | +| Interoperability | Produces JSON-LD review metadata for entity pages and graph ingestion logs. | diff --git a/knowledge-graph-measurement-harmonization-guard/sample-data.js b/knowledge-graph-measurement-harmonization-guard/sample-data.js new file mode 100644 index 0000000..773026a --- /dev/null +++ b/knowledge-graph-measurement-harmonization-guard/sample-data.js @@ -0,0 +1,134 @@ +const sampleGraph = { + graphId: 'kg-cardio-measurement-review', + policy: { + minimumEvidenceConfidence: 0.75, + }, + measurements: [ + { + measurementId: 'rat-pressure-mmhg', + endpoint: 'systolic_pressure', + value: 120, + unit: 'mmHg', + statistic: 'mean', + context: { species: 'rat', tissue: 'artery', assayContext: 'ex-vivo' }, + }, + { + measurementId: 'rat-pressure-pa', + endpoint: 'systolic_pressure', + value: 15999, + unit: 'Pa', + statistic: 'mean', + context: { species: 'rat', tissue: 'artery', assayContext: 'ex-vivo' }, + }, + { + measurementId: 'human-pressure-pa', + endpoint: 'systolic_pressure', + value: 15999, + unit: 'Pa', + statistic: 'mean', + context: { species: 'human', tissue: 'artery', assayContext: 'ex-vivo' }, + }, + { + measurementId: 'glucose-mgdl', + endpoint: 'glucose_concentration', + value: 95, + unit: 'mg/dL', + statistic: 'mean', + context: { species: 'mouse', tissue: 'serum', assayContext: 'fasted' }, + }, + { + measurementId: 'glucose-rfu', + endpoint: 'glucose_concentration', + value: 4200, + unit: 'RFU', + statistic: 'mean', + context: { species: 'mouse', tissue: 'serum', assayContext: 'fasted' }, + }, + { + measurementId: 'yield-mean', + endpoint: 'reaction_yield', + value: 81, + unit: 'percent', + statistic: 'mean', + context: { species: 'n/a', tissue: 'catalyst-bed', assayContext: 'bench' }, + }, + { + measurementId: 'yield-median', + endpoint: 'reaction_yield', + value: 79, + unit: 'percent', + statistic: 'median', + context: { species: 'n/a', tissue: 'catalyst-bed', assayContext: 'bench' }, + }, + ], + candidateEdges: [ + { + edgeId: 'edge-rat-pressure-pa', + sourceMeasurementId: 'rat-pressure-mmhg', + targetMeasurementId: 'rat-pressure-pa', + evidenceConfidence: 0.94, + }, + { + edgeId: 'edge-rat-human-pressure', + sourceMeasurementId: 'rat-pressure-mmhg', + targetMeasurementId: 'human-pressure-pa', + evidenceConfidence: 0.92, + }, + { + edgeId: 'edge-glucose-rfu', + sourceMeasurementId: 'glucose-mgdl', + targetMeasurementId: 'glucose-rfu', + evidenceConfidence: 0.81, + }, + { + edgeId: 'edge-mean-vs-median', + sourceMeasurementId: 'yield-mean', + targetMeasurementId: 'yield-median', + evidenceConfidence: 0.85, + }, + { + edgeId: 'edge-low-provenance', + sourceMeasurementId: 'rat-pressure-mmhg', + targetMeasurementId: 'rat-pressure-pa', + evidenceConfidence: 0.42, + }, + ], +}; + +const harmonizedGraph = { + graphId: 'kg-ready-pressure-review', + policy: { + minimumEvidenceConfidence: 0.75, + }, + measurements: [ + { + measurementId: 'pressure-a', + endpoint: 'systolic_pressure', + value: 118, + unit: 'mmHg', + statistic: 'mean', + context: { species: 'rat', tissue: 'artery', assayContext: 'ex-vivo' }, + }, + { + measurementId: 'pressure-b', + endpoint: 'systolic_pressure', + value: 15730, + unit: 'Pa', + statistic: 'mean', + context: { species: 'rat', tissue: 'artery', assayContext: 'ex-vivo' }, + }, + ], + candidateEdges: [ + { + edgeId: 'edge-ready-pressure', + sourceMeasurementId: 'pressure-a', + targetMeasurementId: 'pressure-b', + evidenceConfidence: 0.91, + }, + ], +}; + +module.exports = { + sampleGraph, + harmonizedGraph, +}; diff --git a/knowledge-graph-measurement-harmonization-guard/test.js b/knowledge-graph-measurement-harmonization-guard/test.js new file mode 100644 index 0000000..7352135 --- /dev/null +++ b/knowledge-graph-measurement-harmonization-guard/test.js @@ -0,0 +1,64 @@ +const assert = require('assert'); +const { + evaluateMeasurementHarmonization, + buildGraphHarmonizationPacket, + normalizeMeasurement, +} = require('./index'); +const { sampleGraph, harmonizedGraph } = require('./sample-data'); + +function testMeasurementEdgesAreBlockedWhenNotComparable() { + const review = evaluateMeasurementHarmonization(sampleGraph); + + assert.equal(review.overallStatus, 'curation_required'); + assert.equal(review.summary.blockingFindings, 4); + assert.ok(review.findings.some((finding) => finding.code === 'biological_context_mismatch')); + assert.ok(review.findings.some((finding) => finding.code === 'unit_not_convertible')); + assert.ok(review.findings.some((finding) => finding.code === 'statistical_endpoint_mismatch')); + assert.ok(review.findings.some((finding) => finding.code === 'provenance_confidence_low')); + assert.equal(review.edgeDecisions.find((edge) => edge.edgeId === 'edge-rat-human-pressure').decision, 'suppress_recommendation'); + assert.equal(review.edgeDecisions.find((edge) => edge.edgeId === 'edge-rat-pressure-pa').decision, 'allow_comparison'); +} + +function testUnitNormalizationIsDeterministic() { + const normalized = normalizeMeasurement({ + value: 120, + unit: 'mmHg', + endpoint: 'systolic_pressure', + }); + + assert.equal(normalized.normalizedUnit, 'Pa'); + assert.equal(normalized.normalizedValue, 15998.64); + assert.equal(normalized.conversion, 'mmHg_to_Pa'); +} + +function testGraphPacketIncludesJsonLdAndCuratorActions() { + const packet = buildGraphHarmonizationPacket(sampleGraph); + + assert.match(packet.auditDigest, /^[a-f0-9]{64}$/); + assert.equal(packet.jsonLd['@type'], 'MeasurementHarmonizationReview'); + assert.equal(packet.curatorActions.length, 4); + assert.deepEqual( + packet.findings.map((finding) => finding.code), + [ + 'biological_context_mismatch', + 'provenance_confidence_low', + 'statistical_endpoint_mismatch', + 'unit_not_convertible', + ], + ); +} + +function testHarmonizedGraphAllowsRecommendations() { + const review = evaluateMeasurementHarmonization(harmonizedGraph); + + assert.equal(review.overallStatus, 'ready'); + assert.equal(review.summary.blockingFindings, 0); + assert.equal(review.edgeDecisions.every((edge) => edge.decision === 'allow_comparison'), true); +} + +testMeasurementEdgesAreBlockedWhenNotComparable(); +testUnitNormalizationIsDeterministic(); +testGraphPacketIncludesJsonLdAndCuratorActions(); +testHarmonizedGraphAllowsRecommendations(); + +console.log('knowledge-graph-measurement-harmonization-guard tests passed');