From 90e23cdb8c2dd9cf9e58ef453a06ea883c12eaa6 Mon Sep 17 00:00:00 2001 From: Seowoo Han Date: Wed, 20 May 2026 19:18:36 +0900 Subject: [PATCH] Add evidence anchored summarizer --- evidence-anchored-summarizer/README.md | 20 +++ .../acceptance-notes.md | 28 ++++ evidence-anchored-summarizer/demo.js | 56 +++++++ evidence-anchored-summarizer/demo.mp4 | Bin 0 -> 36905 bytes evidence-anchored-summarizer/demo.svg | 18 +++ evidence-anchored-summarizer/index.js | 144 ++++++++++++++++++ .../requirements-map.md | 14 ++ evidence-anchored-summarizer/test.js | 105 +++++++++++++ 8 files changed, 385 insertions(+) create mode 100644 evidence-anchored-summarizer/README.md create mode 100644 evidence-anchored-summarizer/acceptance-notes.md create mode 100644 evidence-anchored-summarizer/demo.js create mode 100644 evidence-anchored-summarizer/demo.mp4 create mode 100644 evidence-anchored-summarizer/demo.svg create mode 100644 evidence-anchored-summarizer/index.js create mode 100644 evidence-anchored-summarizer/requirements-map.md create mode 100644 evidence-anchored-summarizer/test.js diff --git a/evidence-anchored-summarizer/README.md b/evidence-anchored-summarizer/README.md new file mode 100644 index 0000000..e045985 --- /dev/null +++ b/evidence-anchored-summarizer/README.md @@ -0,0 +1,20 @@ +# Evidence-Anchored Summarizer + +This module covers the AI paper summarizer portion of SCIBASE issue #13. + +It turns claim/evidence inputs into abstract, executive, or layperson summaries while refusing to mark a summary ready if claims are not anchored to known evidence. This keeps AI-generated summaries useful without hiding provenance gaps. + +## What It Does + +- Supports `abstract`, `executive`, and `layperson` summary modes. +- Ranks objective, methods, results, findings, and limitations by evidence support. +- Keeps evidence IDs attached to every output bullet. +- Blocks summaries with missing or unanchored evidence. +- Emits implications, next steps, and a deterministic evidence digest. + +## Run + +```bash +node evidence-anchored-summarizer/test.js +node evidence-anchored-summarizer/demo.js +``` diff --git a/evidence-anchored-summarizer/acceptance-notes.md b/evidence-anchored-summarizer/acceptance-notes.md new file mode 100644 index 0000000..c17d302 --- /dev/null +++ b/evidence-anchored-summarizer/acceptance-notes.md @@ -0,0 +1,28 @@ +# Acceptance Notes + +## Review Scenarios + +1. Ready executive summary + - Objective, methods, and results claims are present. + - Every claim points to a known evidence anchor. + - The result is ready with ranked bullets and a stable digest. + +2. Layperson summary + - Technical terms such as p-values, regression, and confidence intervals are translated into plainer language. + - Evidence anchors remain attached to the output bullets. + +3. Blocked summary + - Unanchored claims and missing evidence IDs block readiness. + - Missing required sections are surfaced as warnings. + +## Validation + +```bash +node evidence-anchored-summarizer/test.js +node evidence-anchored-summarizer/demo.js +node --check evidence-anchored-summarizer/index.js +node --check evidence-anchored-summarizer/test.js +node --check evidence-anchored-summarizer/demo.js +``` + +The included `demo.mp4` is a five-second visual walkthrough of the evidence-anchored summary flow. diff --git a/evidence-anchored-summarizer/demo.js b/evidence-anchored-summarizer/demo.js new file mode 100644 index 0000000..2e79485 --- /dev/null +++ b/evidence-anchored-summarizer/demo.js @@ -0,0 +1,56 @@ +"use strict" + +const { buildSummary } = require("./index") + +const summary = buildSummary({ + sourceId: "preprint-42", + title: "Adaptive Sequencing for Rare Variant Detection", + mode: "layperson", + evidence: [ + { id: "ev-objective", source: "abstract", locator: "sentence 1" }, + { id: "ev-methods", source: "methods", locator: "paragraph 3" }, + { id: "ev-results", source: "results", locator: "figure 2" }, + { id: "ev-limits", source: "discussion", locator: "paragraph 5" }, + ], + claims: [ + { + id: "claim-objective", + section: "objective", + kind: "context", + text: "The study evaluates adaptive sequencing for rare variant detection.", + evidenceIds: ["ev-objective"], + }, + { + id: "claim-method", + section: "methods", + kind: "method", + text: "The pipeline compares targeted adaptive reads against baseline whole-genome sampling.", + evidenceIds: ["ev-methods"], + }, + { + id: "claim-result", + section: "results", + kind: "finding", + text: "Adaptive sequencing improves recall for low-frequency variants while reducing total reads.", + evidenceIds: ["ev-results"], + }, + { + id: "claim-limit", + section: "limitations", + kind: "limitation", + text: "The benchmark is limited to synthetic mixtures and needs external cohort validation.", + evidenceIds: ["ev-limits"], + }, + ], +}) + +console.log("Evidence-Anchored Summarizer Demo") +console.log("=================================") +console.log(`title: ${summary.title}`) +console.log(`mode: ${summary.mode}`) +console.log(`status: ${summary.status}`) +console.log(`headline: ${summary.headline}`) +console.log(`bullets: ${summary.bullets.length}`) +console.log(`top claim: ${summary.bullets[0].text}`) +console.log(`top evidence: ${summary.bullets[0].evidenceIds.join(", ")}`) +console.log(`digest: ${summary.evidenceDigest.slice(0, 16)}...`) diff --git a/evidence-anchored-summarizer/demo.mp4 b/evidence-anchored-summarizer/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..3a33b9bb95e78834d156d3c8b59232f7dbe9d892 GIT binary patch literal 36905 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 !*l4!d`&u4Ok5k4p;*2-ahkPQ(dNxwJI zv=wM2oSyNygR50qO>om*2?ijR6(NS2Ku>2Mc=0&!tMD)WRE702{+c`9pmp%B-lLyP zcS>_Pi+DhRS`4UKffn|<@B?W|Y>oK2e4lG~N3>bU2@ObTrpL%TidW9As*iQEn5=lm zkhyjTZHZNf1D|lrm3>NJMT;)pFss6`kyNSD3%*OG+$fAilj?Z*>X<6bdT~}@c_u-# zn4POS$$n!Y{ViTSF-P6489>8ZD|& zNm)dE&0RvR2t5XM3)P-^Kb7MdBGwW0&aPx1f;Y@;-j{Bf1|KiKD=m;gVxNQv&S`~i zam$At;at{l3y!m979yyTxj2;%8VJsFmz{J%hmFeuv-V+k$1Bq;cAt?E04AtknP3ph z6!CF*Gf8d>qP4S-bnbjZ4(wHd%5wb}7RK`AVLSRD&Uw{hr8%r8)fS#l;QejXn|UzI z2x&h@6DSX$K*9=CGZS}LhGaq$>CajWPMXWBkWZRL*g@_d3BNMa8HDPE2ktl|wxy^V zbcb98;8taQWy>o{%6pTBjvFf7m8YS4Rc_5hN$A zu->gZ+;Ox8`jKl`mUM*_2_7(7k6Q=*&UEncypPLU69=6#k-Zil%|m*h0CyqT*Sj%waUJ z*U>@zXwATHeKDC9JIZ=%QM8nV3R+QESr%9Lx|0R0BYg#D)&*m%?#*dQul$|S+ zV)5wSXO=#H%qlF_VmhKMp^*n{OfrS{{LP%hrfQHe;FfQy7ra^X1`qc^3suKF@-TDS zO_b>T76zJJ6;hVvMH@y0P)UYuv>v|UYjR#91Z)?-Y4W_igh9zmaG)geaQ3}KgvhOR zQMMD^gkw6KNENbwE4Zq2afAIfE84n@q&C%bBLf~u$QtJi5gi@63j`>u_#w)%R=<7)|6&+H{)#uM^^vdZBuwII6ec;USm*Xtal z!8ia&n5PA2Lwq{p4*}xveB`W`$t@HQWVFW1KThZYKXxdnKK25D1Thn^IAn^n%a8NN zTF4Uin@(HSd2^eadU3G3l%@2wOa^E?aRYuo^N#V& ztf5y2W>}#^sM&>$&#nPqhwrM3oZIl6ls8EL$ybdp1Z1bIDvbwkxsUclVVeC@jFaW` zew4|QU9^{JJtbw}OgTMZ9TeBRiAP`?mKUj;WnALRk5}huoMPpSmaB5UH58r03xS}? zaO6H&)9@YjK&?Jka)xIfBAsTb#i8*Vz^qJGErKz*AK#ku{Q=W5BgMVe-hmhq$}l0v zGG9|RWx8*Ck|JPNrd?+g83rV3?z#S29r%yTc;%l*s!o<&**aNsDgr z!>;q5L>H=Q-ncP=qc=!X1bKabQM26wg9kXsZpxv6qmV(pZ@;P?V{Mj8H0Ta~XG{77 zsf7WZ5$K9Gjag@ zg1dXe$2d!ehFr2jBV|jQQ|`|RZC7?*|yEF3fX#6g zR*GF@(*1R%)-!Y%eYbskV01{l_p&Gh=&S=Gl5$a)%m7!5>M*0x5)IFmq2l?0gSz87 z_48#P&^l0EJm35RzWRVrFhFf)mc*<}>pBtr5Z+TScgm~Z{y`&S)w7Yje@%V#1jGFM za1&j;wPX=eQK=(yy_W~b;bn@Se65WqVP%IK8M{CPW^6?=>*ub)WHHKpaolqYSEHN@ zO~8;xo6G9P3qMemg@HBD3TcIfslD}p5FfPcB5NUnbP{8+&?Z7M5(@Z5_Cw9IUQL4X zA3(D&3+(drk5m_q?RkD$!6S%}jZum!((SC4(qk4m%JamJ7YxT!O-@f`wg>cv(c~ zC0EO^Db7wwRRPJ3N}Iep_hxF!Htz_Kk{rF0u&qxqEDCjHOgzcefrk5p7RSr)#xpB` zv$@Gb_0og@?kGfAigsSx^_TlEZeZ|3*Y^D{{QIPM>YYHBq4`e$d&s0l){6!gq8Uwo z#e%Gzr;f0dm`1%~t;FN<fqv3rWt<7d0N|~lEk7gC<=aJfNark*x>~x5! za4>WX0Bf&;)1-+Jac*-hw)bc3G0Fw;=02F>qhr&+QtGQ1o;2C3x5tf{31-#uG=5W1 zPghnzOxydpKpKxUw#=A|NsWjMo0ecsn%HmKWsAEb&vD^ksnyHTg&wlqKfa+odWN^J zo`vyPdgV`?qwN0RTwa+08OF@RUI7#?Tdh;tNMs)WRDoFM}2K0aGjP% z5aA6M*MMxg<6I>TB}USe^op7Sy+$2LH9nFDk)Jx-E=;E#(>}x&TDWe&Gjoa+m3V+b zisYW&g-{u}?B9L;o+uaqnCanWjdHHS8Ar_y8wxA!Gc%MAj&05I9`?b?K~k%AR*_YT z^F+PXJ$inQ2M;l>tq)-hQlMOi=6Ey>71r1EQs&ha>&P%d4{i3ujaJxomoEq0phuhd zS2_sF>7&+$o-{+RD~E=ur7Rtbbz}k2c0x44JXAC)qtAX2q^^d?&4>jwi`%^hy*mA4 zB5h|zAcLM?o>tj$KAdo2Tyf|pM0Rmzy zlQA@EjwiyS(&)K5eQXP7L zX6yOq9nAf?SrIwi77TB!lff9;%Qzmp0~@*z zAwFeWXy7{>rf;UdbUNK(H3;B+=!X?0S zMKj62Wy$iy_!B%N+oMY$a6P*ZOo7}aZrxacm!m%K<0%x*u=*pM9b%$%S%~!$_N`5F zK_>rLZf@S;SvT7K?m#;Iy;=>l1(zc;<~9NC!Yc2lQYlyL?K~(N+Ag{p6v}kD8REKw zC-hGd(U>i`}Qn&d)Q&$rXS)tg|E_c2=a6*)XT^%ioTukBO z-D5e{^`YI6FB74@hT)>7)Gy1Ka%bT7yENK(qEYh{#5En=1S>p5ay`^&)mnxW*EQwf zcaeD&?cl286JqNMS6@G~gX(JAe$TG(>;wvE+>~q23*hqj!ZLETi`iI%8ojAQTY+&t zEFXuW^t4{sgjAZE&1$178FP%E&PzgKlNf4j2COBIR zWek22T2It0v8h^JD25FNf^aF5tgn;kxMv4tq9BLIRsu2`yizn(9^@D-&J<=g zf7&+WwdHhQ4UAv9^ywGroigZCtPfIidv(r;k-AU&O?zp8^d$Mf_6tL$p}f%ZK5&q< zSll^TfE;ewJhZYlddU|Aqf?xGsrYokg*FHt(z!;uP_Yd1=r0Cw0_|cXdBPLU{%N9m z3FEwI#}e8USdIMOJT>RsZal75-F8*HmcfswpJsNEb1ANIpN^Xh%volZSH4v|^+3|q zV8jS5pv-W)?6Q8!Bp6L86l0W9)q;X;>5#gDF%b`pq~J=OO69gYK{DlGhJ8~W~?ho@ow!kh5g2b@XR6i~C5?leJA^HwWDF1782VAU^l@(;^ zFG>eVwRt3rv}=c#4pI+%k5O?My@2xUv?-8sK(mkt^FX~&J}%6jfA8B~Qa#t2zlJ8%`*S}%?G6`60^i^q3+w5T}X zQJT2vn+BY3EA9xR$SkX$Q;oY%5pQaVDmxD^TiQAvoC5n+M@v+b#(7*lghu*?XzQQp z@hC}xL}P;wkor9)VEcFYQVGb_hIS^Kbrnae)}7q_@3y{Ti_a3~*3jO}6hP`sFSxj{ z3v4v&7R&2MN%_=f=gUS3Ijg2DZ=8@+oLG^kf77ta;g0htS%5TjQk+~HL|mD>25tOd znEwIPs^$5>mXn+K11n86Jf&6xZQ!SYq1tnu$5W~tj!G#W>@?AGDPJP<=&DR8D^SFb!(u2qzqS1+{e> zqq9(Zg;8L;xXCJEGgd}af-h8Q=c4Ya3YoeXbTl-wefDkpV++9 z?KU@^drioi+$k0gM?<))Nzy2+>aN?zykFlMCAF8odt5#YPB$7Qt|Rd7yplog(7=cci>`E4kUD^b>3U9ec4oTqjb@JEyrw}c&i^xYjl z@T62!LkgD#ts1WwJaaBVF7j4RT*y@{qfnaNJ*ohexK*dA=l2c~RE=wdMfNIum4ICW zQtv7hwgRZ+i)&UyB<7IID5>tCm%b=EOSaH!Y%ym{2x{KHmAu;^%Zv;j|<4_qZXKj@SG_NnUtUxZIV z1_id6jEY0p*>uPVvvt_?J*Kv)h_a0+rA?u)op1?PQ(ShOuJ2ME79UY%sK#u;a0j<$#W-n!#79Wr6Lhz|33?5L z$(q#15c{z*eOL`_=1ZlKs1Oj}CEYh|1&R`Fd<%$n#E)STIu1ZPl}crs#u+{2d`!@8 zda{mY_tO3VIiG#*8(CG2Oc&?W{zt6KuOTkn0pYbKzrS>=FcEK^LbW$qyrcU#i?i5wp$-^UU-DMW3b zAv4+u8Vj$1wDL&_DWw(Ux~1B$a)f6e($gY`1%F1c*KOPNVsjpa>p`rn>Ou{_oP?s>;}QD-5fpm|tp zlcoFkEyhJ)n-&X8kykO&jx{O48Op?cYrGpw#XHAz$U#4p2t>kTJs~c|`F!JB3^pA2 zL_}(SX$}7qeOcc2?WK?$G#Q@H!A7jWBUeM%{;~U-0^>cDI7WHeu5@#fvBTJt2X03s zm2a1kH$pjt;}<@yPj#>nCSEA)KZNhmFlUJ~Kdx|2tc|*O$A^v=q1e~478m~*qUNY0 zFpTW1DO(z-RNPkx?TcTv6ZFC%aiEwoe#=45Wv3Iw{AO6yxtyh*H(raRoIxXcgUS2T zPIXhfTX}V7UvO@>I^1P#s+Z~5sQUm{HcFQ2%KWZ&`Xxh0_WY#~9X1Q`cj^#`RXpV{ zs5TfrO>!Poc1F63%OfTukemWddeUdh@Gf^{D}t-!)ZYhq%^R1F_3ZXeX=F%JH$_Vo zEIp4@^O>ISUXYrMHAO!CyPw9jbUK!Yw8G^s&wx@0y`SJ`!E+61uOFm0*G@gPR2wp zu(G3|(PNhJ;i2?Sfq*{YG42&uFeZ~~$IQmj7HbA!A>6$S@fhmA)GsD&zqo!UUIFe& z;Wum041J^)VUCoY)LgtYOvik*L0U%9Qa|AASAagcSIrg1C6Xg*qF37C*{l?iENsU} zJswJw>1^h!lQ%u>EI`l7o|(ZLIuLO%=$)BV4+CV+Wi~yLXDMvWtwcn{0-x6)%7qRgv%2< z9I7A*w<_HRwreA}MLX;K4`PEo?-+Oor&iSSdDB?kHN8C;C2r1=Zv%wwUL=2%KUz4M zwJ-a%W590<<^-EQkhn87=7;d^K)^}stz>Vsfiwg|_@(54H0*kRK^kSGJLP@YpwerT*%8@?2v>_?l1mZl z=mR*WjEK_b%0YrKioGalN55~lNUEOaN1CQ|We?MXYyCtehz&bFq)Gj_5(k?g?g5rG zy)0IO)6mRmqkx&;-D5)+l>CmDWF$B=BDcshIg#$khi!T*i+s1#6QV(!<(ZnEcd6UG zavg1LhfW1XM9_5(4A;b-X;=}PW=9NUy=kfqNXLvLe`ijup8yUsZN?!BFaNfEJJ9ol zlWfhwoW7%KS4s%Mpb-3Y;5{u*(M}GPR6Ih)monFQ>4W2vTP(GDVaRtqUy9)MqkO22 zLS@zwh*rG{-nR^RCN|FKQ*#t0w(|F=HrKKUm?S9iepE}03K9e-+>QRc;{V++c3{Kup3IEp4P2E70)1dcs4Sg{DF4wAx*cK-rgu7kR6nbRg=}g7`@9RTA@N9))$t2lZ`K>IRo$-lca6j%Ky=(T+^{CK6a?qc#d7UV zlDIYQCE1cu3krEDfh3-C!u?scTFz#&{m?>?7L|I)PZL&m6rYbEV#y!+2XZ)s7E=h1@Rf#Z*4%=ZG!o7m~_E znLx!;NMK1dbP*VQE~e3T)n$!`LhXt6m`hbJD7&ni*IbXTv=TY7xDG<1J;YKuv{hl} zApiqsI>NpddOYm$So4FBB;uvfvBW)iVF1=aa;ojF)C;>a;qm$nK3WgoJ_EV;yEx=ii@b7z34t&S!-L$+p*J+}(GN$iLO?czJ+ zbx?xDF%XXJ#=E5%uJK+ zXLOb50%WI;#M%6GzTGdM!?hB=57<^u>rd2mIzA|gMLnDWY9Jk?R*V(y2AJzr6v#a= zE7A^N0vwP}i&}9&d3(>|IuT*zi)P#%>t&m z(?qi=W)8$$W|dX#CSw?CDFA0S^nQR}_dHbPLTG;XQNjj65}5qbOp4NbEYjztK#{wv zPegF3?iSE?Nhw)ms!l|REuM&uRY*t3gqgAagT*x? zHHKtNBfcJQbXa<3#_+9s8MbVjyV~}9NNAr+Ja`;?_n`DLi9s#bL0gBZB2 z+5^}DA(HqtYf%mu3>hL1_h(eC*_T&VC*z-EI)%%BZNU7a-Z2s?{bn|veySbrFzn5$%MHR)?-8f_=&qYY{&azyBS>#Z2CSd!m_)5f8>}z z(3ovZ)X#Ao!7`Xsh^N49kY>=bTO%}C@lksY+;*Cvb#hr_@)^*HKB!gRnw}MTOm;fII5z_lue~_y{N3pwld{$p@@w|+vW;yPfJXP-Xvn097%A_vX45A>t*ru%bGNu&_A>Kd7^ z1xd;Eb{vL^n69>zEguAJYKFIsbXGO#&*xH0pt}m2V8y)ta?WcC6Y*7y2e|QP2srtB z0sxwUk8-#Nv$~Tz(~J#pWJ9r4N4&x+7CSMS?6#E^N?K+NAr7+Wd6sD0M*ygoD zEGqo7bg6Saj{-A|qZEK|{KG9klHwM7K*#MMh(%eMAsTk>lf zQ{yw>Gm;;n3_g(lyoewD+Bz?PMu0F9EzJu6AXPe;@@@jp#*i^8-M_Nxr5?up^@e1} z&^&HQ)UlM&1{+H--zfjAi4p~d`KwuA`fC%fSb4t&%;52;<~nQo0uh4IRbp-pfX^1# zbhCF8q`vj%J)r$EDgc~`*IINS83n%v!w{dlb9wVM_sxa|yP5u_B3qkiK<=Vk|KG5F-5~QS8jI2w zb2?;7Z?8WKj{ec@%Ip6>D;)kUB5MAWv;UIlKcf76&p`CrJQsf^{6E6Ur2ES_Ki3@{ zNGAQ?#7SlTYdC~|;K=RkrIDgJg!(YZ(f90p< zPi_5cIE1gIUl-fLKf}5E%Q#-I{k<->hrf%n_SbO!n4Jv#e}t3q{%_)B@cvDl6w<$j z^T+ID2>dggKRnz|gZ&pR+aHeWuh{B87TYhM^`C9^zp!ktKIk7A>VNk?{~gXBpPj$y zdR`6nf9-ny6V9Kr^V{$I^v!>|(cgaOFPnn3S5x?EsDJyN*X`*4Y|obY+wc60F#d+2 z{_S`E$BXSh#67?L&VL`l{f>A3$tEZBcf9lWEZgsR=clUkKXaqMY>VKgX{MOliDfa(Ya{sNf{k=`jZ=LNQY;ykp<#!BUo-+{t zUkmVS;s3nY@>7-kU!UKh;{Uh&j?&9BJKw*;0bTfm=6`*9$No>nUHawe$4?lD9fdT? K|3vks>Hh%3(!AvW literal 0 HcmV?d00001 diff --git a/evidence-anchored-summarizer/demo.svg b/evidence-anchored-summarizer/demo.svg new file mode 100644 index 0000000..c4ffe08 --- /dev/null +++ b/evidence-anchored-summarizer/demo.svg @@ -0,0 +1,18 @@ + + + Evidence-Anchored Summarizer + SCIBASE issue #13 - AI-assisted research tools + + 1. Claims + Objective, methods, results + + 2. Evidence + Anchor every bullet + + 3. Modes + Abstract, executive, plain + + Demo result: READY + Top finding links to ev-results and generates implications plus next steps. + Unanchored claims are blocked before a summary is shared. + diff --git a/evidence-anchored-summarizer/index.js b/evidence-anchored-summarizer/index.js new file mode 100644 index 0000000..8418c50 --- /dev/null +++ b/evidence-anchored-summarizer/index.js @@ -0,0 +1,144 @@ +"use strict" + +const crypto = require("node:crypto") + +const MODES = new Set(["abstract", "executive", "layperson"]) +const REQUIRED_SECTIONS = new Set(["objective", "methods", "results"]) + +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 normalizeMode(mode) { + return MODES.has(mode) ? mode : "abstract" +} + +function splitSentences(text) { + return String(text || "") + .replace(/\s+/g, " ") + .split(/(?<=[.!?])\s+/) + .map((sentence) => sentence.trim()) + .filter(Boolean) +} + +function evidenceById(evidence) { + return new Map((evidence || []).map((item) => [item.id, item])) +} + +function claimScore(claim, evidenceMap) { + const anchors = claim.evidenceIds || [] + const supported = anchors.filter((id) => evidenceMap.has(id)) + const requiredBonus = REQUIRED_SECTIONS.has(claim.section) ? 2 : 0 + const findingBonus = claim.kind === "finding" ? 2 : claim.kind === "method" ? 1 : 0 + return supported.length * 3 + requiredBonus + findingBonus +} + +function summarizeClaim(claim, mode) { + const sentence = splitSentences(claim.text)[0] || claim.text || "No claim text provided." + if (mode === "layperson") { + return sentence + .replace(/\bp\s*[<=>]\s*0\.\d+\b/gi, "a statistical signal") + .replace(/\bconfidence interval\b/gi, "uncertainty range") + .replace(/\bregression\b/gi, "trend analysis") + } + if (mode === "executive") { + return `${claim.section || "study"}: ${sentence}` + } + return sentence +} + +function buildSummary(input) { + const mode = normalizeMode(input.mode) + const evidenceMap = evidenceById(input.evidence) + const claims = [...(input.claims || [])] + .map((claim) => ({ + ...claim, + supportedEvidenceIds: (claim.evidenceIds || []).filter((id) => evidenceMap.has(id)), + missingEvidenceIds: (claim.evidenceIds || []).filter((id) => !evidenceMap.has(id)), + })) + .sort((a, b) => claimScore(b, evidenceMap) - claimScore(a, evidenceMap)) + + const blockers = [] + const warnings = [] + const sectionsCovered = new Set(claims.map((claim) => claim.section)) + for (const required of REQUIRED_SECTIONS) { + if (!sectionsCovered.has(required)) warnings.push(`missing ${required} claim in summary input`) + } + for (const claim of claims) { + if ((claim.evidenceIds || []).length === 0) { + blockers.push(`claim ${claim.id} has no evidence anchors`) + } + if (claim.missingEvidenceIds.length > 0) { + blockers.push(`claim ${claim.id} references missing evidence: ${claim.missingEvidenceIds.join(", ")}`) + } + } + + const selectedClaims = claims.slice(0, input.maxClaims || 5) + const bullets = selectedClaims.map((claim) => ({ + claimId: claim.id, + section: claim.section, + text: summarizeClaim(claim, mode), + evidenceIds: claim.supportedEvidenceIds, + })) + + const implications = selectedClaims + .filter((claim) => claim.kind === "finding" || claim.kind === "limitation") + .slice(0, 3) + .map((claim) => ({ + claimId: claim.id, + text: + claim.kind === "limitation" + ? `Treat ${claim.section || "this result"} cautiously until the limitation is resolved.` + : `Use ${claim.section || "this finding"} as a candidate next-step signal.`, + })) + + const status = blockers.length > 0 ? "blocked" : warnings.length > 0 ? "held" : "ready" + const summary = { + sourceId: input.sourceId || null, + title: input.title || "Untitled research source", + mode, + status, + headline: + mode === "layperson" + ? "Plain-language summary with evidence links" + : mode === "executive" + ? "Decision-ready research summary" + : "Evidence-anchored abstract summary", + bullets, + implications, + nextSteps: [ + "Review all cited evidence anchors before sharing externally.", + "Resolve blockers before treating the summary as publication-ready.", + "Regenerate after manuscript or dataset revisions.", + ], + blockers, + warnings, + } + + return { + ...summary, + evidenceDigest: digest({ + sourceId: summary.sourceId, + title: summary.title, + bullets, + implications, + blockers, + warnings, + }), + } +} + +module.exports = { + buildSummary, +} diff --git a/evidence-anchored-summarizer/requirements-map.md b/evidence-anchored-summarizer/requirements-map.md new file mode 100644 index 0000000..8e66462 --- /dev/null +++ b/evidence-anchored-summarizer/requirements-map.md @@ -0,0 +1,14 @@ +# Requirements Map + +| Issue #13 requirement | Coverage in this module | +| --- | --- | +| Generate concise summaries of project repositories, preprints, or uploaded PDFs | Builds compact summaries from structured research claims and source metadata. | +| Summarization modes | Supports abstract, executive, and layperson modes. | +| Auto-generate key findings, implications, and next steps | Produces ranked bullets, implications, and next-step guidance. | +| Save time reading new literature | Sorts claims by section importance and evidence support. | +| Generate overviews for collaborators, funders, or journal editors | Executive and layperson modes produce audience-specific phrasing. | +| Raise quality and reproducibility | Blocks unanchored or missing evidence claims and emits an evidence digest. | + +## Non-Overlap Note + +This submission is distinct from broad AI-assisted tool suites, citation provenance, ethics/data availability, statistical consistency, methods reproducibility redlines, figure/table auditors, protocol deviation screeners, novelty overlap, and AI output evidence verifiers. It focuses specifically on multi-audience paper summarization with evidence anchors. diff --git a/evidence-anchored-summarizer/test.js b/evidence-anchored-summarizer/test.js new file mode 100644 index 0000000..d75f661 --- /dev/null +++ b/evidence-anchored-summarizer/test.js @@ -0,0 +1,105 @@ +"use strict" + +const assert = require("node:assert/strict") +const { buildSummary } = require("./index") + +const input = { + sourceId: "preprint-42", + title: "Adaptive Sequencing for Rare Variant Detection", + mode: "executive", + evidence: [ + { id: "ev-objective", source: "abstract", locator: "sentence 1" }, + { id: "ev-methods", source: "methods", locator: "paragraph 3" }, + { id: "ev-results", source: "results", locator: "figure 2" }, + { id: "ev-limits", source: "discussion", locator: "paragraph 5" }, + ], + claims: [ + { + id: "claim-objective", + section: "objective", + kind: "context", + text: "The study evaluates adaptive sequencing for rare variant detection.", + evidenceIds: ["ev-objective"], + }, + { + id: "claim-method", + section: "methods", + kind: "method", + text: "The pipeline compares targeted adaptive reads against baseline whole-genome sampling.", + evidenceIds: ["ev-methods"], + }, + { + id: "claim-result", + section: "results", + kind: "finding", + text: "Adaptive sequencing improves recall for low-frequency variants while reducing total reads.", + evidenceIds: ["ev-results"], + }, + { + id: "claim-limit", + section: "limitations", + kind: "limitation", + text: "The benchmark is limited to synthetic mixtures and needs external cohort validation.", + evidenceIds: ["ev-limits"], + }, + ], +} + +{ + const summary = buildSummary(input) + assert.equal(summary.status, "ready") + assert.equal(summary.mode, "executive") + assert.equal(summary.bullets.length, 4) + assert.equal(summary.bullets[0].claimId, "claim-result") + assert.deepEqual(summary.bullets[0].evidenceIds, ["ev-results"]) + assert.match(summary.evidenceDigest, /^[0-9a-f]{64}$/) +} + +{ + const summary = buildSummary({ + ...input, + mode: "layperson", + claims: [ + ...input.claims, + { + id: "claim-stat", + section: "results", + kind: "finding", + text: "The p < 0.05 regression result has a narrow confidence interval.", + evidenceIds: ["ev-results"], + }, + ], + }) + const statBullet = summary.bullets.find((bullet) => bullet.claimId === "claim-stat") + assert.ok(statBullet.text.includes("statistical signal")) + assert.ok(statBullet.text.includes("trend analysis")) + assert.ok(statBullet.text.includes("uncertainty range")) +} + +{ + const summary = buildSummary({ + ...input, + claims: [ + { + id: "claim-unanchored", + section: "results", + kind: "finding", + text: "Unsupported claim.", + evidenceIds: [], + }, + { + id: "claim-missing", + section: "methods", + kind: "method", + text: "Missing evidence claim.", + evidenceIds: ["does-not-exist"], + }, + ], + }) + assert.equal(summary.status, "blocked") + assert.ok(summary.blockers.includes("claim claim-unanchored has no evidence anchors")) + assert.ok(summary.blockers.includes("claim claim-missing references missing evidence: does-not-exist")) + assert.ok(summary.warnings.includes("missing objective claim in summary input")) +} + +console.log("evidence-anchored-summarizer tests passed")