From 512baaf6d450f88f329413f4aeffad66103a0564 Mon Sep 17 00:00:00 2001 From: Feeelix <44130555+FeeeeelixWong@users.noreply.github.com> Date: Fri, 15 May 2026 12:56:22 +0800 Subject: [PATCH] Add scientific bounty appeals ledger --- scientific-bounty-appeals-ledger/README.md | 38 ++ .../data/sample-appeals.json | 153 +++++++ .../docs/demo.mp4 | Bin 0 -> 38985 bytes .../docs/demo.svg | 16 + .../docs/requirement-map.md | 16 + scientific-bounty-appeals-ledger/package.json | 18 + .../scripts/demo.js | 16 + .../src/appeals-ledger.js | 396 ++++++++++++++++++ .../test/appeals-ledger.test.js | 46 ++ 9 files changed, 699 insertions(+) create mode 100644 scientific-bounty-appeals-ledger/README.md create mode 100644 scientific-bounty-appeals-ledger/data/sample-appeals.json create mode 100644 scientific-bounty-appeals-ledger/docs/demo.mp4 create mode 100644 scientific-bounty-appeals-ledger/docs/demo.svg create mode 100644 scientific-bounty-appeals-ledger/docs/requirement-map.md create mode 100644 scientific-bounty-appeals-ledger/package.json create mode 100644 scientific-bounty-appeals-ledger/scripts/demo.js create mode 100644 scientific-bounty-appeals-ledger/src/appeals-ledger.js create mode 100644 scientific-bounty-appeals-ledger/test/appeals-ledger.test.js diff --git a/scientific-bounty-appeals-ledger/README.md b/scientific-bounty-appeals-ledger/README.md new file mode 100644 index 0000000..807703c --- /dev/null +++ b/scientific-bounty-appeals-ledger/README.md @@ -0,0 +1,38 @@ +# Scientific Bounty Appeals Ledger + +This is a focused Scientific Bounty System module for SCIBASE issue #18. It handles the post-evaluation dispute layer: appeal eligibility, evidence-lock checks, reviewer conflict detection, response SLAs, payout holds, and IP transfer guards. + +The slice is intentionally separate from challenge intake, rubric scoring, payout routing engines, and solver workspace privacy. It answers a narrower operational question: after a sponsor or solver contests a result, what must stay locked, who needs to respond, what money can be released, and what IP transfer must wait? + +## What It Does + +- Validates appeal windows, accepted reason categories, appellant roles, and known submissions. +- Verifies locked evidence snapshots cover contested submission artifacts. +- Flags reviewer conflicts, including sponsor-affiliated reviewers on contested submissions. +- Tracks reviewer response SLAs and escalation state. +- Holds awarded payout amounts while eligible appeals are open. +- Blocks IP transfer while payout is held by an eligible appeal. +- Builds a sponsor feedback packet, arbitration dashboard, audit trail, and stable digest. + +## Files + +- `src/appeals-ledger.js` - deterministic appeals ledger engine. +- `data/sample-appeals.json` - sample challenge with awards, reviewers, evidence snapshots, and appeals. +- `test/appeals-ledger.test.js` - dependency-free assertions for eligibility, evidence locks, conflicts, payout holds, IP guards, and stable digest behavior. +- `scripts/demo.js` - local CLI demo. +- `docs/requirement-map.md` - mapping to issue #18 requirements. +- `docs/demo.svg` and `docs/demo.mp4` - short visual demo artifacts. + +## Run + +```bash +npm run check +npm test +npm run demo +``` + +Expected demo output includes the challenge title, tracked appeal count, payout hold status, held amount, top action, IP guard count, and stable digest. + +## Scope Notes + +The implementation is dependency-free and credential-free. It models the trust and dispute workflow that sits after challenge evaluation and before payout/IP release, so it can be embedded into a broader bounty marketplace later. diff --git a/scientific-bounty-appeals-ledger/data/sample-appeals.json b/scientific-bounty-appeals-ledger/data/sample-appeals.json new file mode 100644 index 0000000..dd7d42f --- /dev/null +++ b/scientific-bounty-appeals-ledger/data/sample-appeals.json @@ -0,0 +1,153 @@ +{ + "challengeId": "climate-forecasting-2026", + "title": "Regional climate forecasting benchmark", + "asOf": "2026-05-15T04:00:00Z", + "sponsor": { + "id": "sponsor-climate-lab", + "name": "Open Climate Lab" + }, + "appealPolicy": { + "appealWindowHours": 72, + "reviewerResponseHours": 24, + "acceptedReasons": [ + "missing-evidence", + "rubric-miscalculation", + "conflict-of-interest", + "payout-split-dispute" + ] + }, + "ipPolicy": { + "defaultTransfer": "after-payout", + "holdDuringAppeal": true + }, + "submissions": [ + { + "id": "sub-aurora", + "teamId": "team-aurora", + "title": "Aurora probabilistic forecast model", + "decision": "awarded", + "awardAmount": 60000, + "decisionAt": "2026-05-13T10:00:00Z", + "solverPayoutRoutes": [ + { + "solverId": "solver-1", + "share": 0.65 + }, + { + "solverId": "solver-2", + "share": 0.35 + } + ], + "artifactIds": [ + "artifact-aurora-model", + "artifact-aurora-report", + "artifact-aurora-dataset" + ] + }, + { + "id": "sub-boreal", + "teamId": "team-boreal", + "title": "Boreal analog ensemble", + "decision": "honorable-mention", + "awardAmount": 10000, + "decisionAt": "2026-05-12T12:00:00Z", + "solverPayoutRoutes": [ + { + "solverId": "solver-3", + "share": 1 + } + ], + "artifactIds": [ + "artifact-boreal-model", + "artifact-boreal-report" + ] + } + ], + "reviewers": [ + { + "id": "reviewer-stat", + "name": "Statistical reviewer", + "affiliations": [ + "Independent" + ], + "reviewedSubmissionIds": [ + "sub-aurora", + "sub-boreal" + ] + }, + { + "id": "reviewer-sponsor", + "name": "Sponsor domain reviewer", + "affiliations": [ + "Open Climate Lab" + ], + "reviewedSubmissionIds": [ + "sub-aurora" + ] + }, + { + "id": "reviewer-external", + "name": "External replication reviewer", + "affiliations": [ + "University Center for Forecasting" + ], + "reviewedSubmissionIds": [] + } + ], + "evidenceSnapshots": [ + { + "id": "snapshot-aurora-decision", + "submissionId": "sub-aurora", + "lockedAt": "2026-05-13T10:05:00Z", + "artifactHashes": { + "artifact-aurora-model": "sha256:4c9dc6f02f0c9f8f9a0010e7c8c0221d", + "artifact-aurora-report": "sha256:9a78aa4dcb8a9b605e63b10ec31e4f62", + "artifact-aurora-dataset": "sha256:f0796207ec9d81a43ff84dc148d873ab" + } + }, + { + "id": "snapshot-boreal-decision", + "submissionId": "sub-boreal", + "lockedAt": "2026-05-12T12:10:00Z", + "artifactHashes": { + "artifact-boreal-model": "sha256:e2e87d98509bb0186ef49b6b37a31a30", + "artifact-boreal-report": "sha256:d33a2ad6f1fb4f8421ec13fb935af884" + } + } + ], + "appeals": [ + { + "id": "appeal-aurora-conflict", + "submissionId": "sub-aurora", + "filedBy": { + "role": "competing-solver", + "teamId": "team-boreal" + }, + "reason": "conflict-of-interest", + "filedAt": "2026-05-13T18:00:00Z", + "reviewerIds": [ + "reviewer-sponsor", + "reviewer-external" + ], + "status": "open", + "summary": "A sponsor-affiliated reviewer participated in scoring the winning submission." + }, + { + "id": "appeal-boreal-split", + "submissionId": "sub-boreal", + "filedBy": { + "role": "solver", + "teamId": "team-boreal" + }, + "reason": "payout-split-dispute", + "filedAt": "2026-05-13T08:00:00Z", + "resolvedAt": "2026-05-14T02:00:00Z", + "reviewerIds": [ + "reviewer-stat" + ], + "status": "resolved", + "resolution": "upheld-original-route", + "summary": "Team requested an alternate split, but submitted payout route was confirmed." + } + ] +} diff --git a/scientific-bounty-appeals-ledger/docs/demo.mp4 b/scientific-bounty-appeals-ledger/docs/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..f48b085d5ec5285c9b1f3b90d843f8c3e4ed5523 GIT binary patch literal 38985 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ΞzV7@RfX@{GE81zTP=Aecndh#jn62vGwksm1jr4$c;4Lk zw0j&99b!C4t7hnzq=!gHHq;aIL^==Gp*!JWaX+N94hOT$jcz?qE73E}oNmS5*-f?m zloz=#U3GS3%;B*=-G=Pi#~N9-s=5j((USnDvaCN2cwR)wjN9im`yqdi0khS2{vc@A zubb{WdADAnVt?-A|B|wihMXt)tMbd&Mm})Ir$`N5%rG@0uZ>*c8f|#ao{UV=*F>|$ zNU)Kp6Y82D_kbbimJQ2%7B{Rl3evHnYa8Br{sL)dI9>sm2&l?FCNVPWW^aS7#Ff37 z3xa#fHoH$!f;|ZcF=x2L_MOE-yy{8haTWP5o4P(3{s!+uJhu`oUPe4wE7rw%4~CX2 z%oFBMmX|heS$?thK@_2fcL@=KRrJ0bJ8T`Cfkx-j6t=O&ztT8mi|$IoiN9Hwvr=WTzi6 zvtZ`+mT2-Q2=Xj5 zJWm3I(dIHPo`*VG&os_o5L>IHyt~0JMVqmdb=0X+Dam&gfo}NTH%i5xNML$xUkL0v zL~*ERA?gCkxwJMF$%*p@brhbwA)HMfDV$N+ay=r~B`G8Ijj+&ixRh6roj0 z#KLO*En8raN`ImaVKd zwyk)0e_EN#YzWnv;M9b3hMSNc#^qVUN-k86e=|wS0QD`-cb?1F9s0Q0G-=VF8In&oqqS!dvVuDgIt1~POb0x%t?X)TMEh=*Eq%_OXv(Si zD0&b@rkSl>CdcRj@?y$6<7UO_)JZjpXmCHNan<9Nx;-$=*LAkL)(wbgGrtPq4*odD zqG)&Be*ChPa@Q~SeS{CD9E#HYJB~H3uUiBn$76z7h?$1C^P<}XMg(4}7I+VbJj`Mec>bii6ad4ZotYZ$D%3V|2 z&113USJbT2Bo|ER-`;P(XVZsF2qP)soAnoai>pMg00S^He7ZLAY)WUiai}Vq#p7ks zzR=s7wyf#?Wc<-WmVi@G`CD^Co$xp~K`FbeIY188E&gk`Uu=Kgj4KMI1f&M9G3)E5 zyjrfXUCLgzEYpQfKd>LmJB_|qS(cNV8d;iEDqX;M$FqR$IgP)3Isi6qV;k6>r5@bh zENf4A>TEDC!;yT5Mss3QEU-zanxJjm_^!d*YqAJXip?5tE_tK1c2t9 zmT@A=)rd;1jdrZBsWtAie~~EyllA&l&cgS(P^I(}9TlY@-Lp1=8kD`J4hQ|EdHHf{iY;|f*t;+tK%}5~XW3xsB#gxElsz)NFZ5cB1Sl@> z#HnRcZ5BL{M1K&mOd1(5kKnK@75pOcSd2=Uj8g=EiEji{l4j#JQ2Db{83p=q8YT-z zrXUH3Dm5M}FVW1j)eDKYbR$DlWc8J*%@}snk{~0N=YBW|3cpwZ&$HWbuHv$+U6L7d z^Lw@gCW(4ks3GG*z-Lp@LC~)z()vDf9KRq^AuG*h;vQ_}QV`e$qV_pg6w39J#!wU` z=tN2u?&gJf?zbGWQe&}GS|r_=+8gt{Ido!GVcW-^x(5()tV@|n5L8}@^a7!r6&uLu zf8HZf)l7RS_f0Ub(!l!CGdUA9ZTaIQpQj<#8sK1B$oAR<%U_FRoY>23R%tavrwyas zZ@ZFW3RD$h^gU`C9~2FFg>atIbB(=x&&SPvA;tEYm0Pjnt#7R1;XzXD%q&_e&sdyv zD!}Iv5})V`J{P;x2xlJW#`2yattnjXT>kyn?omQ1xBVTy#pXKs>a)`OmhUXh3GR5n22Apk8-1&S6k1J{fJgc~$G;vwQ-3`?CLD7YASZbjUBm8gy>~gLr|z9w+t`LMc7LruiRNy!pO36g-l$_l+#H;lV1v(N+uVMy2^nU8Pvt(STV~=@IX-&i zI1nmkzwWIL{*y68M7J@4yV!R*gY9D-{*77k>n^y69wlab(`_5!8lR0`xE9;j_M|x* zYy{6wTn=0Y z(y|~HG*^#t7Jb*g&j|}pR}pCW@4GMFmaj{v<)mumv=5fd(2OENZfiy0i`%QI`?`be z#iQlv+7}u<)0&TqE6_JDGObeJqAhC-^Hrgpeh(q45QUv>f7I2Vx=Pz3Q_9 zi!ZUdOuT^Bmli3jEiPD9|$WZPIxw@4-N0$zbOwXFFX+XmO#&9{9VEU?sa1N*EdtKJ%ji{{EbK% za5YkB24(+Bl{ksTrC_kS^I;#_iZ*9^E|vM;d459}B^GFzr-eEII!FYbV8?BGMljFP zc3u2cWj=(ANkvY?>`iU-faTUHZTR{U@#DRewb_IW<}JVsD6bH zaVWpI;hrgWAySCOnarMdWbIXgY^JmTU=JnVB)?Wh0Z!wfV!T^JN=Kwbo znh}WU!C^d|_^uK)cnl+vpuIAjWhS_@Ck?hkhYbuMJ23zS_a8VJ2 zI;ygb4z^1oTb^>AV7KRy5j+%3WL(yHcy;mbK3E1LCTR9$T0qVW1 z!~jv^ukKU)U;*vuVdELzY}WiNk^bsDxy|n*=$N`f-cN6m$Oz-nU6v1@oYkd*{mf%E zbubI7`LGC9cX%XJ9DgO<;#u;rDQY$45bEif$!6AN*l???)Fe zU4!Sbpj|vjuN|@t%|bW(bY;M}yT4GAAulv-?JU|Q1tTrvAEn)5EBgZ_yBYon5xFye zw&mb);7s~ROR(-OKqejUk~`+%o5mor;SZE~_h=2)Jr)@`1ww(EP}+<`=x4WOzI}1I zv~y?F;l_s<^P{geobq_i+=6mdpdaJNVa|S+PX-eJUFcu#+tLH#An}?bVSQ0YjMSi{ zWCTzls!Y0>J2z$4=UBtGlo9F{thh;67{6L%)zH7* zWmhI45Lo$dTe~9*HX2Uvd%4^9h6~QQ zVgM9b;pWB01we6bYGeQvkj(FGswCyI(v!?8mt0vl>SON0vIm=)jxN@a5veKxqVC2h z0iR6@ac`U`;+RnqeSb|knSF@}1jsp@7#DaV2h;Qhz|(Ai|H6Ly5vicDq7+uVl8-7T zSb=Epdg97u*guEK$fs6{P6bWZ1x<4dQ;ypFi0=hIqhYLG@ECos8M~tfjDOJe{ARRV z^oRHj`&$hJRVZm-GgvBM@UtnoxZTra2hScM!aZ z5WE`hmQuv9q+m21dqdVG{FA;oq?$|%Ylb3gKlX6I+||t_{CG+CATvPw%gHnDOP1II zHVUD|?ii{QSc#3$!*q)6$i|Nwgy{T&q7HR%gdqefIO181v}5d8*zN0W#ae-gCc4j<(LU zSQ)q88ybbf4*J#RoHH@AzQu;pleEAmJZjQk5f&2Ze@ zhB)fq5#{gVFyV0Mdp>jDZyvs6Hr&4le4Zhp8OBWegG2OQM-lPm$Ep#BV@C@6xj9N| zBa%No{nr;}G$AtW{eEP<pZcNGR&_^g(6h-N4iu8lqfI~s`Rm$N9@cCZ7)@5U`D~ZpMysUJiJZ_R%agc;R zU-f;>Q*?J18@G4(dF@^7cSnU>nvz~8!|Tw5XNU3}yfaNK%~!vRG*Cz`)EGv|a4>8( zLQIY^Hms}%i6xYh^&puxWpgAhZm)bpPrWH9TCJH1&4l;u2SpEwouoJHG5^sxMB1l#X(5M|&!?_vA#Wv%4@2evFMzp{Q+!=HTDdKzYQNI!ne*`e)VqI|}@_ z3_JGr`(iI*3=XvPn$ShB?K8M!E9|aD`uwcNKzejRbwt{uh zw)M%6>HvT9UMRxY{!C0*UVTGVW9v{-C++T<&_Tgw7hInmT0j_nO*UbRUbmlr;2U}? z38A0jg@jw$W};Gb2i3XKAXvxC*f>3et17sqcTkGXG8PG6+ZuCpRodXsqUU%9o$cpy znYE{qU;k-4V#6TI8u>|%VpzRihS7u+*zB9QhjkX6k_eI9neQng<73Q(SRRn&7Ss-izH-s1e(|8Nxkt6s?JmPIfu1kKG zBe5c7(~g&k?3biiL4)FCD7&yet{>IWa7Z2r8Iz{@MqlZPEyBcfu!?ZUUl!|(CPjW#StB5-~N7+ds0@qj@gF#ZlDms<(!T< z!H7tB&1SKrmbUrM4JT(sTQROB^U}Z^R-O-f%IeX~KPw1AaQa4j2-D|zG zsy{!xr_1^XC|ZCmG9@#1@>)x8S?8Xj^0cYPWCjXLLaGiFHSf54%O2s?@eZx}Ge+PffAf48>KFM&e=h z1+Fz(4R81kK5=C{6rOOAH|=tU#?=V^LE0R4Qd_W%30^A;&7a3RX;HELnspmN#~O?# zc;_O@^vrQqhlp{%7%w5*WsjnBYSOUmfyW@K7F zUV`|&FaCI|oIp*$onM7{*zdVZJ7MfIN=KBnm>9sxB$IC4a%qunO)3kuHfr~00Jgmy zEwGEwBgW`1BcA`Sy|)aGY)R5YQ_RfFDls#w#LO&}n57bwn33Ibg!qAbCMbW( zGgsLll6y{)#9Ot{0z%eDaAR$npSr*38gG-&K6u0mZKAnCdd-1zAroeT4h8MdQgj~* zNXhqQwrSIVb297q+w;KiT6I=U6OmDgR7|E)raoP{c-%yaTW+zii>1eFeR=1TdWO>oC3@ zCKW^KhvTuCCOwxG>ke`#4)T~eTKExzOZvgH{!~&9JUi%qnGay*h2a%ejcFw`Xl^$Y zP=~P!NFJa({#JizS;2$FW23D6 zcG>RQe0InZ&mX_-&$6bVgdz-9ux9k{Jw_THe^|1ZI68PG= z8M!P4Vv7T>azV_ON;%A;gpJ~_9{kBqK)QzU_zn_|r)UqX9~KC5;F>5vI*G0+Yp1R> z=>{+7!s7Nl=3;|vsc(0FTA~>%lU+ZW0E_9H1)}@n!D7ELh`R})MAaGE7;O0w!NPHt z+b9DLv8g9-F22|gi_Gv)65%=%wkZW|mE7e^jokeTZ3$BtESkmH0BKgd)hQvB2JB|Q zaBuZ)Naq4gcrMpaA zE{8t_J__}zy@RBI)y+0qc4YZE4tk@S&Wkjm+S0ja%{5|La|R=d0F^5I1ck6V-03xw zJofz5h#v;G9m5}z!b7k^GfhbhX}Uelaj;{4YF)H{h9IyOv1pcnfCyeqki}N}UP#s- z{1Uf%@@i7`u8TwuiH@HD1B0+~mOr-9@))Jo&Cp533+Y=cE(@UqW8@1n38a3~KHq^p zJ`rMUtDdq<2B$|tW@_8XmiVjf`OFj*>4RLdWeIQXj?6=huo({-pDQdqQL92cDr`V4 zTbP0cUJBa$rcG%@^M=^9ZG^Vcsg?W!MxrVg$hA3C>H?84ldGo}iN$DV_1X`LPfgLqLzk=Q)~cAH^UgOyvtOR6O<7wO2L%})G~+kqj_U$fzr%Gv zj)SZtOs#(1vyf|x(fSFE~=Hdj`RTkq?!p#pc zSg4|!#%U8~qeoHZA~y~ou?NXwRSu*=t?VpD+58&W^f`Sq zj7=uYLS*|ou!1Gd-Gkr%$!k1BD^GT`UxE`(32gSlY{!KLsASZ-27ub!3a?6jNpC9A zHvNgfed9%0Y=c;?G)*KokbFukc3!^7+;P5lfsJ1O7A4IR3B$=Pmx`VIQSZjn$3kuM zipbcdK|H4N3dg>&wNO?~XzTuPI&9X@XbFdqhV2D*L4pfm&SZH2L0V|yg&Yu8y0W#! zw~!cHx2Fh=7w1K$>mV-7*oB#RpTdKW@HMHwdJr6(12^nkb)N68S+Xt$Y}(dY|A#f7 zi77eRP&BB!OkROTrMxv6%D>mb7o&F6kRnP4ABEbYca+mB`+v8fs#ZPBL#ECllkT-l2o6yKI z3M6Q%{3^#M$u}FQP=nlpIQ^9Hw+Hbd(`zgdQ=qb27G#TN#W5pkmrTwvm|uT{=wB0k zMJN}9usam1EBa-sTTN@~R}u7$#zI3sHTiS3Y_t7dixPw^;6UD@6TLxOzg_n&)(nNO z_46bmgRPCbgFjKKY5y+W_^ocng|JA#-h-zhj;05 zpppX?r~D1W%n_xWTmS@AksSf52yNU%`YY#1)G|+%h)dayh1$iV=fp1`t?x)mA{o*@ zv=6v)aiSeJaWKBNr0J5{)c2)$cWE=3mubwn%Qg}jzhag>R9-y6#>Y94HbG=6A13KC zDvfsBPgh+GKw2oD#N|OO*{{gme$^=2iSjt!cIW8xRz^H%`22Z55zGGhOdusGLi_YH zac#+o7oJw)?fHHjiFa){91^TelOSigmQM~ojr0EP7-4*@pwtLEgZgN*4?12WVz7Y| zBDo~5?m{HXn#!f@o=SlT`IqR?7*1JKjDmGg!UX=nz!2qZ zTfYqOn~e_Sb~pOHIR{)4n{ALjZS$_TE?IrG>5%+J6>M<4gx5()BIh}#VTK%eP4f5# z{vec+y*z%JH6E`}yd%`jQl=9*+L`@s!jk5i203|;h^gtT73Hq`N+sMj*3WU4l$z$b za^{Q0{G#Q|A6}JHfs~sVT^9}+!vuDQ7Z<#02G#k;dyiN5 zqud7-GiM`k=Ssf;yneD6t?};+-j%6JlC%XPjTCYkYAza?wl=MffqTrgH@KtsepiV} z<}*CQKjf_Co^9q5AzvuCix@{s#Izcq=@>KVZKsOHivur2F(+IL^%Ww!UuCYgz&j1! zemSp!FXNyF;^245Et5>YSxxLRKfzP)k}G&~9JI$W;SxKYg|mshZaejFp|?h1WWdqR zL!q8oUuDwJU}VucWpq=@o2ZZ>%#{=>t6!XsSXQsqe>!E^Hy&z3N>;&gp=ccn96=|L zfIj!qa~f1|dQ~B3+6~J%3u{lg{Ds~~$`Zi?VQK1cmi2A1*lXZE#u~d+7^`?!Lho18 z!pnJ47Z=rwr4lgW2rXgum8Y(RKWU|irMvyuQgn+Q?Qv6nffcaw$R6<}(1>|AhM&QE*b$!QlEj$g1GmA9GI2h3cAIDlpQ!^FeUK3 zUya+y#xDK%k<)4z-fSYtPG>6#D29_apa8y^T!zM$xDv=5UC2=JCqCv$f$=O%U_B0( z7zWxXO}Jcj-tK^Jq)i)(z_w2`$S|XG9CN+Tpm#M5;krPq@RibxJUl#A0PI||4z^D? zzInu3;)eOppNx0OEjZYRstF6DR(x_bVc<0?5oAAE9$p+cPHii^wbG(Z?ce!U`F~at zsWd5EC-oLoqwFcloWGKuySTIZfUT{9MnrRIrbPm|hlNne;f4&%XD`d1QOC>*M1b-) zaBMHeC^#{}7+b0ep|Fs`Jm*{*!6Mb7$Hdfg`fAiJpV`Qj1z7hbN-$rtBib751tBKt zf(m+_mr!I8$b$QEG|F*p&|vZ~_cqwUNDjU?y{`YghP8EJkX{MU;8>>9G?q|_Ai9LRfn))N1Dp{Y#l$eNG39?=Gn0jB4=wv`2;v$)Vo zWh?7vVMDm(bpk%pkJk&}S_y~EuRh7`?Lw^NQ;t#^eb4GFrtMbV#b|LH3~27^C?-NPo%KjJfB;)g zz8#ghZ6+ss8G7-U4kGf01uPeRvDvOD7KKK{a$plNj6#{ToN5ghRjIB|KqWnkdOK zgr9xT(gb%~9<*+c;$_nrL+zcTp7Z-1AOM_Y{ErQlktA^f6MW(Sa>Kuq=Rc-9#6O&!+2Mx_Sslqt%W(%+QkoFf!KGU z5gN+avTC!Cw2m}%BU7MyW&80+aYQO|RCnlw0WK+@1f>h@Ajx=iooHd}4X8IO=g3VgX(=G$FCi z7sJ@zYnmnWV1t0JuyRcC?(^}dFTs7QgSr#6+t<5ty0m4Iq>ImfflfNDsVap2X=iP) z8k%X6^*gKqz=RW3{-e=Rp8$sToA3dz%h4g0IFUq9je1+QE*@KjI84A+2(mTaSR@?6 zkr6!o?cqBdh-`vn^VdaOU#SOl1A7tfO-dA1QH<JLXuV(6iY zBt`d%6U#U_PBm+-pdagUqndagb`FVS(4ymh#%?+;#s|A7hPpZ~I1ttv>5ia?7Q&7| zFt&^?U`9wr(UYxzYa&EEZ*y@W3pGT6i|W+U@pj zH$J%_f|4YZUcvfD;2SAT)Gub%xAcfUawEKPzcDv*)5R0it86(HBXW;Qti}2XyEDJ& z#36-6=SsW~I~?6Htik@Ac95{`pY-Ua$P%<{2&Z=qc87!4-`Am-BXy28hb@CI{S} z$6R5#mA>v9=!+4^vHUMaHlF4E-cg95ss@CYj5*4jl(frtb_D=%$>CM05+m zgYrb}bOd|Q+ZLXDmLem2Fw)2>P{YmidDl70VP9e{$#_ZI^|7J>R(OkllP(+cIE;zJ zj*@KYWf3p^3KFux?}2yRHxm%p8qxteVW1@wh9^hwCDL{$k-qYg3!7OO$dBsKQQuEV z&u0*8R~cyW?8KBGz){x>2zVsqMvw0pOF=`%Us2-bocmhPSBwE;%Ea z%Z`@M(1(cLLfi)jdR7E-H0y%R=^^*&W3@CDrwS>xtTVg?FDyDALoqsl;+MZ21fRE} z&%??}qe(hYe&w^i#2;uUb}0CS=8{?R1B&9+mtxByy$RUdG$YT~Kgu|mTG$0O#1L%b zhNQLuxrv@XQ?KlMpx63KKxVy52X~_srESD6iHD}+wz9>O ze&2CHeHA01VW1~2!tB35<7!s0z{p%vuRVS5-OY;9_J1B^Q=@Ax3L_+uC~EL@dZD~T z03nK3dp|KIR4c?YkG0r8bbS0+%F4<5i~zS#4PIU-dptag`j9CyiEGdk#*eW@oHl3l za5SO2S;m67^E9|Ue2^sMr$HO?CD}-9$XcR)vfC{OcM4NtB}up!l7uKWFE@0^_N95@ zoG}%3*#tFH(3-JW=Iz;ZrLUrXR!^Urj-)meiOPoT9h-1=8pRKz(i3esl4*0HCh>B5 z-DPH*Y+=aj2L|o*4QKwUKXl;C&Lo=sK6cG}lR?m8%|zmv%}^7frx1nZ4i>N-P{v$n zi9TZdlxh#7U6nGw+`_G-x*UPLxSbQOf++JIn^@sIPTYPf)ZX<_wC_`{Sa_*q2R-a# zKw$`>;YsjAZ?Q_jTO9xOBaGErT!ueO(`}Zqe(@`ZU$!yAx{<3nJu%z7hc7Tn0vbkD z&qgvrQkFO5Tre#v2dUw%Y`27`T)!oVUZ{4NJpmo=0ZEwm_q+vgRCT*|jWOU&FBReb z$`B%b*^bb!THR*BoX!zDQ~!E|ZlB_-==EKE68Z z@fBD6U$pLQ*>)#uzf>TB96U(fmYO(Q2=;@>=IAf#KtZPuy& zFhxSS8Dfa-PTdMTOEp(ge5miD3A$l(Nw4iXxyK8sZAIV6uaK|uR5Ec?t)nsN(tW0b zK7|RuazilGAq01DZr>L52b0TU&V%<0cF~k!^fh0tuus0rWP;H+2{qx5NPPD0XfD&! zPC5#G0Za+@f@?C$j>IyIcqS?R=s9gIDj2hEO&`1Gs53Vg#&tePAYpQkCegvDVlzJV z9a&f|5ez=t$xq!<;jGLZ`D|9U%CPu2YfYX6o708&su>o_&ZZqrL>S4R+TMTXrc6Ks zRY!JK?CW+h#5{3f9$Tw!Q!Lq8?gU|NIf5-4>v*>81%B|An;jg4H1aZ$VQ{K=Q+_?h zf?tmm!deyzIdRSMaPN(K^ayF}A+VaOoF#RXkVAg<^W#`xE{gA`eD6>KQ)Ztc%Ybb& z_MF*lCU_a|0lnmX`BEn-aUPtnJ*I~BkGHLi0F@(WFTlW)JzTj}^dR2Cpl`d4E$i)*E zl+y`fVu%0WiT~zc9S`S_pxdb(o?jpmj(zKq;#kkL9cQ8mnq&U`hf{Y)u}Xc=(ez|j z6_a5?A8RD?wgE$P0>Iy@>43G$E~gFlvde}z5*IQFX{vtwl-JVWJXoANFV+%C=-N$K z5n!QLM@Ud({Ov14#!Jw{yN@Ay-D=ir=}{KBG*^*mWMWHgMPr;60~Hh+)l=~itIUZu z>=@m8-8TxMTqabtG?&H+FK`~+M}pnk!r;0h5XbL_pjE&Kh~N?~YF>G%CDEWP=fnC5o`$>|otMlF!Q@SODA}AeIHa_+HM{dC(-8+&4$ZZO z(aWZz5NHq7+qd+{xOL&=lMwyM@Bv@S$3IVnCrBo?syirdG8Y z7NPCeEna=r-W-$qe#Ncu$?&VgCLHqZ(5`qclo;?BKfM{3l=za4EwM^-{N$L99_`C3 zs&NM8KDzC0OLh-(Aap}H#tL21K&{zbK4C28h;-XGQ4v`jba4|LF>&P_pgPCpk$?x$ zc6SF8c&s6uFF3;#AdEXh7#2WV*QBIP_!ho}m3IkI?rIU`RvH6GxC_CZMXuQBd42h; zBEnKsR~^@VTt&WZ4N0hdpB1?eC^&TSpiH$a?^?j20(B}KLMs|lu01X%G7qXf0k#MB zmGnT%6@kZcrJgNqb7e;+`NToU;6TMk6dm*eNLC>mO95&X0x+TxkYTIc)w?11<9wARhu#D`njB zNWG#;Cc(7+=HRQ8dorUdHF%w$ES%cKB;HB*vB>n#CYr9PV_(Lts<>wvsQW9wGym-O zE4Mf_%kA-sd&I_ALsR}ygsc&{#zPW~7u9s2Fp#uj5W+BV7S^gVwE~VH-*rZ(B%rR|ral2YI2|%n&BtUL20NjX3Pn zIj-IJ9)!r$J_2V>y0P{wziyV3RwvRq(5H@g_`%E|ZA|`@Oqe$MU++V`8BK;S56FsL z0Oxszo3^xch!m!q6rfW|_rq`vcchMVSChOmPTI>wSVY-#OWv^E%Zsv7Cxp8FOuP24 zf)`U#lo)d5*LnPKfV?i{8~|)YNrdj8KG|z(xo5kLxaF?jlYzb6k^K@Jx7)hghUSUR zPqRuq^&cUyYaMtK@A9R=?Y5RBLT*0b#*MAHuqfP)R=8Qy!lVJN7b zN{f8A8?Y|>lt^wOng=ZdX+A;gxKMj0f3q90J%|D)6D=6s^s=?N&p6YF!n*SM?qwHh z>!vPQHIoK44sR} zD#PdUf}iM;N6?l^oyN=4q`hg@XEP$TB8&Q`1Mb`fN-@-`L?bRukZdDwrWqb ziE&HQyWpBB2dbl+5-v@VN=Ugv6al&?>A;K9JBAr!*uF7z-$2mBcF`i9mi=ZrugihX z%Mq-fg6OyPkIBd~Pze1;3r{;?lz!b!#)ajfh5F?Xj6+mKQF^*EFYC7^-I;7$l(c0qqJ=Cy8v4ID)I>5&<<+Ox zJV0no`a9U-u{Xjlt;u*kMNygs-q5a3jogilQbfN3Ac<#MrLv&wwiQf_$hEhf#9?%D zhgKC3pI;piud*_#Q``!`AFD5PE5Y2ndUeNxpa!5_8m^su@E++SpwsO2TJZz$mk}9U z61`fAxD$AuNFg*9RrPhgD6qK;Z38Ppu6cT5Sl!4~ zIcEX<^od3xlt!SbR6u23x_H-Ac|ZX@<;LScPA`7L7;V*;a{u36ORzXp7>hvV(q3QBTG94 zu;;akT1se5&w^b1ouclXG3c@MMjcw*e6Hi>ZM+qY^#irvXIA4{{J{KYW*$f=)J>(e zutm66&o6Fck~?;~KMlo@OP%#YxTyvu2rWZr$^h-iA8Fp>P?6*>f44*9yxOYImn!p( z)tu7Oqp?o+9bVongL{3!x)QvM<1jQD9?x+x@Y@BuqfAsSrES*UXUq&>LDRKXv0c%L zbWPPHu9%o(2cIyfCavhCDR7a%7{RRTt%^>o+TBA`JAapjs9nx>cg8dDo2h(jTAP9} zzktU$;Koaw~5pZ{^L$K=KC0)48E{bNwodwxG^OXZFBX{Jr6Im z8hLL_TfGzBWn-wdhwoAtAfA$O1jA+cp=3ARUmauAHLd`iauu|?BJaFJMoM}0Ms_Q0 za$Rir7W7)G4+DmYD8FfmHv=7me&*pMV}K}HbspiOk1~Dp4cW;(Oo>TFC%ICH zxq8120~ctUL~txahDP`1Gho8NDV&*$#0By+`#ELXRn$2`kaucvFr#Y8O4kBSQ$Uhg za=DOZNIteaLP#8}(i@H11Wa?PxyiJySU zS|@@tf24Qx8!qn>spw**QMnj?6Z`bED}-Cv>#de&f*!@SuUPp-YiQ$nm8(oHeP8b# zq|2WYsn+BT&Ha$NPTb<OEVqABE>V zBmY1*5iJWvaYJ#T9kLcjmm(8^h6ocwi>q|$Su1*XOTR?Gcu;=2J{LB=P@aCky8#ak z7wVxyGO}`Ka3+xTL|re>ez|lRK9KsZeAUwCHWgKUQ$n)r$Yu?m*EH$oZ`C)h$iF-~MhM@RT@* zdasS-dE@NW8|QN8%EVr#%aFl4x_*oo94?h%cHleg2b?=ESigO*W5Q`s=}KL=gBT#wyoKKWEH?FG{$SQ zNYduKes%Sb;1%0N`nu<_JqJ2vYpcstmfp$%#^aLBmTjLsh)Gq-|H!*P$Q>Ga0GL;uNNGaIgj4`A?I;1xh>l zk&5X<`u?cTW%1da#l;ySUAnH;XyCw_uW0I0$pPt13gxXaEfXq=jF+=3TfwDJMp&5g zk>Qc-MtDt~y8Uu6h0{NAFM`yUgAAOTD$cYa--Ml3aCUQr9HxCyTW@rNju#FyI+zNO z#9&E=}PK;vJTzqo2R5;=vq4Se% z3!LM}u?JO(k4P9wAhv71QU9Ss)hq^6NHr=$e_HJ~C;<%dwLMKQ&u`?Ux2$v}a@1)n z{x(;MYrqkB-6^9v_6qu>uHCIu$cr)J`zrPxcfU$&u0~HJzCS*Jn1VBiM$yzaY_(a2 zQ=WRr;@3&*(cedinNuA-CU|OVH_~ic`8`jSk>#B|Ry69jG<;fZ1t*UmbFkH9xGC| zu5r#dtB(f8Sj|)9Og;F029!YPI2`gs2$t8e3uj8Q(p=yIa=;tgyIY=njg(Eu7uwiT zvv2T9wmsROBCWnJre*}CBkm%`)?{e0k6L+H8>T>lgc!GM-qf*4T#r81TnK~i_!=lf zxA7?Gj{*g;@yp?{Z-|v^&@(G^C{$=0rsPse2Ot`+$BX>L&7kA7vbfA*>dW=mTz5wc z*E8xB{ZYC%jnIBIB6q`HCR{dIo%6}ZMGmk1s}WKfurMMfnY(8+bz+mTT20o276P&f zWR*Fm9V*y0=JehqXD04rl8R2ahi&1F0DU$x)SwU{HmT6B6gc9raFdblGeif4EDBt!KNg+_ zXOeefoq$gtL4UiET>ce&+VcIO6u28iw*nPDxB%GbQAbM(Y~Edj2aZz8N)B8TRC%5& zB-^7C!YW>GuKF|LM5g}b!)XM_!LveB|N9-N57o{AJB=CDTc!_n^Xm>Ff3agf1l(7= zk2M2l4SofDPF=D#1_1N*Ft%RzeG8=ml%o3zbFIoSs?K)%v=+!;74RFoyTZ)cQ{PKQ zrR*LU1mt0TfjB#TW9BYv0>s-&RZ4boCKw{9_No9Nmw&ShR-3GWcLVmc+YOPZ7+P^ z^^XKq42kq4OADs0iO`vOfAWjg08Y*XeFO%+8a3HgyWhpP)VK_{D2PAX?BZ)@w+P@o z03v7-U8l*dfl6UP(DNXKDQUytz*EVj_ZEj1?v(`@s*GYVwk7!X`ZcbOB{@g1(vO<9 z!GtFiVm*oe;XAr?|3?TYz@CE2BK0xJtQqZvK+hf#r&I-(r3QzvD2l||xdoC7b;nf4 z8BHivK&M}@7jYc5xZ*C34A@-Js7W_-rt|UMxVS88bg|vLu48+JMR9*$ukGw`*dvZm zltb>%Zn(4w#<{j^s#2L$G3sskSn>mU!HwPJ%I($AB^m*mIRdBcB(8K& z-&x}vuZ@V(1;M`C3~xgS+f>m~IFL_tgIW+j9IP!0vj)wR8)@>2yIuO#`Ecd@K2n6F zwey^e=!o(36y`}<5GRu4I{n<-#2v+Nk0+t==vV?CkhuCDd^ z+q|_)?MSFOyq%q6p6!L4;rSp9DB(wmrY2ZD0?V!29{vGn1yQ3ExUDy zQMW^QYL>=xNURGKKWQ&VW851d&}w(Vn)OTLGDWl$!`Y*~k7eLQGKA>2voxi|1MW}J z3qNsZHnE{$Wr~IZE|~2f7l&jQYh!A zf=?fn|FXEXMK$JafnWprGz1Y@)W|ZBRLl|5QnwJuK~>_G;eb8`_svhobL!Pd@UBE? z)v}tIjuC{I-b|1Lvnz=HT7+dh?Q*i4k8NSsq^c>itd*eeN&WinD+Wp6Fqan)5%VfM z5g>nKAkr0v5fK>@Q7F~1ZYCBM0xb^GC?V6!&9VQixCWkWS93b1c*+iob>vj5FGYjg zMl@BaR>_v z1^8{UA03jrvRZ#cv}063mpaiT3Y+QkFP&hw2p_(h3@JQ#%%@@i-XcX(hlZey;>4_N zFX^Kh$5}h7;$?yZJvBTL+Zrq8xpUTlreVDxTmvaDvAZDarc3Mlg$qhUnejI(42`H` z8*&jKGl7MCF~#P?VG{rP6Z3(7wQ;x7N4a8850Cbogq91D>-#HEe0l=*3qI|TU3szm zG`V;^xV#B?G;{taTL`*=>>mm{LW_3a)jw7TonMR^?_ZumO1@3l;+JoX#Q7>=m?F!J zZdD)vtEphFoZrDcBRapozw@5SQvYIb#ZXD2ebfdEX@tc5gCoROcK_{LU4zAxXZ*uEh${_V*W2n;2s{N+?C! zz^1}-%bjMUv$67|$H;?UOGoGk&TigJ6F}~0CuQ4X$Yj%@MyEg7srUi3 z8LjWLh$s1E3I5$wUnc`5D$H(5f3ry5=t)|3(ujIlMl zlMFlQ!75hPpdCpxq|>~;sK&x^cpm!NY-lRZCUn`}xhA#M){4Sj#kjpcF-aZ!m|riP zNwzUx?#a*^8jP56c0NNs$NDSXbZ2i@*Fidy5J7+>WLbFDpcx3G6Ok4WKMx;8fvHSM zMa!8=kxXY zVa<18z_ygJ3h1D|c!^gip{I3ZcW# zQ3YLNr#7d`h%z2@CZ28tH|UF6uv;dRc%LugOd*T916UpK=60x6h3`y;O_IUGVJRq3 z()M@tyIH_J2b2jnTcAg8bW!k$$*R>{Sj;LabyZjnS1xK2i0^d8>V_WKtSj3@}WBW6Y2Z)Z4!D*LO1wU)Jh^1cw|y`OD|1>eD{3^u;d#EXR&?HrEU<8yS@p0S4+{S%W+_0 z;Fg@z6loh^*lUTSu4tpfsCSM1+NoMbK|>+_X8%;?8CGC-wIQv@NfDl@Yni$uiJ90h zc}RB;HH^~?EmiSW9)Bualo~dkS~TkH&hP-Su-2Ida&)dSt*e$^gAf;WSeYA4db?zJ z@Zdi6riKzoh=W8qQ_V8ZPBK39tZJE*MnQ%>OabMs>b%orNAV3|snxTDs=bR*96_IX z=&MtA05kBtqqF{QzW6-4j+&(_ml-9;S0jc3>~A-on#l0h1xzWsKo-}K99tumb-oU~)DRNZQs8i{ zD7>o}$>uP~vPv6BH0nd1ED8hm1iouhuk^r1twLN0Br@)lS8>rGhgBelk!=uZ$DSQx z4iARN3q&`P1u#FCi=4*AMRW30kxYR!JWkyu>olJmc1#UIf2 zLb<(B7w^I2xN)FT*K}jw8upG_=G1Y_pyAe@7NY)40qU(t&rYe*Jl26~!cYz>p>B{^ zK+du3?Blz@{jfk>2wZro-QWOC9nqED6<=;v+TVb?;ux>*xm=T{vwauu&3NCVe>XNz z3gNB0I(WJW!c<#(-;eUHi2UQ@2;S7ix?XNHm|NsmxWT537BX*wF)i8sg^OO@a+O+e z!}#P4%8RbKWT(Ls=91iRJ~OE@88kjD0 z%`O8H#|4eymn4r6lF(hDO4K%#Z(80=U-q4g-IYX{AbeadLw8Dg#3;SG-1xQzJ%+qn zz*k0VH%z(3kiLMe_YtN@uGD6dXkEFQl%(jc3BHM&I;6Qan5|* z=5{;93q*xBRqNX!!}7S!QjIv(yloqwqCQXWZNHAqX>b|p`tp4(qE8_Pu^Tx=wK$(K zmC@fg@!85~SIiQ$o1NP=vkIO8+H4zibZ6#Q)@Bo#WnxDwm~o7sNd1~v-hsdgD2gnT zJ(`{_RP5lm`1JPXTmnfhtu{|95SKBjJw0B45osC#s;UDDOw-ym+1D#MVcHyYdO$H7 z?z6CgOIcGl$b^1B0Up(k|9Iq9{ENV zhyG>1NM}9WcGl>Wi~&N%lghZ3t8aNsMT5V4Gf@(_{zIT_VJP{J~ke> zx^FB~prI!c?}%p(=bWIHF_AZRM()#qEnNc;;IH+Q3BaIww_1)?<*Lhmee_Y$@7Hsn zq3(gH>cK?)c^+VyT$YC}Rk%WHC`7)ScRcr+L6Q%}C!(97YON==l9t7FRiP-f8INJl zXD&tkC0D7cd;?|>YEVg4-T*k1E6}iry>}%ANk36=fY))vW6B9V1@ZvgKGlwj6Cxgj zWByj#_*d2_(-02!sJUOEzm3zY4vDLI$Jco$;W;N0Kb)CqU(Z#OOfwC^%ku5^CL`{4 zR&M~f@_8t&J5Rg&6(a$TR7s1~UHo}*O_BwRXGB#q+TFOvI<0kL)K@2Ha&Fdr7lVw% zy+l3=`1hYj=6{o1=fpa#s|E+N&ZQkTt$N@Ksl8muBtb9c{XWWLb0 z^>Vxl$1&OLuin!y%hkKx1&@qJKJ96oz|YjKWUvX0NF}i@F>)g9M&GRIWe;7x!hLqQ z73-_x9}G2TfGWX>DTV8uT_%TMNYNRmh%axv22n&#Vnna4P>bfwAX=m*&$~%Ao${I% zc{i4w93MDV@}XYHp(11Xl)zHOEWJA~rZzVA`mDdZE?ev23)LJpJ|+?OZh@OZuh6!b zEAP7;gXJ3}vnUZ3%`jH10{x0ns7csA)o|2`OqrDME0OwZ!EEs$D&8SwCsd3}b?%c{ z$mYwrmnOyUL;oNk^C6P9g+NM9hUeqIKQRF5KVLL0uwU|t_&DYRZqM2N#)%E%&aoBIspk;BAoJ|q);wK&zJ`>M8$->xl6}=fmk>S9$ zue?URPZw=-@L)xos*a5+x-pxmDA?O~zXoft>N032e=S!ByWod5qMbMXQJjx=AP1M- z1Hs?R0wBKvRgk{?`%k}rfEC55@~TcbZ$Qag|2cjMSYqT78-ab4pZ@&>Pyn1DS3m*y z*AxJN+y9GlbgZN!&L0XidOlA3H4{uDwG!(3RhsmhuHB57I!m=~jF%_^Sh6rW?g`!q z(qGGsrN+SmN(nsCIuW<{dhP_(I@{)UFQ5*emQ*hK)VWlOe^t!Eh?+A+0}OB?H)-C; z+i97O(Pot--0Nm66}UoO>BhU_it_Ra%NlJ1{j)jFh&O~d<@#m_TZu2AUKnY4F(Vfg zB`at{WHA#uoR|-9(#WTAYvw+4!HW6Kkhq=M{I}Y$B^rs~nhY zs=ayH3Me+!Hrf{2#FZIi@KjuhC9~$ILEJ$HGGZXdSx?#41Cv`@MLQL6lhd~rkP2U z2%4ozLps0zS^ZEzcqxE!)o!DbjLWDIr9E-dv6Mh_ikzwBIdJlw2^8yJ!HH&OlmHS# zUy>jB5&bYz(P_&@E?tWDENw(QkYz^9-!FG$v#9$brhqno#`PEzit=`6I)g__AKC#R zSt<1Z2)>TSp#~sM5(2*!3wrsZAnJlyK_W;r^uPNB;F`zTOatfE;|yuaqH)~GM??si z;hE=v%j!`8l2hVQKlA~>w>*QF06?!s|F8le6F(VUYz?FPPoq4nH}Vhy0BDRKx>65l ze+U8BZ?FEL1mxoc;txqR!K@Jxw7(m|%j26(VhS_n3Ev{I7IrxRhy>^KHBFgltk(Kg zhS3iI0Nc6)?gapwuK!sAR!gsONkdnL-;{s+iqdA`@}Zwf|3meM9ghD?J6JpAGcy6= z8>N4j0d=(hQ`d(Eu#Z{>U;xTG`I{X)Gz=JTRjaJbBJ@1PeE7=!KD2-Q5yHqnWT2Nm zWT*twF8`AS{$``U8|&i(=5H>N7tDtL2WlNA7G#BfGq$p9FAh2=$8a>WY)#neW-~I zKfRjtuY~&1%6{`|<39!UPp>xrGf;nvNK}I9?SBpGpI%-0S3>=>0zdszQ2+Gm%l`oC z)?b7AhgY-k|CLbx@M;$C{{U(h|342kjr6ZU{llwSpZ}Fm|M2SHIUn$s!qOjJ{RhAQ zl?ePt1^%7e|KA++zlp#f1pIg2`Bz2Ye-Q6qg8D~W{Rip(6{vrb&3~rb{}Sq-Ui}Xg z`!9p~cW(VlQ2+Gmzj^h4*M;~uul}QZ@<*TPZ@r~|^XmVnj^V#~^}p!@{~4D4&8z=) zSo$}w{;vV)&#?4wUj28z`cI|i|ISzcn+W_b$X9LNe-#J(*A4ZfxiSy}ejjl8zan2X zegBn(_}@Zp`^}j0g8v8StCWIiB!A|sYVW^3_RIJq-u|a|;E#c<-;fG^0Jn|uN)FT>sRj~qd{4<+rm0x|#qbHh&osD+XM literal 0 HcmV?d00001 diff --git a/scientific-bounty-appeals-ledger/docs/demo.svg b/scientific-bounty-appeals-ledger/docs/demo.svg new file mode 100644 index 0000000..e84874b --- /dev/null +++ b/scientific-bounty-appeals-ledger/docs/demo.svg @@ -0,0 +1,16 @@ + + + + Bounty Appeals Ledger + Dispute handling for evidence, payout, and IP guards + + US$60k held + Eligible open appeal + + Conflict review + Sponsor-affiliated reviewer + + Top action: assign an independent reviewer. + + IP transfer remains blocked while the eligible appeal is open. + diff --git a/scientific-bounty-appeals-ledger/docs/requirement-map.md b/scientific-bounty-appeals-ledger/docs/requirement-map.md new file mode 100644 index 0000000..50407c1 --- /dev/null +++ b/scientific-bounty-appeals-ledger/docs/requirement-map.md @@ -0,0 +1,16 @@ +# Requirement Map + +This module contributes a focused dispute-handling slice for SCIBASE issue #18: Scientific Bounty System. + +| Issue #18 requirement | Evidence in this module | +| --- | --- | +| Platform-mediated arbitration system | `buildAppealsLedger` creates a deterministic ledger for post-evaluation appeals, eligibility, reviewer conflicts, SLA state, and next actions. | +| Automated checklists for deliverables | `evaluateEvidenceLock` checks whether contested submissions have complete locked artifact hashes before arbitration proceeds. | +| Optional third-party reviewers or peer validators | `evaluateReviewerConflicts` identifies when a sponsor-affiliated reviewer must be replaced by an independent reviewer. | +| Feedback loop between submitters and sponsors | `buildSponsorFeedbackPacket` summarizes open appeals, overdue responses, conflict reviews, and requested sponsor actions. | +| Escrowed prize funds | `summarizePayouts` separates held and releasable award amounts while an eligible appeal remains open. | +| Partial payments or honorable mentions | The sample challenge covers both a winning award and an honorable-mention payout route. | +| Payout routing | `evaluatePayoutHold` keeps awarded submission routes intact while holding/releasing funds based on appeal state. | +| IP transfer upon payout | `evaluateIpGuard` blocks IP transfer while payout is held by an eligible appeal. | +| Auditability | `buildAuditTrail` emits decision, appeal-window, response-due, payout-hold, and payout-summary events with a stable digest. | +| Reviewer demo | `npm run demo` prints appeal count, payout hold amount, top action, IP guard count, and digest; `docs/demo.mp4` is a short visual demo artifact. | diff --git a/scientific-bounty-appeals-ledger/package.json b/scientific-bounty-appeals-ledger/package.json new file mode 100644 index 0000000..96cc70f --- /dev/null +++ b/scientific-bounty-appeals-ledger/package.json @@ -0,0 +1,18 @@ +{ + "name": "scientific-bounty-appeals-ledger", + "version": "1.0.0", + "private": true, + "description": "Dependency-free appeals and dispute ledger for scientific bounty systems.", + "scripts": { + "check": "node --check src/appeals-ledger.js && node --check scripts/demo.js && node --check test/appeals-ledger.test.js", + "demo": "node scripts/demo.js", + "test": "node test/appeals-ledger.test.js" + }, + "keywords": [ + "scientific-bounties", + "appeals", + "arbitration", + "payouts" + ], + "license": "MIT" +} diff --git a/scientific-bounty-appeals-ledger/scripts/demo.js b/scientific-bounty-appeals-ledger/scripts/demo.js new file mode 100644 index 0000000..530fe30 --- /dev/null +++ b/scientific-bounty-appeals-ledger/scripts/demo.js @@ -0,0 +1,16 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const { buildAppealsLedger } = require("../src/appeals-ledger"); + +const samplePath = path.join(__dirname, "..", "data", "sample-appeals.json"); +const challenge = JSON.parse(fs.readFileSync(samplePath, "utf8")); +const ledger = buildAppealsLedger(challenge); + +console.log(`Challenge: ${ledger.title}`); +console.log(`Appeals tracked: ${ledger.appeals.length}`); +console.log(`Payout status: ${ledger.payout.status}`); +console.log(`Held amount: US$${ledger.payout.heldAmount.toLocaleString("en-US")}`); +console.log(`Open appeals: ${ledger.dashboard.openAppealCount}`); +console.log(`Top action: ${ledger.dashboard.highPriorityItems[0].nextAction}`); +console.log(`IP guards: ${ledger.ipTransferGuards.length}`); +console.log(`Digest: ${ledger.digest}`); diff --git a/scientific-bounty-appeals-ledger/src/appeals-ledger.js b/scientific-bounty-appeals-ledger/src/appeals-ledger.js new file mode 100644 index 0000000..fc7e550 --- /dev/null +++ b/scientific-bounty-appeals-ledger/src/appeals-ledger.js @@ -0,0 +1,396 @@ +const crypto = require("node:crypto"); + +function buildAppealsLedger(challenge) { + const validation = validateChallenge(challenge); + const submissions = indexById(challenge.submissions || []); + const reviewers = indexById(challenge.reviewers || []); + const snapshots = indexBySubmission(challenge.evidenceSnapshots || []); + const appeals = (challenge.appeals || []).map((appeal) => + evaluateAppeal(appeal, challenge, submissions, reviewers, snapshots) + ); + const payout = summarizePayouts(challenge, appeals); + const auditTrail = buildAuditTrail(challenge, appeals, payout); + const dashboard = buildArbitrationDashboard(challenge, appeals, payout); + + const ledger = { + challengeId: challenge.challengeId, + title: challenge.title, + asOf: challenge.asOf, + validation, + appeals, + payout, + ipTransferGuards: buildIpTransferGuards(challenge, appeals), + sponsorFeedbackPacket: buildSponsorFeedbackPacket(challenge, appeals), + dashboard, + auditTrail + }; + + ledger.digest = stableDigest(ledger); + return ledger; +} + +function validateChallenge(challenge) { + const required = [ + ["challengeId", challenge.challengeId], + ["title", challenge.title], + ["asOf", challenge.asOf], + ["appealPolicy.appealWindowHours", challenge.appealPolicy && challenge.appealPolicy.appealWindowHours], + ["submissions", (challenge.submissions || []).length], + ["reviewers", (challenge.reviewers || []).length], + ["evidenceSnapshots", (challenge.evidenceSnapshots || []).length], + ["appeals", (challenge.appeals || []).length] + ]; + const missing = required.filter(([, value]) => !value).map(([field]) => field); + return { + status: missing.length === 0 ? "passed" : "incomplete", + score: Math.max(0, 100 - missing.length * 12), + missing + }; +} + +function evaluateAppeal(appeal, challenge, submissions, reviewers, snapshots) { + const submission = submissions.get(appeal.submissionId); + const policy = challenge.appealPolicy || {}; + const asOf = new Date(challenge.asOf); + const filedAt = new Date(appeal.filedAt); + const decisionAt = submission ? new Date(submission.decisionAt) : null; + const appealWindowEndsAt = decisionAt ? addHours(decisionAt, policy.appealWindowHours || 0) : null; + const responseDueAt = addHours(filedAt, policy.reviewerResponseHours || 0); + const eligibility = evaluateEligibility(appeal, submission, policy, filedAt, appealWindowEndsAt); + const evidenceLock = evaluateEvidenceLock(submission, snapshots.get(appeal.submissionId)); + const reviewerConflicts = evaluateReviewerConflicts(appeal, challenge, submission, reviewers); + const sla = evaluateSla(appeal, asOf, responseDueAt); + const payoutHold = evaluatePayoutHold(appeal, submission, eligibility); + const ipGuard = evaluateIpGuard(challenge, appeal, payoutHold); + + return { + id: appeal.id, + submissionId: appeal.submissionId, + status: appeal.status, + reason: appeal.reason, + summary: appeal.summary, + eligibility, + evidenceLock, + reviewerConflicts, + sla, + payoutHold, + ipGuard, + nextAction: chooseNextAction(appeal, eligibility, evidenceLock, reviewerConflicts, sla, payoutHold) + }; +} + +function evaluateEligibility(appeal, submission, policy, filedAt, appealWindowEndsAt) { + const acceptedReason = (policy.acceptedReasons || []).includes(appeal.reason); + const withinWindow = Boolean(appealWindowEndsAt && filedAt <= appealWindowEndsAt); + const knownSubmission = Boolean(submission); + const allowedRole = ["solver", "competing-solver", "sponsor", "reviewer"].includes(appeal.filedBy && appeal.filedBy.role); + const failures = []; + + if (!knownSubmission) failures.push("unknown-submission"); + if (!acceptedReason) failures.push("unsupported-reason"); + if (!withinWindow) failures.push("outside-appeal-window"); + if (!allowedRole) failures.push("unsupported-appellant-role"); + + return { + status: failures.length === 0 ? "eligible" : "ineligible", + acceptedReason, + withinWindow, + appealWindowEndsAt: appealWindowEndsAt ? appealWindowEndsAt.toISOString() : null, + failures + }; +} + +function evaluateEvidenceLock(submission, snapshot) { + if (!submission || !snapshot) { + return { + status: "missing-lock", + coverage: 0, + missingArtifactIds: submission ? submission.artifactIds.slice() : [] + }; + } + + const hashes = snapshot.artifactHashes || {}; + const missingArtifactIds = (submission.artifactIds || []).filter((artifactId) => !hashes[artifactId]); + const coverage = submission.artifactIds.length === 0 + ? 100 + : Math.round(((submission.artifactIds.length - missingArtifactIds.length) / submission.artifactIds.length) * 100); + + return { + status: missingArtifactIds.length === 0 ? "locked" : "partial-lock", + snapshotId: snapshot.id, + lockedAt: snapshot.lockedAt, + coverage, + missingArtifactIds + }; +} + +function evaluateReviewerConflicts(appeal, challenge, submission, reviewers) { + const sponsorName = challenge.sponsor && challenge.sponsor.name; + const conflicts = (appeal.reviewerIds || []).flatMap((reviewerId) => { + const reviewer = reviewers.get(reviewerId); + if (!reviewer) { + return [{ + reviewerId, + type: "unknown-reviewer", + message: "Reviewer id is not registered in the challenge reviewer roster." + }]; + } + + const items = []; + if (sponsorName && (reviewer.affiliations || []).includes(sponsorName)) { + items.push({ + reviewerId, + type: "sponsor-affiliation", + message: `${reviewer.name} is affiliated with the sponsor.` + }); + } + if (submission && (reviewer.reviewedSubmissionIds || []).includes(submission.id) && appeal.reason === "conflict-of-interest") { + items.push({ + reviewerId, + type: "reviewed-contested-submission", + message: `${reviewer.name} reviewed the contested submission.` + }); + } + return items; + }); + + return { + status: conflicts.length === 0 ? "clear" : "conflict-review-needed", + conflicts + }; +} + +function evaluateSla(appeal, asOf, responseDueAt) { + if (appeal.status === "resolved") { + return { + status: "resolved", + responseDueAt: responseDueAt.toISOString(), + hoursOverdue: 0 + }; + } + + const hoursOverdue = Math.max(0, Math.ceil((asOf - responseDueAt) / 36e5)); + return { + status: hoursOverdue > 0 ? "overdue" : "within-sla", + responseDueAt: responseDueAt.toISOString(), + hoursOverdue + }; +} + +function evaluatePayoutHold(appeal, submission, eligibility) { + if (!submission || eligibility.status !== "eligible") { + return { + status: "no-hold", + holdAmount: 0, + reason: "Appeal is not eligible for a payout hold." + }; + } + if (appeal.status === "resolved") { + return { + status: "released", + holdAmount: 0, + releaseAmount: submission.awardAmount, + reason: "Appeal is resolved; payout may follow the recorded route." + }; + } + return { + status: "hold", + holdAmount: submission.awardAmount, + releaseAmount: 0, + reason: "Eligible open appeal keeps the award in hold until arbitration closes." + }; +} + +function evaluateIpGuard(challenge, appeal, payoutHold) { + const holdDuringAppeal = Boolean(challenge.ipPolicy && challenge.ipPolicy.holdDuringAppeal); + if (holdDuringAppeal && payoutHold.status === "hold") { + return { + status: "blocked", + message: "IP transfer is blocked while an eligible appeal holds payout." + }; + } + if (appeal.status === "resolved") { + return { + status: "ready-after-payout", + message: "IP transfer can follow the payout outcome." + }; + } + return { + status: "not-required", + message: "No IP transfer guard is required for this appeal state." + }; +} + +function summarizePayouts(challenge, appeals) { + const heldBySubmission = new Map(); + appeals.forEach((appeal) => { + if (appeal.payoutHold.status === "hold") { + heldBySubmission.set(appeal.submissionId, appeal.payoutHold.holdAmount); + } + }); + + const submissions = challenge.submissions || []; + const totalAwarded = submissions.reduce((sum, submission) => sum + (submission.awardAmount || 0), 0); + const heldAmount = [...heldBySubmission.values()].reduce((sum, amount) => sum + amount, 0); + + return { + totalAwarded, + heldAmount, + releasableAmount: totalAwarded - heldAmount, + heldSubmissions: [...heldBySubmission.keys()].sort(), + status: heldAmount > 0 ? "partial-hold" : "all-releasable" + }; +} + +function buildIpTransferGuards(challenge, appeals) { + return appeals + .filter((appeal) => appeal.ipGuard.status !== "not-required") + .map((appeal) => ({ + appealId: appeal.id, + submissionId: appeal.submissionId, + status: appeal.ipGuard.status, + message: appeal.ipGuard.message + })); +} + +function buildSponsorFeedbackPacket(challenge, appeals) { + return { + sponsorId: challenge.sponsor && challenge.sponsor.id, + openAppeals: appeals.filter((appeal) => appeal.status !== "resolved").length, + overdueResponses: appeals.filter((appeal) => appeal.sla.status === "overdue").length, + conflictReviews: appeals.filter((appeal) => appeal.reviewerConflicts.status !== "clear").length, + requestedSponsorActions: appeals + .filter((appeal) => appeal.nextAction.owner === "sponsor") + .map((appeal) => appeal.nextAction.message) + }; +} + +function buildArbitrationDashboard(challenge, appeals, payout) { + return { + challengeId: challenge.challengeId, + appealCount: appeals.length, + openAppealCount: appeals.filter((appeal) => appeal.status !== "resolved").length, + payoutStatus: payout.status, + highPriorityItems: appeals + .filter((appeal) => appeal.sla.status === "overdue" || appeal.reviewerConflicts.status !== "clear") + .map((appeal) => ({ + appealId: appeal.id, + reason: appeal.reason, + nextAction: appeal.nextAction.message + })) + }; +} + +function buildAuditTrail(challenge, appeals, payout) { + const events = []; + (challenge.submissions || []).forEach((submission) => { + events.push({ + at: submission.decisionAt, + type: "decision-recorded", + submissionId: submission.id, + amount: submission.awardAmount + }); + }); + appeals.forEach((appeal) => { + events.push({ + at: appeal.eligibility.appealWindowEndsAt, + type: "appeal-window-ends", + appealId: appeal.id + }); + events.push({ + at: appeal.sla.responseDueAt, + type: "reviewer-response-due", + appealId: appeal.id, + status: appeal.sla.status + }); + if (appeal.payoutHold.status === "hold") { + events.push({ + at: challenge.asOf, + type: "payout-held", + appealId: appeal.id, + submissionId: appeal.submissionId, + amount: appeal.payoutHold.holdAmount + }); + } + }); + events.push({ + at: challenge.asOf, + type: "payout-summary", + heldAmount: payout.heldAmount, + releasableAmount: payout.releasableAmount + }); + + return events.sort((a, b) => `${a.at}:${a.type}`.localeCompare(`${b.at}:${b.type}`)); +} + +function chooseNextAction(appeal, eligibility, evidenceLock, reviewerConflicts, sla, payoutHold) { + if (eligibility.status !== "eligible") { + return { + owner: "arbitrator", + message: `Reject or request correction: ${eligibility.failures.join(", ")}.` + }; + } + if (evidenceLock.status !== "locked") { + return { + owner: "arbitrator", + message: "Lock missing evidence artifacts before continuing arbitration." + }; + } + if (reviewerConflicts.status !== "clear") { + return { + owner: "sponsor", + message: "Assign an independent reviewer to resolve conflict-of-interest concerns." + }; + } + if (sla.status === "overdue") { + return { + owner: "arbitrator", + message: "Escalate overdue reviewer response." + }; + } + if (payoutHold.status === "hold") { + return { + owner: "arbitrator", + message: "Keep payout and IP transfer on hold until appeal resolution." + }; + } + return { + owner: "platform", + message: "Release payout according to the recorded route." + }; +} + +function addHours(date, hours) { + return new Date(date.getTime() + hours * 36e5); +} + +function indexById(items) { + return new Map(items.map((item) => [item.id, item])); +} + +function indexBySubmission(items) { + return new Map(items.map((item) => [item.submissionId, item])); +} + +function stableDigest(value) { + return crypto.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); +} + +module.exports = { + buildAppealsLedger, + validateChallenge, + stableDigest +}; diff --git a/scientific-bounty-appeals-ledger/test/appeals-ledger.test.js b/scientific-bounty-appeals-ledger/test/appeals-ledger.test.js new file mode 100644 index 0000000..ead66d9 --- /dev/null +++ b/scientific-bounty-appeals-ledger/test/appeals-ledger.test.js @@ -0,0 +1,46 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const { buildAppealsLedger, validateChallenge } = require("../src/appeals-ledger"); + +const samplePath = path.join(__dirname, "..", "data", "sample-appeals.json"); +const challenge = JSON.parse(fs.readFileSync(samplePath, "utf8")); +const ledger = buildAppealsLedger(challenge); + +assert.equal(ledger.validation.status, "passed"); +assert.equal(ledger.appeals.length, 2); +assert.equal(ledger.payout.status, "partial-hold"); +assert.equal(ledger.payout.heldAmount, 60000); +assert.equal(ledger.payout.releasableAmount, 10000); + +const conflictAppeal = ledger.appeals.find((appeal) => appeal.id === "appeal-aurora-conflict"); +assert.equal(conflictAppeal.eligibility.status, "eligible"); +assert.equal(conflictAppeal.evidenceLock.status, "locked"); +assert.equal(conflictAppeal.reviewerConflicts.status, "conflict-review-needed"); +assert.equal(conflictAppeal.payoutHold.status, "hold"); +assert.equal(conflictAppeal.ipGuard.status, "blocked"); +assert.equal(conflictAppeal.nextAction.owner, "sponsor"); + +const resolvedAppeal = ledger.appeals.find((appeal) => appeal.id === "appeal-boreal-split"); +assert.equal(resolvedAppeal.sla.status, "resolved"); +assert.equal(resolvedAppeal.payoutHold.status, "released"); +assert.equal(resolvedAppeal.ipGuard.status, "ready-after-payout"); + +assert.equal(ledger.sponsorFeedbackPacket.openAppeals, 1); +assert.equal(ledger.sponsorFeedbackPacket.conflictReviews, 1); +assert.ok(ledger.dashboard.highPriorityItems[0].nextAction.includes("independent reviewer")); +assert.ok(ledger.auditTrail.some((event) => event.type === "payout-held")); +assert.equal(ledger.digest, buildAppealsLedger(challenge).digest); + +const incomplete = validateChallenge({ challengeId: "draft" }); +assert.equal(incomplete.status, "incomplete"); +assert.ok(incomplete.missing.includes("appealPolicy.appealWindowHours")); + +const degradedChallenge = JSON.parse(JSON.stringify(challenge)); +degradedChallenge.evidenceSnapshots[0].artifactHashes = {}; +const degradedLedger = buildAppealsLedger(degradedChallenge); +const degradedAppeal = degradedLedger.appeals.find((appeal) => appeal.id === "appeal-aurora-conflict"); +assert.equal(degradedAppeal.evidenceLock.status, "partial-lock"); +assert.equal(degradedAppeal.nextAction.owner, "arbitrator"); + +console.log("scientific-bounty-appeals-ledger tests passed");