{HVA#PTkSVygy*^?eojTMiV)f`JV`s9-nPZt!f)QZLvUux6_4(N=9lHd
zN13J(y_oZUA2KZ%Wk0>fv|pt@Kofnl<9*
zk4T62WIE90=@8UKh{DQ~K8@dZLVT7Hjpl1+D)D^iqkG4~Rd%zA-WY>lN2mvpTob&k
zxbb=qr|IMDu-8kIJG-^-H#0DHv8k`-b)W7o&FBqkhkw>r+QXUi9}mBBV(xpd%O5lu
z+*7ZU)a;PVi$ZJ3ZWtZ=#=O*@{L+R#C9KRWV(UpT)I^pB46frdwVA0-Msdlm7#Oe6
z!Z86f-9qm542m4LotHR^GS$miqA+Ae^iSW!K2bThvfPW_J#Ej@Vs#b8k@_q&x{BTI
zP;A@()z3aMwGH)hgZXkw;oEcxag#vOF|AKHvvT32Nh?0SW~O&M@BHcfY1h~oFPpt(
zEB%O*Xe}Cpqp=7I!Z@Blw(I+*DpcHQvkBm_-cbjJfFPvBD#8loc4Wq94`0r`j%bT&
z?DrtO8e0e+f~U={mg7#hl9RI(=A$K13uxhS7R6~UWKWrXz3(+O_3SmWmg>HNMMc({
z-%Wo%spI^5P9-sN(foS2XaZ6LT1~0zlBoRrS5eo;}sMQ4{Z*
znaJXijSJF9z$N?kqyZ<4*Wjmu8}%ghS{_ASLl+UvQE?ZUyYvGBE#~7h^uWV*kXd#%
z_6e|IRmRqqTrx@dN(x?hv5TY!W1GKje=&QsFc(4)`b3T?Mj@Vm**vt7r_*fEZ>CP@F;;HsvTBi*7~-p?#J#p@bf
zvM=NGz<99cB${{vM8oEnD24V2%=pNNwjoBPZCTqvC5M?2Xe>=fri3$?FqdHBlLDCi
zjmn;U!-4(ke!hWk)*ZdZ9W8c38rh}p*4;DtkGzYobF{YKIfSr5h)#ax3Mu-?L19AI
zM_qO8&hs_HlKH~F){tS<%e!>YVRov=#eMo_9oiS1lO!1U)
z4A~@(r_4AwvFBR^FODhGbpyIgvg5|2&ty7bMo#Ja1AU(y?kN{~Yn^?Jk#hS8e=ceU
z(gJJdKw}k+_?(Le$7a!Y`(J-LJ5z5tXyK2j6jRR}kW=GSDCpZy=qK2EyL5E18@qM*
zkl5XW@o2uQ@BrIOilTirHO5VN7H+lEh7LXPRASR+k7S`$l5L}#0C{l+j1mi%2{YW%
z&k!on-U5Z8(1e~phf<=JK{F+0NBf;jALfB^-4@7*m*|Vc(Hq7GYMV@=YQFI?Q8r4N
zR$|XIR0lV?*6gF)szgOmd4?Eg9+&E;dx%MjMP>IF&bLweE@T*TzF#_T4m7O9pB1k82uVD9d#
zS=l?w?tJ4ZFbr2?ZpEU5X`}uya0@=sKB~tlv9+~qDp4F7==3?RY9j2KNcMpBD}AvH
zsZor_?|e+lC-*_okOo&_dCl19i_S43m71%YkOjnv7`{K}t)9;yIG$K(=It{4gb>r*DcH%-tY%(xb&kRlDFsZTo7{NJ%|>sls2
za9E1#ByGu@lOsp%!91|lcs_xv0Tqa+xRwf`Ayp_nMEyEx=F)FAMm86fY9YTpHVN|s
z{Y^?ikP_s%8$Lh9d|ivxLv6}NO&(2V8QI2sJ6siHr#PWnH43U|#|ukD*`^U4IX-jR
z$NeZ!!hPKEr~`#`!!G@ej}10WT!Y6cw#)OCy4E((k*p#fa4yX&6wMCsvBTEKxnP+(
z#vUU
z6V>uYw9mVIIuZXCG@L4{72Ub!;oIx}wLF*L5UKyAA^2&=#h
z#OKeMx*j6Q=G{ElD1Z&wpfWT9-l4whEQA$H;U{JE1DV+BK{RY7Ay6i5Hgk`1YMi#A
zJ>Ao-gEV(MmQ|Ih9Ha^O(M%vP*Nx3MxX-6nyZ2EAJ712z`tFl7g!1n8OKsHRw9GgD
z0gFhXPm!4raq8~Jxs1RHhd`*pj50gotwa?S13u)0e^LdIv2IT;wBb^
zf&AVD#t0s3fOnYz(PifI_i_zlHH>6<0kjXFS$Cy}(md@dZ+Wu2vJK?vJ%)6nqpRg#
zI4e7o(H5aihet_Zi&K7s=ZBZMj&{tQA1}@_Ywp}Uo~*&@->#i(e=5qW-i8}@^!%t4
z+aOb!%Q>Op?b#lOLAcbyr$^@F9(PT(rQ=J)Mq-Uyv|T)yTGC{Jt4+*eA>BHzN0jeVT+2
z{P@t#tQ$iF)rcts%5wfe0MWkCXEw-mL9FD}GoPfIY0PuN8uJU_f(ap2@7+L|D13dF
zMCY1IsAxf%pQnE-$d7{k0Qks20L__Kn6TQ}7Yd8!o8Ctpjk~0uW7~IH7nr4W*bXYm
zrW_9QN+F725C)+dq#!h}z(TLJU_p>oJfXRq9m1s#EXLlfSJADS@N6~eR!qwiK7at1
zZ-~1Vq8sz=mn|vFcOKzyEewZb8ipE2yVwy%1fjloMzXCDXwi73mFQn>lrr{Z!SexK
z2?pN>#4?*Nk*(S1LpC?ED;r!?qBc!lujIzpQD^m~!w8OSNUP@79o{>T4WQ~yIzHg2
zDzuXns(dcLYDlQk`aQGck`@jjkr8=yZ-sAU-1yzW78z92`VxXV2CFw|;zI5!k$xq+
zyL?x8!9GVHeiaUvAGEF_j)Cu{u*xFydrhXEci64U#?jIl6SUck2ZCt?@S+dRR2jZY
zs44_TqY7b@HJdgi-YoDGU^Nxp6iifFR*3Pnpo~WKEyLizkQVB_bUI<>>|?Fs8o{gy
zEQTnrrh2zAK1K0P$RQ^%R*kjy{Lz=F=%_R{6V2J90K~l!vYJ~L|J_3GzARY_OT(w~
zMDNpsdzO(?OP|gPkZ!B!7Mz=Sc-IwWnXDb_wcSj1U+LmHe~mxU7?KW<03o~J%_cma
zz~Df9vr|orx33gR%J!Y<6Etz7a*Z38VLn*~Nr?0*#q{Xyp5I$>s7G3(S`~~WtL|4@{my|wCf)fTmT$7#D4pFQ$WATTU$0AxQAV1z
zKTKPPhP`m&3Pn}HD`Wo%a>9$pwoGOiEh%tcqHR^-z#rJoT2c}^hhD+9H9MhK9=2x+
zkz}O^iJ9j-ayMBzV6BDSiboD_w9L%8!V0MlWjWJLRHs6JWJi#zaX#K{l1^$p4`u9r
zEr8hOSN`6jQ%9mwjB3K5ZXw;mnSO=EmaM2ig1&z*IAdazenf@R=EKs4vgOc7;zGY+
zSMu%Bhj0~0$~4b}<98F*RqvYeYe**
zK8c*cHnlT%UdxOWaSBnLH8I;*lEjmpg+cid69ka}rVcV*KKZ68;B={MN-YL2cM#jF
zBKw^@Tm3~FES+uA*6qn9?hAXny>^f4i@9RcE62-X`{9~uC(*7NH2;y529&4?+R^7#
z%+7&m9eTmbYrjF)^pMjU!%^&
zWNZ7cI$RRd)ou{D;>op8&0$pP&%)>{s6flkvJ1CivAOr#DqBmftH8qGcJA09H5s~g
z;^0U0sAu3{HSxSWb*)HrV0DTm3GT4o<I$azbn=i{zSuA$gn9v
z&CO1I)9X6T{wZEfubjOhJ5z!pwM4a9tWNrXvb?g%1Rsu2>kE0oHJEE=4V07MJbcL@
zn&_HUvumWsxpRI4=PWRyBya$BhT&Lh9a@5-uiw@1*lmy)hU(DE}a=k?3
zTwUcnEsk*brdUw^0XcMjqI4#wZ0F6%QQKDj62JJj((Uma%S}XW7C~&t;PWsDeTE)g
zcrt}3a%pbLG)fbi))QMnM*)`XD~PYH^*I^to(K{=#hmcDPs1+LIr1ja)9kmTvqs@D
zxRXO^D~rF-`FQXa622|?c-l6O3fcQ1P`m^5WnzuZXWP^~xk@{ZvT`Xgu@wDj6@dV2
zu{;Xjn+FaziBE)Zns*gn$}a{zE!T-%JZ$U`KD@m-msZyf+g}50-HRi``D(TZG#tmx{{OZJ0SVVi8#GDTY`U_mSHNUeamX
zBr2pi2%F%cowfOByS1JxE3R*ha0Mel+FxtuPQQSYK@N(Hj7lGgm`Jh&0*{aP+A#buTUcfGdw`KYmPZ5P8e4~JK|Uliqm
zKA(*HbvKp*hw2~^!(8L?@Nx@zTR5wJN{(<08l~7%i%tiD(>Yj+dWZ9}caJ4MRnB@7
zwL%~29eV~v8y2)Ec;cuCSSShL%Ovi%U?5;9blY*3D;2DoZOhL+7Vum{EARI$TR0e!F&Lm8r0hejVZ~%2x|QV8%xEi`4qq5I
zhVnY&R+df%1Ml!B1vL4xwpZo%nq7RIWnL_+soT%;?2=XYq)m0hdYfA4Oc>=}p
z)+$q^@2ESlZsjWBEGutMG*-d9F!Mi1*O=Ryok5k%cja#O`HkIFHzBcXM?i+PYC=$3n8v}5`5R>Cap
zSEnW~78DUHp6IPTMYbb_O=4y#Jvc)UpH_kyIORz4Ur#ChsxyU|O4D-om4?kUb7-h3
z9Gn?JX|At20i#fT#rc7ulj}y46I+kj0@~#)pJA^fd5dE_F2_H-Y&K-N&gLz)2#(lm+zesu9!U}B3Y>b50t1E^=Bswx_*6ictDpRVMW5?}dAD69l(
z2YrO*BMy|Gvzx<}@JC`4yn`hky-bD0z5y*lnG1QaH#yUuv>iKin6m8iw^{^`{1#4p
z&zZn|7|2)89UgNhGy3N724NVs_VBVN+!EmS7$1{?c=xTvHFS8?4MR2$FOmA`Yez%}
zQPqu%rjf0Ww6>Huzxno(c0}4CNJ4IK
z5W@X(1aEUP`7(bABxDf*o44~GX%D1-*)xZw2>=3)$UmL@(w2mAD^
z$D(o}i}T6};vg@4it$OaI`WY)b8ernEW^26GA>DG~TDoGL^;*-gPa
zeP;)seS!xl#9`!Wk!|uJy=>>$PKpsmNyo*aE=ROrC~TiO=-NY;4!o~8TxOz<7mc4b
z8PE;&B>GEuhwc75jJ{)kovJt|W4h9w|E>_gZfq0Syl<4($?!DM6)gd)f*G2R_2
zZ)d^AacwY0faMgN?Zmt@gw)YzV5`CAZR~EuGy%K_Dwv|3?ADgVC+7)cVx6&D0iKRG
zU5rwA!mM<3+ynN3#-=Beyzm*=r;1=U&E(mZ>?nTD
z@lu%^^HwJ4=KM@XtRXXTb`-H9k!bQG?)RKGazr%J?+<+8pxoKJBXjB3k&m+xhXq>#
zVC@DVZ)?s72zBUbPMAMZhOO!EEqsjg<=^62sowt>n(jkVffpt$a!Max<^1_gOaho{
zjD-Rtlb2!PX0OC>Rs_807&DFFD-_PuPx=6h?qyLBs{0^|k5Vqbf!nqF5y4>Pe*N_+
z(y?o1swMlA6(7)Uy8Q;=s009Nxf~hduhPs`z*@bzf;s-*!VtokqcW
zrwARBIY6Q=zX*y#sZhs5%->6ud%m|+9eg_P3N`3)8z;7*{B&UcAq>^0FUC6YlfDG_
zoemS={D|naAg|*vUcEuxiVpD(<0Leqj4MsdYE||SGZ<)vtRuNi!L`2j!Z9s`sOLq=
zxma@^lwzHdE
zg|Y}f*sI)7*BxVF1-zyqoEr<5$eu^AOcn6;tUj=00h5l~-(r)bxBPP`nljR}g{q~q
z4dbrKcgZ9Au))E#J?U#9Ri16XVbdK=+L=@u%zIlSQ
zZ9NwbmloSgT65GV364l1+#TCqaCLSYKIjTmVYzz1NZNDde>vnW>l0@r5WweGVzf75
z1gj}27qnPfJF}wSLR;f)<3DAN?3knib4u03#;)x9A{;7!R;|2yjv}{
z8Qe&orn=|`k-xK_W_asiKeGEQINO7RahycI@=0CJ+EFNH{b`&84$^++;8#pX>Id*$
z$zHu5V-IYVLa(5B>}G7n3t-VCc)vzA)twJXF}QLo_OFOMU-M6S1#VrC
zD+6aR9&`KokqV}sT7CpZ2ZuIngh~we)TRjOYwQU+14?*rFOsFy@{FF>NwRT6Qplf3
zuSj@2RcX11f?{9xG)CeV+3xeMqq1oT~EI#-HN3-U)E*=5x=WysYP-4bQN
z8X%lJ{pb+FF{(>EN_Fn@Y{<~r)ad2oU>4AzgkMw2mw?Wr9*0%Sr*B0%14OLc1rmga
zgU&I=_gf!x97d5s4kh10zxJE%e!Q{Upd@DQd(!h*wXO}>#C3A{5f*Hm)W%oNsD$|0
zZ+;D~v5$$bk35P|)hh=zAxuuxkZ60}`pw8fmm@H}x(>gf9ez_;72UV#_2g!kP%lN}
zQzH~9CoGw_r;?y(fifSs1h%SX@vu8Qiys}1g1&(Z*;kQ?o0?ih90W?f`vNqifJ4rs
zb{A=_jwQIrBYNHf$=hYG>6@-=bSy&aC0M^1!QcMe`r|9o$j%@T1hh~*g@q96O};V+
z#1iKQf&t%z)#5n_oi)c*b-Fmitt%O<#CYM@c{VlMG1aIGJPWmpQtvmgeeH>{%y+#8
zT8bX_5Z<1Hwu>EhANKY@>VARsjNy=~6gLw!O&%@=Kis8C9;DTPVbMe*$SV>ErRCcL
zFTSQ76^__wk%yetsIyfr3ZbMIK6go+A%12eP?vv_<9#ZRWc`*K$`ZD9Ohvb6Z85AR
ziH!A&j+kUdmoDPjkuPEdYfg<+F%Gw};V}|$Gm-jY1PwQ8B^-5H<^biH{Mq59d<)2i
z`PEn&kvjp56?fCSJ;c(k-6cLL<~adeG9<);cDq#h6m#3qz1?Bst7
z0`U`#A%^1OZcN2=1k#Z_)thvwSp@S%3AUi%oQ{Pg8x?~wUCJ=}s)TajoWel(rK_nZP-LdONFRCOw
zEu#){@p2YVxgQ%CN`OH{DE^gXuF$IwGVo-#_oMPq-X&qtHYE;{Ldg8XmAiNprOG0v
zM-g5@s;&nszPrYuAXMd!xCKjv_3Nk-d_>9!N7}Y5|sCmGIc<#h-L+%Ariujxe{>
z4h#NN4vr9@;y@(t+$)DXn$YC+i;@XYD23wp|ESu106;XPKwPJ084#6l@3
z|51ZTRi}cR3B+u~CUVCC5ejhNK%~&$tAIS@@gI<^9~8!af=^)kNBG2mAMjp)2mt=Y
ze}%XGEBu{}fMVc(gm?N!_&-@6`(NR!{tExo`kKGO-%%)q5*q#y{!iA={8#w%zrz2t
z{_-E;m;Mp{kJcv>|2Oz#zJG#G7WgOlc#407|D*NE!v7Wi$4v7F*ZX1p&q?JEp8uos
z?`MO_cuv{$cUI5#Ipt-POXMAf_}5zgcfdkFoa_57;{B?luAIwdK9g-_@ot{6Ql$1okJoYYe12
zI+BB(1?i7Ej49Y&-vX%Tf$hP+2IP+C!`)_CKzWybmwA5zss%7x0;t~QyYv^1AE$r&
zjdu&&MSxfVsm~w`K_s`!AOaJhESwhHNB7uAzw;D82o#7I6^}U<0tEYNXSMH93(y43
zMh|>9I(HCv84qlK-_t<#{#XM9g1)O`0S4TSz+L*u3%O+QA6|g(?mRFW_a)ZP^4Ia*
z;T`>67Et~#{YQWNZ~y%0hdcN9|H(h@$Au8^d4qe+?sdEO`=5Re;ReuLe$ws=;Jh|1
zz;@S=6UdA}Ci`(7L
+
+ Research Image Integrity Assistant
+ Synthetic reviewer packet for manuscript figure forensics
+
+
+ Approved
+ 1
+
+
+
+ Author Response
+ 1
+
+
+
+ Hold Review
+ 2
+
+ Checks: duplicate panels, raw provenance, processing logs, scale-bar metadata
+ Reviewer findings: 4. Outputs: JSON packet, Markdown report, SVG summary, MP4 artifact.
+ Synthetic data only. No patient data, private images, secrets, external APIs, or network calls.
+
diff --git a/research-image-integrity-assistant/sample-data.js b/research-image-integrity-assistant/sample-data.js
new file mode 100644
index 00000000..71bee283
--- /dev/null
+++ b/research-image-integrity-assistant/sample-data.js
@@ -0,0 +1,135 @@
+const scenarios = [
+ {
+ name: 'duplicate-panel-hold',
+ manuscriptId: 'ms-neuron-organoid',
+ generatedAt: '2026-05-22T13:00:00Z',
+ figures: [
+ {
+ id: 'figure-1',
+ panels: [
+ {
+ id: '1A',
+ label: 'Untreated brightfield',
+ rawImageId: 'raw-untreated',
+ processingLogId: 'log-1A',
+ perceptualHash: 'phash:001122aa',
+ scaleBarPixels: 80,
+ scaleBarMicrometers: 20,
+ },
+ ],
+ },
+ {
+ id: 'figure-3',
+ panels: [
+ {
+ id: '3C',
+ label: 'Treated brightfield',
+ rawImageId: 'raw-treated',
+ processingLogId: 'log-3C',
+ perceptualHash: 'phash:001122aa',
+ scaleBarPixels: 80,
+ scaleBarMicrometers: 20,
+ },
+ ],
+ },
+ ],
+ rawImages: [
+ {id: 'raw-untreated', checksum: 'sha256:raw1', pixelSizeMicrometers: 0.25, capturedAt: '2026-02-01T09:00:00Z'},
+ {id: 'raw-treated', checksum: 'sha256:raw2', pixelSizeMicrometers: 0.25, capturedAt: '2026-02-01T10:00:00Z'},
+ ],
+ processingLogs: [
+ {id: 'log-1A', operations: ['crop', 'contrast'], software: 'ImageJ 1.54'},
+ {id: 'log-3C', operations: ['crop', 'contrast'], software: 'ImageJ 1.54'},
+ ],
+ },
+ {
+ name: 'missing-provenance-hold',
+ manuscriptId: 'ms-missing-provenance',
+ generatedAt: '2026-05-22T13:00:00Z',
+ figures: [
+ {
+ id: 'figure-2',
+ panels: [
+ {
+ id: '2B',
+ label: 'Western blot lane composite',
+ rawImageId: 'raw-missing',
+ processingLogId: 'log-missing',
+ perceptualHash: 'phash:778899cc',
+ scaleBarPixels: 0,
+ scaleBarMicrometers: 0,
+ },
+ ],
+ },
+ ],
+ rawImages: [],
+ processingLogs: [],
+ },
+ {
+ name: 'scale-bar-response',
+ manuscriptId: 'ms-scale-bar-risk',
+ generatedAt: '2026-05-22T13:00:00Z',
+ figures: [
+ {
+ id: 'figure-4',
+ panels: [
+ {
+ id: '4D',
+ label: 'Cell migration assay',
+ rawImageId: 'raw-migration',
+ processingLogId: 'log-4D',
+ perceptualHash: 'phash:abcdef01',
+ scaleBarPixels: 100,
+ scaleBarMicrometers: 50,
+ },
+ ],
+ },
+ ],
+ rawImages: [
+ {id: 'raw-migration', checksum: 'sha256:migration', pixelSizeMicrometers: 0.25, capturedAt: '2026-03-01T12:00:00Z'},
+ ],
+ processingLogs: [
+ {id: 'log-4D', operations: ['background subtraction'], software: 'ImageJ 1.54'},
+ ],
+ },
+ {
+ name: 'clean-image-packet',
+ manuscriptId: 'ms-clean-image-packet',
+ generatedAt: '2026-05-22T13:00:00Z',
+ figures: [
+ {
+ id: 'figure-1',
+ panels: [
+ {
+ id: '1A',
+ label: 'Control fluorescence',
+ rawImageId: 'raw-control',
+ processingLogId: 'log-control',
+ perceptualHash: 'phash:control',
+ scaleBarPixels: 40,
+ scaleBarMicrometers: 10,
+ },
+ {
+ id: '1B',
+ label: 'Treatment fluorescence',
+ rawImageId: 'raw-treatment',
+ processingLogId: 'log-treatment',
+ perceptualHash: 'phash:treatment',
+ scaleBarPixels: 40,
+ scaleBarMicrometers: 10,
+ },
+ ],
+ },
+ ],
+ rawImages: [
+ {id: 'raw-control', checksum: 'sha256:control', pixelSizeMicrometers: 0.25, capturedAt: '2026-01-15T08:30:00Z'},
+ {id: 'raw-treatment', checksum: 'sha256:treatment', pixelSizeMicrometers: 0.25, capturedAt: '2026-01-15T08:45:00Z'},
+ ],
+ processingLogs: [
+ {id: 'log-control', operations: ['crop', 'linear contrast'], software: 'ImageJ 1.54'},
+ {id: 'log-treatment', operations: ['crop', 'linear contrast'], software: 'ImageJ 1.54'},
+ ],
+ },
+];
+
+module.exports = {scenarios};
diff --git a/research-image-integrity-assistant/test.js b/research-image-integrity-assistant/test.js
new file mode 100644
index 00000000..3073d718
--- /dev/null
+++ b/research-image-integrity-assistant/test.js
@@ -0,0 +1,175 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+
+const {
+ evaluateImageIntegrity,
+ buildReviewerReport,
+} = require('./index');
+
+test('flags duplicated image panels across manuscript figures', () => {
+ const result = evaluateImageIntegrity({
+ manuscriptId: 'ms-neuron-organoid',
+ generatedAt: '2026-05-22T13:00:00Z',
+ figures: [
+ {
+ id: 'figure-1',
+ panels: [
+ {
+ id: '1A',
+ label: 'Untreated brightfield',
+ rawImageId: 'raw-untreated',
+ processingLogId: 'log-1A',
+ perceptualHash: 'phash:001122aa',
+ scaleBarPixels: 80,
+ scaleBarMicrometers: 20,
+ },
+ ],
+ },
+ {
+ id: 'figure-3',
+ panels: [
+ {
+ id: '3C',
+ label: 'Treated brightfield',
+ rawImageId: 'raw-treated',
+ processingLogId: 'log-3C',
+ perceptualHash: 'phash:001122aa',
+ scaleBarPixels: 80,
+ scaleBarMicrometers: 20,
+ },
+ ],
+ },
+ ],
+ rawImages: [
+ {id: 'raw-untreated', checksum: 'sha256:raw1', pixelSizeMicrometers: 0.25, capturedAt: '2026-02-01T09:00:00Z'},
+ {id: 'raw-treated', checksum: 'sha256:raw2', pixelSizeMicrometers: 0.25, capturedAt: '2026-02-01T10:00:00Z'},
+ ],
+ processingLogs: [
+ {id: 'log-1A', operations: ['crop', 'contrast'], software: 'ImageJ 1.54'},
+ {id: 'log-3C', operations: ['crop', 'contrast'], software: 'ImageJ 1.54'},
+ ],
+ });
+
+ assert.equal(result.decision, 'hold-for-review');
+ assert.equal(result.summary.duplicatePanelGroups, 1);
+ assert.equal(result.findings[0].type, 'duplicate-panel');
+ assert.deepEqual(result.findings[0].panels, ['figure-1:1A', 'figure-3:3C']);
+});
+
+test('holds reviewer packet when raw image provenance or processing logs are missing', () => {
+ const result = evaluateImageIntegrity({
+ manuscriptId: 'ms-missing-provenance',
+ generatedAt: '2026-05-22T13:00:00Z',
+ figures: [
+ {
+ id: 'figure-2',
+ panels: [
+ {
+ id: '2B',
+ label: 'Western blot lane composite',
+ rawImageId: 'raw-missing',
+ processingLogId: 'log-missing',
+ perceptualHash: 'phash:778899cc',
+ scaleBarPixels: 0,
+ scaleBarMicrometers: 0,
+ },
+ ],
+ },
+ ],
+ rawImages: [],
+ processingLogs: [],
+ });
+
+ assert.equal(result.decision, 'hold-for-review');
+ assert.deepEqual(
+ result.findings.map((finding) => finding.type),
+ ['missing-raw-provenance', 'missing-processing-log']
+ );
+ assert.equal(result.requiredActions.length, 2);
+});
+
+test('detects scale bar and pixel size metadata inconsistencies', () => {
+ const result = evaluateImageIntegrity({
+ manuscriptId: 'ms-scale-bar-risk',
+ generatedAt: '2026-05-22T13:00:00Z',
+ figures: [
+ {
+ id: 'figure-4',
+ panels: [
+ {
+ id: '4D',
+ label: 'Cell migration assay',
+ rawImageId: 'raw-migration',
+ processingLogId: 'log-4D',
+ perceptualHash: 'phash:abcdef01',
+ scaleBarPixels: 100,
+ scaleBarMicrometers: 50,
+ },
+ ],
+ },
+ ],
+ rawImages: [
+ {id: 'raw-migration', checksum: 'sha256:migration', pixelSizeMicrometers: 0.25, capturedAt: '2026-03-01T12:00:00Z'},
+ ],
+ processingLogs: [
+ {id: 'log-4D', operations: ['background subtraction'], software: 'ImageJ 1.54'},
+ ],
+ });
+
+ assert.equal(result.decision, 'needs-author-response');
+ assert.equal(result.findings.length, 1);
+ assert.equal(result.findings[0].type, 'scale-bar-mismatch');
+ assert.equal(result.findings[0].expectedMicrometers, 25);
+ assert.equal(result.findings[0].declaredMicrometers, 50);
+});
+
+test('approves clean image packet and builds deterministic reviewer report', () => {
+ const result = evaluateImageIntegrity({
+ manuscriptId: 'ms-clean-image-packet',
+ generatedAt: '2026-05-22T13:00:00Z',
+ figures: [
+ {
+ id: 'figure-1',
+ panels: [
+ {
+ id: '1A',
+ label: 'Control fluorescence',
+ rawImageId: 'raw-control',
+ processingLogId: 'log-control',
+ perceptualHash: 'phash:control',
+ scaleBarPixels: 40,
+ scaleBarMicrometers: 10,
+ },
+ {
+ id: '1B',
+ label: 'Treatment fluorescence',
+ rawImageId: 'raw-treatment',
+ processingLogId: 'log-treatment',
+ perceptualHash: 'phash:treatment',
+ scaleBarPixels: 40,
+ scaleBarMicrometers: 10,
+ },
+ ],
+ },
+ ],
+ rawImages: [
+ {id: 'raw-control', checksum: 'sha256:control', pixelSizeMicrometers: 0.25, capturedAt: '2026-01-15T08:30:00Z'},
+ {id: 'raw-treatment', checksum: 'sha256:treatment', pixelSizeMicrometers: 0.25, capturedAt: '2026-01-15T08:45:00Z'},
+ ],
+ processingLogs: [
+ {id: 'log-control', operations: ['crop', 'linear contrast'], software: 'ImageJ 1.54'},
+ {id: 'log-treatment', operations: ['crop', 'linear contrast'], software: 'ImageJ 1.54'},
+ ],
+ });
+
+ assert.equal(result.decision, 'approved');
+ assert.equal(result.findings.length, 0);
+ assert.equal(result.integrityScore, 100);
+
+ const report = buildReviewerReport(result);
+ assert.match(report, /# Research Image Integrity Assistant Report/);
+ assert.match(report, /Manuscript: ms-clean-image-packet/);
+ assert.match(report, /Decision: approved/);
+ assert.match(report, /Integrity score: 100/);
+ assert.match(report, /Findings: 0/);
+});