From 5ad1a137d636e6a04fe159b91c13447f74d90cb2 Mon Sep 17 00:00:00 2001 From: YX Date: Thu, 14 May 2026 18:57:23 +0800 Subject: [PATCH 1/2] feat: add offline collaboration conflict resolver --- .../README.md | 45 +++ .../docs/demo.gif | Bin 0 -> 52177 bytes .../docs/demo.svg | 32 +++ .../docs/issue-12-requirement-map.md | 19 ++ .../package.json | 13 + .../scripts/demo.js | 89 ++++++ .../src/conflict-resolver.js | 267 ++++++++++++++++++ .../test/conflict-resolver.test.js | 152 ++++++++++ 8 files changed, 617 insertions(+) create mode 100644 offline-collaboration-conflict-resolver/README.md create mode 100644 offline-collaboration-conflict-resolver/docs/demo.gif create mode 100644 offline-collaboration-conflict-resolver/docs/demo.svg create mode 100644 offline-collaboration-conflict-resolver/docs/issue-12-requirement-map.md create mode 100644 offline-collaboration-conflict-resolver/package.json create mode 100644 offline-collaboration-conflict-resolver/scripts/demo.js create mode 100644 offline-collaboration-conflict-resolver/src/conflict-resolver.js create mode 100644 offline-collaboration-conflict-resolver/test/conflict-resolver.test.js diff --git a/offline-collaboration-conflict-resolver/README.md b/offline-collaboration-conflict-resolver/README.md new file mode 100644 index 0000000..1b38692 --- /dev/null +++ b/offline-collaboration-conflict-resolver/README.md @@ -0,0 +1,45 @@ +# Offline Collaboration Conflict Resolver + +This module implements a focused sync layer for the SCIBASE real-time collaborative research editor bounty. It handles the tricky part that appears when researchers edit scientific manuscripts offline and later reconnect: +deterministic operation replay, section-lock conflict detection, suggestion/comment merge safety, audit reports, and restore-ready snapshots. + +It is intentionally dependency-free so reviewers can run it with stock Node.js. + +## What It Covers + +- Client-side offline operation queues with actor and block metadata. +- Rebase of offline edits on top of server operations. +- Section lock checks so protected manuscript areas are not overwritten. +- Safe inline comment and suggestion merging. +- Stale version and missing suggestion conflict reporting. +- Restore-ready snapshots with content hashes. +- Reviewer-facing audit reports with stable audit hashes. + +## Demo + +```bash +npm run demo +``` + +The demo prints a sync report where an abstract edit is applied, a locked methods edit is blocked for manual review, and a review suggestion is safely accepted. + +Demo artifacts: `docs/demo.gif` and `docs/demo.svg`. + +## Verification + +```bash +npm run check +npm test +npm run demo +``` + +## Files + +- `src/conflict-resolver.js` - core queue, rebase, snapshot, and report logic. +- `test/conflict-resolver.test.js` - focused tests for rebase, locks, suggestions, and snapshots. +- `scripts/demo.js` - CLI demo with sample scientific manuscript blocks. +- `docs/issue-12-requirement-map.md` - mapping from issue requirements to implementation evidence. + +## AI-Assisted Disclosure + +This contribution was produced with AI assistance and manually verified before submission. diff --git a/offline-collaboration-conflict-resolver/docs/demo.gif b/offline-collaboration-conflict-resolver/docs/demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..c06090fbe3a5b1dbd13ea7d43680d2b8e3db1403 GIT binary patch literal 52177 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⪚0S$jklJsBK_%1FeRTG)2Da#L=@yCWHIV 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 z zId_!(i{qV?O-N@a=}yMf_Kccajo!YYM)o4Lt`a$GDgm9RDqd762JDQa7ibJ5+^s~5 zmLhuERURS=jfBW|uNpsq7?*9vi%Zo(*;dV=ZL*@xiGdwryjSd$NVm75opPPs1MlqUg;A) z-lX2e)DS}aj~svsP;+L{>s9}i12F%x-u`*_hjsh&=kU*;zlR5hhkt$_9v=KYWF-Or z3%$+)*!T8#_kQp0?fqii`Bz{6`RmvJgWHq>^v(6c>iw>F!6roKRLkI#C!VNcdy4LU(qM% zuV1l3fRTUE^^wunL!+<%Pj|hm_jzZ}^Ol~MBOML@b^gDrudS<>b^gDly_@B(KWX{L zU1y=|4NqDhH?{qjyWY@J`@hrcjsKHgZz!vNTwYbj($_}{@0C>66_?c%{tK|@7gy#L zRb&;EXXce=WYf~JX)K04n>Jzro4@ zoUZu&cYXc;bk|u$fP;&-y^FW4%QZVEFP6T3#?k8^ciq9w+WxXNi(a>NHMenQbpb|} z7YxkL{kJaQtj@nhz<=ccr?IRSfaR{UasZaQ&ME>l{x3y<3fe&NKl=JnJvj|MIrY;l zeO+Gll&tD06zlds0d`gwz={F{B~%2&mH(3ipp1YRDpDqf`S&hLss5iFKt?!vN@>YZ z3;tVP4H5#=)XGNoNIe=wGZ(@(4I4ayeT z2oHr_@Cdp1%3b*Ggc?DOb#Rp|hh}Ps-dZmw2{T7(Tr??W7_a87gk2T0Ue(6At=bkf zTVNjhbDW*5q!Rv>!Ed{MGTRG2xfsdb{C-=5x015PXTHgUilTS!;;ClThZg(*vA<82 zqJN$s7Tr7CACd-W>3Aj?{9!yfiw>eZFSxT9_eBs;?yNP8;qZ81X8=#!^TZwnR?Ve9 z5^AzB{!@mIHS4s=Ldd;)K)h@`!&la$yei+6qy#TEn!iHL-z~;$oDefLsboC)q|212 zTSmG%_8Z#+jkn*(pD%9V_63P;!Ha-vM=l%5zniA(=0vxaQ{ZXX%7UlzfVjfwzwB{E zC(Md@DLf-L^Uiq1A1vkg0hu_nTPM24=n53RY45s@&zzqgr|~rr3}GDXFt!alKX#i5 z*u+&Mx4!}kfE1mKLKA;}heAxCf!xQ0&`f3eRY1N-4d$Jk7}EH!Y1tKf!_9tQlfM~k z)wC+(5=!2CPZhf;1naUwOL2Q{VCvS&20}(azp)(aFHYSw{7bhRSA|=~I6rm`*_5kC zvO_RO_CBKMa>A+C(>dY#*3<@f&I}Vn=z~TA$R+=WM>Bw~HrWkuCs4rxF~1on&2S8J z9)M1aot_V+hdme1)23m3$Uqr$lAH4Egn?r2@MmLaz8KyT=cW{{e@>c@TOF5ba zF-&XWzzyKHkKi#j&O}9PwbL7?UXF8mV@-wy&4_Oz{8U#RKrnS*zfGgGV$6kl5FiH_ zqmE!3gL1y^j^R{+(B-&h;xSi{WoJO`g`Q(_z_!b=bzN^?ohn-sdXu*6rajP!S)3Y( z5i81nqGlwHxI#qdhN#_CUW6 zh7}ExUa(OJu-D8)4yxr_R&foubeYcJ=&a)olF19DoCfvr0tK&PVbi^ne^YtEM!cvE z#IO<*p$Va}EkcC($d*P9b>IREyh|SHe@E1C>Qv*mGZLD ziX7G82N#jlCf3&#b}20_yu1f86QV53`sN4*#;N6i0FRc(Zy|tv_9sjs=9|KAe8Dgw z&aWyto+FI<7*SX&r(2cmGEeasEdn|!b5bv!7U^yZSSdyEAXCkvMI@CXdG5ltE}N*i z)LZ{05P@uL(pISC+o$9bLH1t8VSx03;#4HAo+86q5}U%{(?aGZRSoY9`5z2$Bt9&! zI_PbR0gYb;ID=6fjY*`|9?B$PK=wSe$>N?u+DS2KEyOa=^$M38gbhRcYtKCWCXU?= z8Bs_(#~3w!ig^HADig32I79qvQv7*BA(BTHf!QoY?JNZPTgRSFm^;C#Npc3;c~H4`>j#JhE6bIWkpH*1BKw=g?W}V7eH1X^= zDJ{7k?|K3TQ}di0zBs_G+@SUbX%BY#X z7?o22dN7ph+%GHA{>UU`?7^|WUfn;-QNdi>kiHXT{V>ZQqp9mo=~~F9ls}OEnp&Z3 z>~1@nMA-%Lw~Cwsx=Dj@80HqIL@)r}G?MIjRHf=X0||7?Mm-aTxdF6}qHZ91`84(P zI8R$fP_xOBXK`#tDePhdJ0M|azxbZ$N02N6WXO6=5~GD-UV|y;u~+!;-K}zOhWQ0s zmUyHp$;33(ACe!qq;Vwp=CF35pJa3JuiQ^8;m{BTRsJoxbI z+2y?du6SIq7nHX-@W{RlE#&w*X?sbvJ(XKT4h%=S2tGs&1upB$oJ&q)9ZRh1yVWe- zMerfJ)e0@Od|>^4O{IYw{$mokW z9P~>pyyzQRoNhE}q4xz8{8ClM#W%%lC%P!sy6*OtSgqCZc<|i`=j0c)Gu7TJPVwV51`-gO^m+vM z_Y5zAQ&i))B<=}M5vtm2B#{==4+c6FzoCl8VtLGi+S zUhcq-o(PFQKkG%Txb-BX-sb<68LsX6cISTnv|`3CH3X-lmZrZb!Q7*6{V_>>EG*@W zgUp}B>I(K8uDcqlj*?e@#^QH&^6&g!5*IyjIs4jhhyX5%F%Q@o}!tqMkc$HSX+SLC~Ilyqt?)zAG9RV)Hzd3-Cpj$Y} ze+mJoX#hjGSUEsTJ;|dA!i^+{L&;%Pu`TZ8h-`9Hcx>=0`96glgN+TrQeu_JWYO50 z%qmKPC^>1B7!Hj~tsL$YJ>rc0fm%(n;ssIp@*5>r`$(Btnf@&8xegHeIFh ztd$dJl%^1urZP>kqVbLcQj1zSS*Cb0g6A_8hN1)T_3%|Hml-Dh*s1grIpF+O&IL56 z!kvdv&uumhXac~b=m^8LI9JQ`lYXiG(}@94t{HdGWIay-ouu+F=Q@XmX(AD6G+q}* zoGp!akv*|vD(k*jwkDnQ2Ecn=Ip@HgBYzKiKxgmt%?hv15?jf_DQ6e$v6rwm+t4tn z9(XOCq(4nE%#mAI&53Bsf-h#5isj@(L6v?yX&ClyXoA?Od@C6BrE~sZj$9Zl{zbJg zs+wwC3{{^->;b^=o-~bVfQLITyg2g;XyF`1Wo&Z3M&$2J>EWvF2y1>>A>P*7At*#Hh|-Rzi7Oqp18LdcpsGYp`%py zVuhM?r5OMQ!*fubUec3WXIWSegFTuCJP|8aw=CApE!0;5EmI30A@X;X^2OE)*T)kt zs^nijDEHmU&r%W;L-0;gK@#pPlAO1ZD)+NWgTr2$1*XWnVN1+0^!)3g<3M1V`h zwcLv9GZl5%tXh_hg$ARL9IH$O?;0ASWCuDf%Ml=28G5nuj(;A;k0+1@)I)H^ujPW* zE1@%hOsmS<@s$@p(VRHBuxsohc5HBW*Z~7(PG>U!l+bV*pM#6w7`6|=@J7poCl_m5 za%&F&oN#J3+dIHN8akb%L*q@dD}5|p`fiU&^ENylYvnwkL-Mm>0;6#R8t)B2eSlH}kgf4fecr+h ztu(#?<5H!@JY6_bm0z%0bB!LyQqac%Y=PNaJ7^#k;qE~vU7+#CArb9+PmDGYrl)~| zXr6A`g+zDWS5%%x6^LqNIlT8tA@r$nZT|UM;0^3jlkhX$9RX?&+7|K_Y09Cno}B^s>mp)LPzj*R~;8b0E0!=rn0u*(ksla$pCUt!m59gO_d;Q30U* zDYk>)r=>6AA(4dvl#YVa?X~`86fRiZI(tDjNHn+b><{+v8PWq9Z!oH*NvZ2?ZTeYe zZ~;s2hSRxUA>6I#fC3zk2(1;k)mgaOrLJ5Q`2$2Y-Q0o;}7EIb`H5vnG2UpGS^Muk#*{+~LOpm%H zm+Ym6=$Uxn7%*hEch|iH`LLCJzwp40y`ZWj;2r6fT}J@6&&WTu^EWU*oUON`55Cm* z#j;Pdrc*}(0=hw~8wD1$HZ9chUPdBbPt_u;7wh&%eKQsU?@ zQQ3p~W}rvc7iEMFa}5ScnsT0u3TgAi@=H(0Xi*}iMn6r9#eyRq-!)#?&TL} z9f>!NFD!5Nv?H5H>#2@74bQN66=(JJm9YNn0#V=(3{R>UzXqB& z2M61wvNLQFE`6OubaKDP!M@X58W}uA!Mp@9{^d;{zdw_Reby=i54$?z>SA+hIj_As z;=I(@S`F_CH!MQERRPJn;0|0jehYt?k@@wR;j4CO*It3me8r?Vqb1Nm50^zf8!DJ% zo^^(;41BN%g}-<;!(!g+*+~H211dMLccxAqk(eR_A+`BCG=M;b*m-ZvNy6ibIk z@O;4m3->q|81QyR+(P#J#)tXuQVRjT&;tzb7$+=-R`jB$+@p5tM%BXR*Lg+~5RPf= zr-4{R*aC8D4nEg+cR23M>ww|K!#|5#7vEaofIeuRJ-g}5=vJ9C;|SI9qXr{|f2K|C z%Xy88sr5~i3m_o|_m8~7qzB8oZ9I>fK|7e9te#w;eYt|J#%4~&3$cmk+^~$%euKGX z!@!b-;16T@AE*qj0N<6hoE1#mr?7|VuE|fN@6H45*&nLW%vOA~VX$V2!GZP9XLw+{ zh`s>d*9T~*rEC`!QTpxoGtO^5;Y^+{0jog!PX(zR-JS*o-pNydhf_t_tMhGNIv>J8 zdYqfIk1?(zs(({Z0b>cO%f}0QUAEXrX!shP=Z!lAfkX(}{bRmQhqCAOz!~oFibakN z8m|ce_VnJy^E(_P44!!^REV}h?OCtm*=P*RpaMRTX6BwB&RL$DknG&(`n%C<06IDM zsYbFhpRzgndh>PNM(4K;*4cdj!1>H?q@xVzG@5q-!R3gKm!k48086)@CH%bieLtD? zo1abO8Ec-Bae$$5M7*!pVeHj?FIojYK=7C5N~>5w%B7c zPDY&(HCIEL2^ug>($ssY^`!T?r-avBD36=QqkMZWJ?V@4+et+QrShuJGg>0n)5?!c zxeK-NWWG+4kRMq3^=+YFCL=Gj!bm8wZe;nSrMVTdWH2Y)^hl+V{6Sf{8XV)xXagJr zoIg^fBraz(ly8rCnMAT|&LK-!2V>(nEb8JE^GrAs{kp*QIC7rMR4A{(zCz9SXNOg? zRfc{DP(7P}O#`0$QSLdvCh6dDdV|BiXW~n)IF#R0674$um`xtJ|Gh^>!Y-yj*YBl9`30o%y5db@ zymd908NLWU6BLbH<+#p$Fv5=)t}Q7DBB6(07IB|TKcoGG_%=iIPsfvDHMB2g=#g~$ z+hLhoUQZpwzpAZbD0wPPblYs8cZ5FFbo3-q0pjD@l8r%p~^&6|W= z#13D2!++P{SP8p~Yz#;ce{t-Z5XzCtUZ8We>pcJ6U!sj~ws!0*lZE1k%ir{085Z;q zovd|sGn-xs4~-QYxm+ah!clxD?o+WL+bJbtz@{IY-QDn$!`En!PwZ54&!mEzH*XeS z0|_!$$9xF*kHaI5E)v@&MVEZvwpW)d%KH+7HtpHgcrNvPkr0S{XI><&p<_`XA=zhK zWNIu}+ItFqv3)IrmYM5c3yUYzWNy?vpryIhjaeY$G7i;2_Q|J0Kl|LS^TAVe2ngH8 zF|u$0%BWZ)pc0m6=DBAnmu9=g0 ztIC`2U>H9W7Dgd*r@(FKLke~8sheL*binrRU%AJ7AQf_WHLkSf+hNasTJkt%g~%*V zXdBALm)|Sod%;mx%S!ZX1kaDW5Ssp4u6jVrT|Fp$bbLrtDFn>tg?l-C`Z7Hrjx0gmEQ<-G=ME^E==kwBnA%I6 z9q!@bX5+=mEjv@8WHDBR5nXNP3%jfoa?(S*e3+l#EfOqb+=JI5E=vxwKkrG#0gorL z%jljGGvY5!!I%bKt+TS5?P0{o9Rm3U{&=#x7pHjW7=f&VmySZy;_e-bsn|c=fA1SO z#(UKS4(_coKecX8jn61kII&>;p``!9QyXss8yhR@3ol1Ly?w|xt!O!TF6P>*iPeIR z@85ej;Z=fWbq2+?euyf8+zIzM_x2%4D4x*z$6Ig{vzEU;a72879mRCNXvAKK(QEXF zNNMJ5MBf}G1k%`pU#_4m>xZe!>X@;$lMg-w~OdK z(s(Q-XrPZ%dBjNJS!(hbBB0?A_26yTw>*9YN_CoxaZ&>M=&dkA(jLq$=X|glhD9_6 zN7Qq@U*a6G9=w@(`mNk0vwh^1ciTQBQ6J5$oO@cwLvtrPeNK{n)B{MN`JeQz>As(S zeP=MV@Mq^WgWc&j_rYN_4t-x^kr_JKJgh{h%hz0cW-NsiRwl3SXXP|Ao;4U&aiYub z?46mnMc`XiX8Qi;vSubK&2QCQ=<;`LnVEb{y7lm?{`HIRXWq38-m1IRb^Xfj%=>Qe z?fO{#8=fMwQ$yyr8y|Gt@X?-~enYz5RIVT3?=(B}e(-klKlJ*Y+1Ul~oz{Qo^{m;s zFD!a}vMca*%j}2mEPDNueh}-?n%_0wW#efCMAy159R9u2&HV5vDCSAtB8Q>2r)i%p zkbIV)vLe9SZ&`Dfiop&(7cM zC3bQ3{FZ7MYLVc#r?V2RFG2saeyLJk=vBte5Q=aw@1MPbN~l4Rr~yia^Vc$XmtgoU zFZu8F@*jE6j=FZ6g!k+cpj&&! zU>PDfzU<3bV`i|KbB0-B^>hA?=a=T5UpahkcnqR#$0kkT!0H%b8A9%S!k)*3zfy&L zQe*ua%q~rsUD+%q2N?zDmT2HnLnJz?kpMt_KM zR;XoGAG_jc|Ix3(wc+^|cUK3QIBtu(=uF>B7XGFR;TaZ@m!L`cgSU4E-T1JCW5U>a zaA?&LEDaaf3MTlPUj%UwRz+h$Vj_uRlZ4JHy7FDDN{@&5R4}w}gmxW=*9Pj3dF6%S zi3m!b$xQX}U`kJjI3Ah*Q!GZya`dNi{AnF15=V?~5X-lu78`Mw`o)xPTCGH6Z6d6% z`HlMd1FAA4+`08Duo)cjAW5sZylL~N%Fz71^y=!|;;MMk%NI^!d3XRmrY)z$F4*c94#>?LG&$RVur!p=y6u`z)rB@2!&fliE9zUcijc?jKc3*aVNua=H_0_dp{^aSQAzOck7kO0I4y) zM}L%GW;jdD?ZkcQ)L&x>>a7qj97q)hvT>LEIML}c{bJ@27HoeEJN$aAEmL382BUoB z!;;j(Qqp&Sz{9nU7V2v}+IB~e@>@RL%+qQ&ebYX2-}H zt0=K9a#5B29lg`oR=~y$=;1N$g0-RyC7Nh~eNolelbu)KdI;g10{^;z^rs=GrE>*0 zI1gDbt`7BRZ8)MZQXMjrcQK9z=#l_l%_*U)u{?GH=Db#j-4V0#Qy7H>B2oV`6!ZL? zQ7qAbLjV#>yox3BwBmwsq_MK2;#Ndr_Ibg(ow?K1eTXAoX(EzS1luVv7Dpb|)zkNU z6Nl`zJ(FZ7G$|bkJ$K^5UF-$QrE!I)%JQDFiue+EL1`P3qVcifoc`n`Ax(`FN-7?2 z3{btjKiWx)odb+Xg~$@6oAl6^NWv=J(4`CyWg{KdEyx`yw(K-MQFWceF~AJ!`l!Oe z%!!D0AmN-YU1q!e>K#0mScf90xhtEFlaF159yg{S++V#@t83UB>BA9ks<04i9J9dH#ZKN$A=JdeS`3L&Yk_(ia zg7<`~_mpgx+7r-+u^^FbvJLAD`!L(h#`0L_nc6eXuhu3SDQaX&%|DHVzsVIHJRBOM z$^b_|#ZS;77P#j{o7F{}HQxEI_^R1zmMS?i+x2y}8=h+z(GJGAU>S-yex-0a*;~h& z;#np5(G*K}efHC}XL2%Q>U{rCW!lYiSj45if_KKRYCLD=aN<>XSqjsFAuf$zJBfxw z?)8JA)t5!9({!uJfXZq0xvboa`X{R9zRtb*WO`TU!wARRqX+Y`s{@;7J_yt1V;biP z;juw`B#G0M!LHefQ}Zj&Yi#0O8}=qLeAQ=R4@pQ7t+ig!i)2}yj6mr4n{$_b|EV=? zvX_oYC$9EZKc9odf|^H-)QGBo9qi5)!;)d=6dv+pTQB+C>VG{W2qb}6X8UZs2-&@M zY&$LpNs*^P&~2bVr7L|vftg&vx4?7QJvWmb_q?g|u(|@yc{jA5du6E;;UY!$FYJhb z=Nn^+79&mw ztKn@om|_0Q3v2FKcfm7u@sq_#=%v3aCKa#4aAcL^6g}x>0=?dG-c#e(G2`npX zKG6(F!~|$sgRHQPFBJ%aJ%BIDHwf4ptk1(VLqj>}Y&MwKSO(E(7-|{*F431jT#dPi zrI=GT2}#0e@+2C?#iBeoRf z`%qR~!1s7dB84J%e~T$<0R@vmyQJ+{G*PK2SX7Zi%n+*S1T8@k&2e7|xX@GD6wPpu zCk}La_@DFaJa2AF$s|Zr5>){)vFIQRr_FEgV)P6tCvWsCu^9II-u+Iu zei9~cT`>f@X+zlzDXi;?-zhl?FJwYWmxP1J?1OlDNDz5QfOvzDc}2+ca5spY-+7WZd4*tkm!Ei@ zHwdGrbc#s&gb;e3KM0iHZ-ZDsrC)NUPYCv|`jnq{i#U0(KL{!GcY`R!qb2&Wz=8?K z2nQGNVuu6_AckKjhCfhG(a0BgHZU+KH7{(ZAd7D1n=;OkZ*(-eDW@Q zh9HA~=X8a5d=H-(q)mv)H;88q_si$&qdok@AMAwy{mh4i(XVidXncg2diq{`h5$>! zPl(V@h(IuL|4yKc*jEVpMu-r)ZbN`=-tT&ZP^N=;g42(Du_%LP08?g|eTIPh_pY&p zPymK#elWdoj7S9Er-&3lg+{o3CAoa8M+h0e?vwa~X2=9UkOD zRYK@#kw7~IL!mU7Ns~ZIEkDK-2}u&BkD(Ga@|c2@B8mtb8dO5_C{2z-4;n?vD5t1E7Wg{hmF3M1 z?$M;Fk3j0KKZ1^XQM~n|PqaIS#MX-q$PIlzh7>|zubCA=h%kQKJmX9C^jg+L=h zQX)bprr5%YD-CFnj5HqA;fEnY(xZit010A!q3j3|h8haQ3?vd5SrCyLHB+8IO#c}hk|rc;79>eR zo@ms@BSi|72c3!*BnKqQR0fes%0RM*P>h235f-EnLy8}5Kq=53l~~%9rYt7Z>8GKN zDv&Y@8I)>Wm0p@@qZZnVX+g1e1gWG48A=Q#LKH-+MxKyz5U`Vev + Offline collaboration conflict resolver demo + A simple diagram showing offline edits rebased onto server operations with an audit report. + + + Offline queue + off-1 abstract edit + off-2 locked method edit + off-3 suggestion resolve + + Server revision + remote abstract update + methods section locked + open review suggestion + + + Rebase result + 2 applied + 1 manual review + restore snapshot saved + + + Conflict audit report + status: manual-review-required + blocker: section-lock on methods, owned by reviewer-2 + audit hash and restore snapshot make sync decisions reviewable + + + + + + diff --git a/offline-collaboration-conflict-resolver/docs/issue-12-requirement-map.md b/offline-collaboration-conflict-resolver/docs/issue-12-requirement-map.md new file mode 100644 index 0000000..19f7f7f --- /dev/null +++ b/offline-collaboration-conflict-resolver/docs/issue-12-requirement-map.md @@ -0,0 +1,19 @@ +# Issue #12 Requirement Map + +This module is a focused offline collaboration slice for the real-time collaborative research editor. It complements the broader editor modules by handling the state that accumulates when a researcher edits a manuscript while disconnected. + +| Issue #12 requirement | Implementation evidence | +| --- | --- | +| Multi-user editing with live collaboration semantics | `queueOfflineOperation` records actor-scoped client edits, and `rebaseOfflineQueue` replays them on top of remote server operations. | +| Inline comments, suggestions, and change tracking | Offline comments and suggestions are merged idempotently; suggestion resolution is audited and blocked if the suggestion was already removed. | +| Locking / unlock modes for controlled sections | `detectConflict` blocks offline edits when a section has an active lock owned by another collaborator. | +| Continuous autosave with local caching | `createSnapshot` creates restore-ready snapshots with content hashes before or after sync. | +| Fine-grained version tracking | Each block carries a version, and stale offline edits are audited when the server version has advanced. | +| Restore previous versions or compare changes | The post-rebase snapshot contains a `restorePayload`, stable `contentHash`, and audit hash for comparison. | +| Integrated review workflow | Conflict reports identify manual-review blockers before risky edits overwrite locked sections or resolved suggestions. | + +## Reviewer Notes + +- The implementation is dependency-free and can be reviewed with stock Node.js. +- It is intentionally not a UI mock. The value is deterministic sync behavior that a real editor UI or API can call. +- The demo includes one applied stale edit, one locked-section conflict, and one accepted offline suggestion resolution. diff --git a/offline-collaboration-conflict-resolver/package.json b/offline-collaboration-conflict-resolver/package.json new file mode 100644 index 0000000..02a487d --- /dev/null +++ b/offline-collaboration-conflict-resolver/package.json @@ -0,0 +1,13 @@ +{ + "name": "offline-collaboration-conflict-resolver", + "version": "1.0.0", + "description": "Offline edit rebase and conflict audit module for the SCIBASE real-time collaborative research editor bounty.", + "main": "src/conflict-resolver.js", + "type": "commonjs", + "scripts": { + "check": "node --check src/conflict-resolver.js && node --check scripts/demo.js && node --check test/conflict-resolver.test.js", + "demo": "node scripts/demo.js", + "test": "node test/conflict-resolver.test.js" + }, + "license": "Apache-2.0" +} diff --git a/offline-collaboration-conflict-resolver/scripts/demo.js b/offline-collaboration-conflict-resolver/scripts/demo.js new file mode 100644 index 0000000..669747d --- /dev/null +++ b/offline-collaboration-conflict-resolver/scripts/demo.js @@ -0,0 +1,89 @@ +"use strict"; + +const { + queueOfflineOperation, + rebaseOfflineQueue, + buildConflictReport +} = require("../src/conflict-resolver"); + +const baseDocument = { + id: "manuscript-alpha", + baseRevision: "rev-17", + locks: [ + { sectionId: "methods", ownerId: "reviewer-2", status: "active" } + ], + blocks: [ + { + id: "abstract", + sectionId: "frontmatter", + type: "markdown", + content: "We introduce a catalyst screening workflow.", + citations: ["doi:10.1000/base"], + version: 2 + }, + { + id: "method-step", + sectionId: "methods", + type: "notebook", + content: "Run catalyst notebook", + version: 3 + }, + { + id: "discussion", + sectionId: "discussion", + type: "latex", + content: "The yield improves by 12%.", + suggestions: [{ id: "sug-1", status: "open", text: "Mention confidence interval." }], + version: 1 + } + ] +}; + +const serverOperations = [ + { + id: "srv-1", + actorId: "editor-remote", + kind: "update-block", + blockId: "abstract", + content: "We introduce a reproducible catalyst screening workflow." + } +]; + +let offlineQueue = []; +offlineQueue = queueOfflineOperation(offlineQueue, { + id: "off-1", + actorId: "author-1", + blockId: "abstract", + expectedVersion: 2, + content: "We introduce a reproducible catalyst screening workflow for open labs.", + citations: ["doi:10.1000/open-labs"] +}); +offlineQueue = queueOfflineOperation(offlineQueue, { + id: "off-2", + actorId: "author-1", + blockId: "method-step", + expectedVersion: 3, + content: "Run catalyst notebook with seeded environment capture." +}); +offlineQueue = queueOfflineOperation(offlineQueue, { + id: "off-3", + actorId: "author-3", + blockId: "discussion", + kind: "resolve-suggestion", + suggestionId: "sug-1", + expectedVersion: 1 +}); + +const result = rebaseOfflineQueue(baseDocument, serverOperations, offlineQueue); +const report = buildConflictReport(result); + +console.log(JSON.stringify({ + report, + applied: result.applied, + blocked: result.blocked, + snapshot: { + id: result.snapshot.id, + contentHash: result.snapshot.contentHash, + blockCount: result.snapshot.blockCount + } +}, null, 2)); diff --git a/offline-collaboration-conflict-resolver/src/conflict-resolver.js b/offline-collaboration-conflict-resolver/src/conflict-resolver.js new file mode 100644 index 0000000..636d318 --- /dev/null +++ b/offline-collaboration-conflict-resolver/src/conflict-resolver.js @@ -0,0 +1,267 @@ +"use strict"; + +const crypto = require("crypto"); + +function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + +function normalizeDocument(document) { + if (!document || !Array.isArray(document.blocks)) { + throw new Error("document.blocks must be an array"); + } + + const blocks = document.blocks.map((block, index) => ({ + id: block.id || `block-${index + 1}`, + type: block.type || "markdown", + sectionId: block.sectionId || "body", + content: block.content || "", + citations: Array.isArray(block.citations) ? [...block.citations] : [], + comments: Array.isArray(block.comments) ? clone(block.comments) : [], + suggestions: Array.isArray(block.suggestions) ? clone(block.suggestions) : [], + version: Number.isInteger(block.version) ? block.version : 1 + })); + + return { + id: document.id || "research-document", + baseRevision: document.baseRevision || "rev-0", + blocks, + locks: Array.isArray(document.locks) ? clone(document.locks) : [], + snapshots: Array.isArray(document.snapshots) ? clone(document.snapshots) : [] + }; +} + +function hashPayload(value) { + return crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex"); +} + +function createSnapshot(document, label) { + const normalized = normalizeDocument(document); + return { + id: `snapshot-${hashPayload(normalized).slice(0, 12)}`, + label, + baseRevision: normalized.baseRevision, + blockCount: normalized.blocks.length, + contentHash: hashPayload(normalized.blocks), + restorePayload: clone(normalized.blocks) + }; +} + +function queueOfflineOperation(queue, operation) { + if (!operation || !operation.id || !operation.actorId || !operation.blockId) { + throw new Error("offline operation requires id, actorId, and blockId"); + } + + return [...queue, { + kind: "update-block", + timestamp: "offline", + expectedVersion: 1, + ...operation + }]; +} + +function findBlock(document, blockId) { + return document.blocks.find((block) => block.id === blockId); +} + +function isSectionLocked(document, sectionId, actorId) { + return document.locks.find((lock) => + lock.sectionId === sectionId && + lock.status === "active" && + lock.ownerId !== actorId + ); +} + +function applyServerOperation(document, operation) { + const next = clone(document); + const block = findBlock(next, operation.blockId); + + if (!block) { + next.blocks.push({ + id: operation.blockId, + type: operation.type || "markdown", + sectionId: operation.sectionId || "body", + content: operation.content || "", + citations: [], + comments: [], + suggestions: [], + version: 1 + }); + return next; + } + + if (operation.kind === "delete-block") { + next.blocks = next.blocks.filter((candidate) => candidate.id !== operation.blockId); + return next; + } + + if (operation.content !== undefined) { + block.content = operation.content; + } + if (operation.citations) { + block.citations = [...new Set([...block.citations, ...operation.citations])]; + } + block.version += 1; + return next; +} + +function detectConflict(document, operation) { + const block = findBlock(document, operation.blockId); + if (!block) { + return { + type: "missing-block", + severity: "high", + reason: `Block ${operation.blockId} no longer exists on the server revision.` + }; + } + + const lock = isSectionLocked(document, block.sectionId, operation.actorId); + if (lock) { + return { + type: "section-lock", + severity: "high", + reason: `Section ${block.sectionId} is locked by ${lock.ownerId}.` + }; + } + + if (operation.expectedVersion !== undefined && operation.expectedVersion < block.version) { + return { + type: "stale-version", + severity: "warning", + reason: `Offline edit expected version ${operation.expectedVersion}, but server block is version ${block.version}.` + }; + } + + if (operation.kind === "resolve-suggestion" && !block.suggestions.some((item) => item.id === operation.suggestionId)) { + return { + type: "missing-suggestion", + severity: "medium", + reason: `Suggestion ${operation.suggestionId} was already resolved or removed.` + }; + } + + return null; +} + +function mergeUpdate(block, operation) { + const next = clone(block); + if (operation.content !== undefined) { + next.content = operation.content; + } + if (Array.isArray(operation.citations)) { + next.citations = [...new Set([...next.citations, ...operation.citations])]; + } + if (operation.comment) { + const exists = next.comments.some((comment) => comment.id === operation.comment.id); + if (!exists) { + next.comments.push(operation.comment); + } + } + if (operation.suggestion) { + const exists = next.suggestions.some((suggestion) => suggestion.id === operation.suggestion.id); + if (!exists) { + next.suggestions.push(operation.suggestion); + } + } + next.version += 1; + return next; +} + +function applyOfflineOperation(document, operation) { + const next = clone(document); + const index = next.blocks.findIndex((block) => block.id === operation.blockId); + if (index === -1) { + return next; + } + + if (operation.kind === "resolve-suggestion") { + next.blocks[index].suggestions = next.blocks[index].suggestions.map((suggestion) => + suggestion.id === operation.suggestionId + ? { ...suggestion, status: "accepted", resolvedBy: operation.actorId } + : suggestion + ); + next.blocks[index].version += 1; + return next; + } + + next.blocks[index] = mergeUpdate(next.blocks[index], operation); + return next; +} + +function rebaseOfflineQueue(baseDocument, serverOperations, offlineQueue) { + let serverDocument = normalizeDocument(baseDocument); + const audit = []; + const applied = []; + const blocked = []; + + for (const operation of serverOperations) { + serverDocument = applyServerOperation(serverDocument, operation); + } + + for (const operation of offlineQueue) { + const conflict = detectConflict(serverDocument, operation); + if (conflict && conflict.severity !== "warning") { + blocked.push({ operationId: operation.id, conflict }); + audit.push({ + operationId: operation.id, + actorId: operation.actorId, + status: "blocked", + conflict + }); + continue; + } + + serverDocument = applyOfflineOperation(serverDocument, operation); + applied.push(operation.id); + audit.push({ + operationId: operation.id, + actorId: operation.actorId, + status: conflict ? "applied-with-warning" : "applied", + blockId: operation.blockId, + warning: conflict || undefined + }); + } + + return { + document: serverDocument, + applied, + blocked, + audit, + snapshot: createSnapshot(serverDocument, "post-offline-rebase") + }; +} + +function buildConflictReport(result) { + const blockers = result.blocked.map((entry) => ({ + operationId: entry.operationId, + type: entry.conflict.type, + severity: entry.conflict.severity, + reason: entry.conflict.reason + })); + const warnings = result.audit + .filter((entry) => entry.warning) + .map((entry) => ({ + operationId: entry.operationId, + type: entry.warning.type, + reason: entry.warning.reason + })); + + return { + status: blockers.length === 0 ? "ready-to-sync" : "manual-review-required", + appliedCount: result.applied.length, + blockedCount: blockers.length, + warningCount: warnings.length, + blockers, + warnings, + auditHash: hashPayload(result.audit), + restoreSnapshotId: result.snapshot.id + }; +} + +module.exports = { + normalizeDocument, + createSnapshot, + queueOfflineOperation, + rebaseOfflineQueue, + buildConflictReport +}; diff --git a/offline-collaboration-conflict-resolver/test/conflict-resolver.test.js b/offline-collaboration-conflict-resolver/test/conflict-resolver.test.js new file mode 100644 index 0000000..0debfa4 --- /dev/null +++ b/offline-collaboration-conflict-resolver/test/conflict-resolver.test.js @@ -0,0 +1,152 @@ +"use strict"; + +const assert = require("assert"); +const { + queueOfflineOperation, + rebaseOfflineQueue, + buildConflictReport, + createSnapshot +} = require("../src/conflict-resolver"); + +function fixture() { + return { + id: "doc-1", + baseRevision: "rev-1", + locks: [{ sectionId: "locked", ownerId: "editor-b", status: "active" }], + blocks: [ + { + id: "intro", + sectionId: "body", + type: "markdown", + content: "Initial claim.", + citations: [], + version: 1 + }, + { + id: "locked-block", + sectionId: "locked", + type: "markdown", + content: "Final figure caption.", + citations: [], + version: 1 + }, + { + id: "review", + sectionId: "review", + type: "markdown", + content: "Review note.", + suggestions: [{ id: "s1", status: "open", text: "Add limitation." }], + version: 1 + } + ] + }; +} + +function testRebasesOfflineUpdateAfterServerChange() { + let queue = []; + queue = queueOfflineOperation(queue, { + id: "offline-intro", + actorId: "editor-a", + blockId: "intro", + expectedVersion: 1, + content: "Initial claim with replication package.", + citations: ["doi:10.5555/repro"] + }); + + const result = rebaseOfflineQueue(fixture(), [ + { + id: "server-intro", + actorId: "editor-b", + kind: "update-block", + blockId: "intro", + content: "Initial claim with shared dataset." + } + ], queue); + + assert.deepStrictEqual(result.applied, ["offline-intro"]); + assert.strictEqual(result.blocked.length, 0); + assert.strictEqual(result.document.blocks[0].content, "Initial claim with replication package."); + assert.deepStrictEqual(result.document.blocks[0].citations, ["doi:10.5555/repro"]); + assert.strictEqual(buildConflictReport(result).warningCount, 1); + assert.ok(result.snapshot.contentHash); +} + +function testBlocksLockedSectionConflict() { + let queue = []; + queue = queueOfflineOperation(queue, { + id: "offline-locked", + actorId: "editor-a", + blockId: "locked-block", + expectedVersion: 1, + content: "Changed final figure caption." + }); + + const result = rebaseOfflineQueue(fixture(), [], queue); + const report = buildConflictReport(result); + + assert.deepStrictEqual(result.applied, []); + assert.strictEqual(result.blocked[0].conflict.type, "section-lock"); + assert.strictEqual(report.status, "manual-review-required"); + assert.strictEqual(report.blockedCount, 1); +} + +function testSuggestionResolutionIsSafe() { + let queue = []; + queue = queueOfflineOperation(queue, { + id: "offline-suggestion", + actorId: "reviewer-a", + blockId: "review", + kind: "resolve-suggestion", + suggestionId: "s1", + expectedVersion: 1 + }); + + const result = rebaseOfflineQueue(fixture(), [], queue); + const suggestion = result.document.blocks + .find((block) => block.id === "review") + .suggestions.find((item) => item.id === "s1"); + + assert.deepStrictEqual(result.applied, ["offline-suggestion"]); + assert.strictEqual(suggestion.status, "accepted"); + assert.strictEqual(suggestion.resolvedBy, "reviewer-a"); +} + +function testMissingSuggestionIsAudited() { + let queue = []; + queue = queueOfflineOperation(queue, { + id: "offline-missing-suggestion", + actorId: "reviewer-a", + blockId: "review", + kind: "resolve-suggestion", + suggestionId: "missing", + expectedVersion: 1 + }); + + const result = rebaseOfflineQueue(fixture(), [], queue); + const report = buildConflictReport(result); + + assert.strictEqual(result.blocked[0].conflict.type, "missing-suggestion"); + assert.ok(report.auditHash); +} + +function testSnapshotIsRestoreReady() { + const snapshot = createSnapshot(fixture(), "before-sync"); + assert.strictEqual(snapshot.label, "before-sync"); + assert.strictEqual(snapshot.blockCount, 3); + assert.ok(Array.isArray(snapshot.restorePayload)); + assert.ok(snapshot.contentHash.length >= 32); +} + +const tests = [ + testRebasesOfflineUpdateAfterServerChange, + testBlocksLockedSectionConflict, + testSuggestionResolutionIsSafe, + testMissingSuggestionIsAudited, + testSnapshotIsRestoreReady +]; + +for (const test of tests) { + test(); +} + +console.log(`${tests.length} conflict resolver tests passed`); From c8172a692a160a81b2979ff2ad10333b81f04c58 Mon Sep 17 00:00:00 2001 From: YX Date: Fri, 15 May 2026 07:12:01 +0800 Subject: [PATCH 2/2] test: make offline operation replay idempotent --- .../README.md | 5 ++-- .../docs/issue-12-requirement-map.md | 2 +- .../src/conflict-resolver.js | 21 ++++++++++++++++ .../test/conflict-resolver.test.js | 25 +++++++++++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/offline-collaboration-conflict-resolver/README.md b/offline-collaboration-conflict-resolver/README.md index 1b38692..b1fd647 100644 --- a/offline-collaboration-conflict-resolver/README.md +++ b/offline-collaboration-conflict-resolver/README.md @@ -11,9 +11,10 @@ It is intentionally dependency-free so reviewers can run it with stock Node.js. - Rebase of offline edits on top of server operations. - Section lock checks so protected manuscript areas are not overwritten. - Safe inline comment and suggestion merging. +- Idempotent operation replay so retried offline operations do not apply twice. - Stale version and missing suggestion conflict reporting. - Restore-ready snapshots with content hashes. -- Reviewer-facing audit reports with stable audit hashes. +- Reviewer-facing audit reports with stable audit hashes and duplicate replay counts. ## Demo @@ -36,7 +37,7 @@ npm run demo ## Files - `src/conflict-resolver.js` - core queue, rebase, snapshot, and report logic. -- `test/conflict-resolver.test.js` - focused tests for rebase, locks, suggestions, and snapshots. +- `test/conflict-resolver.test.js` - focused tests for rebase, locks, suggestions, duplicate replay, and snapshots. - `scripts/demo.js` - CLI demo with sample scientific manuscript blocks. - `docs/issue-12-requirement-map.md` - mapping from issue requirements to implementation evidence. diff --git a/offline-collaboration-conflict-resolver/docs/issue-12-requirement-map.md b/offline-collaboration-conflict-resolver/docs/issue-12-requirement-map.md index 19f7f7f..80cc9e1 100644 --- a/offline-collaboration-conflict-resolver/docs/issue-12-requirement-map.md +++ b/offline-collaboration-conflict-resolver/docs/issue-12-requirement-map.md @@ -5,7 +5,7 @@ This module is a focused offline collaboration slice for the real-time collabora | Issue #12 requirement | Implementation evidence | | --- | --- | | Multi-user editing with live collaboration semantics | `queueOfflineOperation` records actor-scoped client edits, and `rebaseOfflineQueue` replays them on top of remote server operations. | -| Inline comments, suggestions, and change tracking | Offline comments and suggestions are merged idempotently; suggestion resolution is audited and blocked if the suggestion was already removed. | +| Inline comments, suggestions, and change tracking | Offline comments, suggestions, and retried operation ids are merged idempotently; suggestion resolution is audited and blocked if the suggestion was already removed. | | Locking / unlock modes for controlled sections | `detectConflict` blocks offline edits when a section has an active lock owned by another collaborator. | | Continuous autosave with local caching | `createSnapshot` creates restore-ready snapshots with content hashes before or after sync. | | Fine-grained version tracking | Each block carries a version, and stale offline edits are audited when the server version has advanced. | diff --git a/offline-collaboration-conflict-resolver/src/conflict-resolver.js b/offline-collaboration-conflict-resolver/src/conflict-resolver.js index 636d318..fa6808a 100644 --- a/offline-collaboration-conflict-resolver/src/conflict-resolver.js +++ b/offline-collaboration-conflict-resolver/src/conflict-resolver.js @@ -193,12 +193,24 @@ function rebaseOfflineQueue(baseDocument, serverOperations, offlineQueue) { const audit = []; const applied = []; const blocked = []; + const seenOperationIds = new Set(); for (const operation of serverOperations) { serverDocument = applyServerOperation(serverDocument, operation); } for (const operation of offlineQueue) { + if (seenOperationIds.has(operation.id)) { + audit.push({ + operationId: operation.id, + actorId: operation.actorId, + status: "skipped-duplicate", + blockId: operation.blockId + }); + continue; + } + seenOperationIds.add(operation.id); + const conflict = detectConflict(serverDocument, operation); if (conflict && conflict.severity !== "warning") { blocked.push({ operationId: operation.id, conflict }); @@ -245,14 +257,23 @@ function buildConflictReport(result) { type: entry.warning.type, reason: entry.warning.reason })); + const skipped = result.audit + .filter((entry) => entry.status === "skipped-duplicate") + .map((entry) => ({ + operationId: entry.operationId, + type: "duplicate-operation", + reason: "Operation id was already replayed in this offline sync batch." + })); return { status: blockers.length === 0 ? "ready-to-sync" : "manual-review-required", appliedCount: result.applied.length, blockedCount: blockers.length, warningCount: warnings.length, + skippedCount: skipped.length, blockers, warnings, + skipped, auditHash: hashPayload(result.audit), restoreSnapshotId: result.snapshot.id }; diff --git a/offline-collaboration-conflict-resolver/test/conflict-resolver.test.js b/offline-collaboration-conflict-resolver/test/conflict-resolver.test.js index 0debfa4..d79354f 100644 --- a/offline-collaboration-conflict-resolver/test/conflict-resolver.test.js +++ b/offline-collaboration-conflict-resolver/test/conflict-resolver.test.js @@ -111,6 +111,30 @@ function testSuggestionResolutionIsSafe() { assert.strictEqual(suggestion.resolvedBy, "reviewer-a"); } +function testDuplicateOfflineOperationIsSkipped() { + let queue = []; + const operation = { + id: "offline-duplicate", + actorId: "editor-a", + blockId: "intro", + expectedVersion: 1, + content: "Initial claim with one offline replay." + }; + queue = queueOfflineOperation(queue, operation); + queue = queueOfflineOperation(queue, operation); + + const result = rebaseOfflineQueue(fixture(), [], queue); + const report = buildConflictReport(result); + const intro = result.document.blocks.find((block) => block.id === "intro"); + const duplicateAudit = result.audit.find((entry) => entry.status === "skipped-duplicate"); + + assert.deepStrictEqual(result.applied, ["offline-duplicate"]); + assert.strictEqual(intro.version, 2); + assert.strictEqual(duplicateAudit.operationId, "offline-duplicate"); + assert.strictEqual(report.skippedCount, 1); + assert.strictEqual(report.skipped[0].type, "duplicate-operation"); +} + function testMissingSuggestionIsAudited() { let queue = []; queue = queueOfflineOperation(queue, { @@ -141,6 +165,7 @@ const tests = [ testRebasesOfflineUpdateAfterServerChange, testBlocksLockedSectionConflict, testSuggestionResolutionIsSafe, + testDuplicateOfflineOperationIsSkipped, testMissingSuggestionIsAudited, testSnapshotIsRestoreReady ];