Gf4mPP)|V+YX*dX;J^Poe=C6Mf0VWU!})JHDhQ(0 zAmAO`LE~A1JEc$RzcB8gfqwt_{G*?L>zA4gtlUQt6vj9cd_is>#>1De0|gLZT!tN; zX(~LOu@0aO>FNAW+w}ks5;AWFtq>&b=Inu|vq5_0Kc-ljQn94Ig}jI1M3(dudGO6-u~IMwFNTDfJ+DA3?0}S zh4@@STHLQS$9{Dbf~etMs3Bq2J#u#u5S`n%&mBVKjpb!nn#+-Z-*KvzJzN2vJ-!5r z!{I!vAJB#d&T|1f;6T6+Tn`BG!RbG|s-O-qf}l{^xOUP%Ww;2-P_RJC2A9Dg`cr29 zNBjS^{C~y!C;xvd|9|cIZ}R`N_n-RwxAy s!C0_f3V`4~j{>>@2*y?f z1Y;Qj0&~X+2U7R}KBR;39Q~XL4j{Gh#86VWP4KS^6j;>o4&L4#aKoQR8oRv;iWCrV zFra|514S5sLJ(LfT);{p2mVm-3d7)n$ABdcdDs(hfyq`%N4vFEG<5vy^Dq5+PwyW3 zfb2gwGYc;#NNJ=K4v#^~DJvoI3i9&uNJj-FWt5TwXwZ`Z46=qM$4^KrBGDE)fYTY{ z1R8X3-hmz%7Xnfqg;J1~N69OJN;d+*TTNEh-``&bUSM$^4qh@iysIqSi;Nq=(*y9} zya`yGm#-Sq$-&XVNka|^7Euiaq%+3R1Lx$fA*Y5?Lm?f!96SPjF&d}<1vONFoSXvE z6QkjV2|)V#If9%D(%Ux>h=N}`yt9U!32Bgz2@JG_IJD@H?38R_JP z$9X!~0Z}<50gv(U!1{vR;ef-=P6Uu~^6}I_0TmpaFX6l}8uD_7 );D_g2iJfhWTT#u5JWJ(1!EIc-gt)yg}=Br8f|D z#{>dw4S6NhPSy@=dm3_wk-knCFN~8PK|>J*tBH4jeTv8Ux&d*#limNC3%BE)G@S4* zNKdevU|_RAK|@|a28Hy2jnP2KD1)pw3`|KK0yLCWL4_{? i!Dn&=p`bPT0V)C+h5H%#z2Pmww*uY&}^Cw zveoxqd$@+`By>oCLHB39c@DKIF+Y-Lf$Q>B**5iW*=Hnt3W?5JNyaHCn9;9Vj|pSP z`%3AhDW_0){mSHU;=0287opX!8_`xRjbP8ChJ?IWI#m6>hd|r~ b6U1@bIOMBI}FU}+<3%bFJ-Y{AHIc}+Fx(}*ay%~2Wk*IP`{MaQ&!{}_m zxo-S=^o?C65MNfK^4-r>ruJ;mF6LS*4YT>? j|@d)W5^;tZ|~LYVnQ-OOB9>wX6GL-=itFtU{zF38iiTHCO)UZxxF zX}DZLyHAB`-toK9r^jaNp$D8;Ue@f| UO)Nc5wf-(y zhCXw0{e(I~y_z5+RnsUYzd}5z<=iFUVfOT#TNGH|0(8V^uIfd_M#Prv%46d48_D7x zxUFe_sY$i%(s>K)wHW@FIo!MEF4f6s3zjxY2t6oN)_p2Z+SjW1&E{LEI%I!1WhK85 zndtOA6{`+)zVN$%9BQaoyouu-e<_fw!886MYDG(+wjzA*i@WF*qk~~?jw85a;h^RX zWAj^rNdv#xP?Dbyr<%Q~o~sz;dSbngWX?>(oY<~261tF~)g5|pWAkysn)v~iwMp{n zH_*YoU3-2{KGTByPN$eIoFZYvsO!1t^smx9yO293dz)@#DC#$70Hfp2YbqTB*GYYQ zAGn+G``nXr%{d MXZ$52vfqb-<`5JoV6K6H<`EtW5SGD`k%R&zxTI(czZn&P$Om?0= zArYt;sZB5c!{?UbevgU6zXkC+T_~%Eue;sri|1(dj8w|&X|cTA=)|jB0%4B?)dEwi zA8Nn%e0 JXTjO@Gw(6*3NyRN?H<~Bz9#y?nSt|Q_eC-tTs`zrfOSI$G zk@8nVXQcVvB7Yy-%iJtdHTCmWZ-e&z!jytaS)wd1H&o>7ZeK!(3zu1MgK+GV6tnP4 zzdxv9@S$bJ8Ttn8Eps);xGoSyAIZN%)N8&PRC;n{h?iQEZam!K<~dh#FK@+ohDJ4^ zGlT3}(0geJNp x2(J%WG4F?_x9_rAn9k$^`hLCm4cGuNMH@8=L zX=W`y$*)b!L1s$R)XinrKKuA}floc{ ;) z=@Eh_abWE1`n6gt;@N|48b{ndMVn_g4C%*(-@C|g3$7;YZy|Bmy|R2@_4Y;IY^)fP zfx1Ld^17Ua#E02f3qku{ lwXT*scuep2_4Ec@=&w?(6D zyYopZ>|*`gQ5WPCwN;up$}Q`voy;EI)x5+>Z@yxq=YU;u)|?nfD@fa)Xn4nb^=P+O zsjSo$`goHH8RqmWX1Sr#*+;z j9ff=j?-B|rfy9|7 zQW%=rpL|)7kdU{*IdA#5I!->E8huhwL-+oRmx#cx727GZHHX!Et*)XblVjA4mPtQX zW3ok2LjDMUPJ3?JHvY~I%p;B~SER()29er!i?(63H~O!YKETc3_uX_ek6g0-Sa(ot zMxQ-)YBkM8WQO79)i&{37O7uP=aRVZKX}x>>-+FX4WHyf(D#=LEz^mWYYQ@*Is9rZ zijvVQ7Zsma_@8nef8%&oplPXja5ZT-I;~Z(Cx059)*NvBGCiwRDgA_7W6QTuokx5% zC$y8fXlagHerPnbG!SpUK8o(Jw+M}k%Q8OqWH`OEoc8U!s_py>;p}IjHu9I%lF)7M zT}!;ZRSiV^4;fc;9uHByJlHoVY4!_iBi8%zT=K}Z(rS+ghLKFozAEI`$O0XrVqo&( zn|?>%wW)BSqxz~M`J?9RN@PQ-nFB%DdIoyjdsS`2Rqj; !2IrMjHZZg>hLd+MR>m5VeM467_YlJEG~ML+jn@1zPZ?0LiR8 zxB`xWAZHJsFuTxcf*v~cn+12liLZ`l`2_l8h_*`U*6f^U65~R*Mc-8^-E*-&-)gv> zda+XPlg}1!5I3FB-4Sm8U{VqP%Fr!w)`SJ3E22rL^!C2{mNxPO&H0IWlEdq*%?-om zH?$0kEar{4{Zq4sc&;VY@@sf+(RaYMly#6PRjTv+xi}V9Ys~W z{H(Y%KRJwrNjW(Lm=udn(^eNul!!R8wQbe|iF2>0`i{o0ZOH#Fw4B-%Cwm5W`0;C= zYx)`a&Qp6HH!FRd>G@ES+!Nk0Z5&}rh9KTH!QLOF;*^0Cyq jj(tV@b>5bq`2@~2|RC7q!?2{|RaRkkb=IGcrJ^m4BgF&Y` z>5sCCno%9tQD=Aae5`rERJ|AqVVl~-*6!8q-D1HSlI8M!r8!N|A;jO8|550pBT6$^ z29?K %{rFEYyX9bV2@Lrx!^revuIm?+CP~}AgH!sc1k!rIbg47!j >-Fhf7We}Z0R%kGl%)I4J=nwo9fYd*TbBC&{AnUPke-aSyY XhyBMY8b=+Ci)r$`AF6jpE*e1sgUK)wLgplwY*5e2% zi_}o?i>^9iAp7`c0l<($5St0w0D*USY5ab^zQ-M{X-O8rrS@H|BjA>&Kqpg9fQQ3X z{^lL&AH${{HKXvoi2{Pya)BbG%k&}Tt+l;^K0*S;TJ@fYg=d8Rd#OcY;})bJsj9S_ zY}$7w1l0}Ck^AVhm-_i=w^RcL|83WD9?1B(|Lj)zI&?>#r`Jg8-Bx_ROz_m4lhNdo zM7@ZnxM^Wr>imrM9Dt?1(Uzwst?oYeu 8o!x^U*S~H#Hr)#hh%{}cq*C3MQj*WPABXQebm(>M{ZGcq_xxncNZTLTPB%r zhn{BUCJE2r&z-aqngOEn2+}T!OL&A|#SbW|Gdb5VXLS(9rmSKlC_d@2KAAakkwL$} zr*Rq=uyE@XIJ-ek=yYb_5sy>@E`yy}#&ICy-rk#md`L4fhgdW2(dhn}6TCXcgLi&2 zYDu*SW)4qPq``pN*zqT{G52ce^JQfMvZ{SfhTja+6o0dKQI(_X%x}~??bovSv49@R zOjbpZq$tX9r QrpLDUF6N zTTH;EfIG{fk3G!3Q;$26XzjMn5 ci9r+=nGb3t_F8b@4R0-mNP*mh&|Q?*Wz zKY&(5kZ=@*xKJ~u;Uj8N^~Uz_=o3~?6lTBu1R)v1@Y?z2y#TcY@zBq9^`a}SRpLk> z7l}^CQ B*ypEzg(j%{jEo_-I}(H@iRc=XI8EQa>JkKJMx9;_O4zOE^wHD 0tX8GBF(dC&-1hVb9;xFnO0(3S&lgh!`2TxVbFKlx6N ==8qrm5pm|mNG|JOUW0CU$>sHT7TeRjCL({rQ7J=n0~hZF4ko$I@vel`}zaq zy^FyuR;2jP+#On_&9A^}%k(+SmWnTfUdh%jm*gYMd!XN@LLa~%Ly#6J%5i*{dsJ+| z79W%k`KupGp130ZI6+PNiDmR2?su 4O4Gl@C5WF*3&e_T58m z2=MIy@<6Ar{F%zz3oxr?lfwN`=2K;FNS$x_^bn&=_qNzenkFyPSxnqt`MR8$n`1aT zto4Xx_~pVdAKAnIvj15reh?(8SKmS{*}Ptll!N8k!!&K=_Mz|Cto3^xYpC?#eQ)fq zhl2ie5X3#(dRuMl?Qgp9(!Vb<1# &*`UDV3*+-0c9qQ`5Ogq{ zt
L<>Ghjvg^gn#>N|ltz6eBlF{hP`9!zL8%u{l1Qw72TO?XNm_8ryG|QX+l3=mE zuQX*4baNC YRXtO2uiH0{c;7^v5p}OXW5w00n!@l*;%-6c zK40`s@-O|6IAd}bVFn-+5ky;x!R-AVi&a7*x;eH)m&%gZ{>RmpOuJ8<%R++HmQBBA zYSL>Wc0Q{mvl_~Q9d!nsX1B952Je`d*JQmQg}#kyEmymV=CgfcbE>|dBc}Z;r?u7| znGChKhEAtiy5958lO*F^n-<`Rm|GU;zLlL1_dh=JYi|bSRBDPQ2JLKU?A9HdPtHOv zrcVa~)aV!H=!ixkS<)r9=j|ti7e_QTf6bhX=-hWxdv)eYzooe>#(MwWA0gnYMu!>J zbz%up|K*zQ|89gbS2PerBE?EJ%5bzymMVB1Z+zMENqc4~y+(!1db4?K;ZNVyUY>-< z@Rh0R?dSJrl)sdkJWjeERtd}vM5l#qZ+i?o=I&l dxNnb($_f_2ry zUa)0co&o1LYN%X`?!d@}0L{l5*t7zAbA#kL6ZjOOiza3q=ca7(Z17wUrY}U1saH>= zEybNKe&&_JD_Eta@u4%TN&VJzukRy4Zx}0TX#eodSjJ58hfJZQ0v5=J43IPt#D;A@ zSc7P*5cO5;b<_onY#5Qu5q!kY359nO_6o;$-ZI>)k_m$dUKQPvJL?&PoeX58p@#2~ zucOmi{^1BX?jqE-^TgNkE=%LGEO8Hwcpu+Iycl7Q*NB*xUHOf?Iv)DIXVtGQW3C}v z*5s97l6H_i*mBJeO^`NFy(UM+d#Tu&czqOWw$6Quo^Uw9Q5g0iOHU^*ELU-}Dne%j z=QqGMm?ZCizH5sn@>T6ET+QpI565AHj-iR;z%}rJl4+KFAZ|p?ll^pA-q>n_b%T_T zCBAaWaps0!51&oLa=cbl6%UZ1Q !Y6oymAV}1dLnZ5QNqL|L z flgell)yZ!xM6&GfL7SFlJ5iO}C0{M*A2Gl7GLCU(13sdrRyx0&WpbTw zJiZ9g3ZRZ6NVF6mvqslG=!!N>Y0J3!Ol0#Jf|q>$Y!w-=J3zm+_s%|8mTH4!Bibz3 zabw@1s33pN(E+3=EiHh!j85OR htn~}T=9-&Wu$EAd+W>}lo zKm9#GV$dP~6v;U6S1ydwuRQK?n(2Pe4EJ55i7^G1g*QWH(qU60vSQ)^5Yvq!D)TAH zLWGT5!{WoIn?XNxY_e-48(DJN#Otf?%U3>x7jF$TNo;4lvC7tWEuDTX#r1vDuP+B- zjyh-&(J}n(;*TLDJU%{MJj>|5-=u#1q3h{0c8mpQNHQY;0GvqlC@x?ro^Mm3zkiPT zZ8KBtUAzcQW (j>^O?{D-dVHpQPxukG{YEX4x@W+3-*<=U7FKHqlM8vM?q9 z4h&a!aOF#@^rISgqK5L$y3kFxw26#vYj WJ$nSfcNJ=cUc=(@`0k~7n`2RFvs(9G-tgLz)_XKXj9LnGld~!Q zJ(AAG|Ir?Fp@k;J9al7fhcjLM2&?R92z!*^W#>0!-zlE^r#Bh2?3lspJw%hR%hbp5 z*OmL1l|IuX%ezQ;&j_p-UZJ*!%>zg0V#-k7b%@_rDr8leo&BA9S;vrUhT8H7m?lV# z&5$Sl@0LT@B#MNmR_+V_$QDcO94n=PKQD mgsKAh&X1~@}L)*Vn=>mKW8KKjA{~jc> z5f$vvJKDi3U*}aXtyS5D9-CVf nvR@-=Z#z71HA-kxEO)fZ5xFej>K0w z+*5e`bKq?jyvb^#Nxwi$;By;8@x-Pvzr3b#gFVza5X3Vu@B2Re$bs0*sbMJ@{WSb% zB(RRU vR%*ti~4ET;e1HfmViiJ3brW7swo)rusL3(yfgsS(JLPg>uMKxt(rZ(d9I|cv3Ai z?@|$S) Uy2eo_ncs|D8SD=^;`1>)0U^T zOZ}EmO-Fz2{QFF?3(?U>n5NT#onYD#Yql+E`ZTG34xe)AoS)vrJ{eebg!uF^_4QG8 zO?!AxhK{4t>i(>eJFjhY;qbg0v9{4!7&28W ta98;H0 z+%4QS#(X4@(?O?^{~XDKH}k;C$V{rO-Q{YaU}7O@VEOCr3G#-|F@}RHuYMs$z|~?T zHn7$2G(+Rh6Wk52sSmq#I3nR;EsG#-ZFldnRC6=;EYWvN?TU!O-8Vs}!MT(g61j1n zql;{~F*QPdlcRqU-j|M{(?+*X)lpm99czSNo^*yVmFhQ%ZU(J3d36g3qbgu@QA1LA z!Yh*mT~^&Q;a>!LF9zC&w)AV*qS&ke&JlDv)!*J8=_Geakc+4Yh>ojY5~?<8_o=+5 zHvxMH&C0huTl!Tc*6qU5lgbQ>`yax)a|}9NWLtdiwi-;|imY$be`FsHK*(7xzy5V9 zSOP15XfP8UnRba)pm2eC&kflx7v4sk!P>d^e&cR#W&nYth6#Z+#p@&k>qrMu*f5 zA12kD8P)!2wEH>33qpdbQh0`&{RWz9sM}TLHfLh`n)b3-yx=J6X)29``PFXaVLL6L z3)nDpc3vv!x&GCU!j-B)GX>n|FY*MQrQVaGMJj=KB*zDIUpdBO4+?bv3>1Royd4Ry z<>r5hgz&X#$84FDypmIN-{YOD@$llWjZSyjnL4gpn?yG7-%94)FHgD`xfP3DGd`$? z1n-8TItQ`@Z$}hcMN4-zrk+05{XB0mcU` + diff --git a/supplementary-media-accessibility-guard/sample-data.js b/supplementary-media-accessibility-guard/sample-data.js new file mode 100644 index 00000000..ef2c1a32 --- /dev/null +++ b/supplementary-media-accessibility-guard/sample-data.js @@ -0,0 +1,138 @@ +const cleanPacket = { + projectId: "synthetic-microscopy-atlas-v2", + releaseDate: "2026-06-01", + mediaArtifacts: [ + { + id: "figure-panel-astrocyte-calcium", + type: "figure_panel", + checksum: "sha256:figure-panel-001", + previewChecksum: "sha256:figure-preview-001", + previewSourceChecksum: "sha256:figure-panel-001", + publicPreview: true, + accessState: "public", + altText: "Line plot showing astrocyte calcium signal rising after stimulation and returning to baseline.", + caption: "Figure panel summarizing calcium response traces for the synthetic astrocyte cohort.", + thumbnail: { + id: "thumb-figure-panel-001", + sourceChecksum: "sha256:figure-panel-001", + generatedAt: "2026-05-29" + }, + accessibilityMetadata: { + accessMode: "visual", + accessibilityFeature: "alternativeText", + encodingFormat: "image/png", + schemaOrgType: "ImageObject" + } + }, + { + id: "protocol-video-imaging-setup", + type: "protocol_video", + checksum: "sha256:protocol-video-002", + previewChecksum: "sha256:protocol-preview-002", + previewSourceChecksum: "sha256:protocol-video-002", + publicPreview: true, + accessState: "public", + durationSeconds: 120, + altText: "", + caption: "Protocol video demonstrating the synthetic imaging setup and calibration sequence.", + transcriptSegments: [ + { + startSeconds: 0, + endSeconds: 45, + label: "Microscope alignment" + }, + { + startSeconds: 45, + endSeconds: 92, + label: "Stage calibration" + }, + { + startSeconds: 92, + endSeconds: 120, + label: "Acquisition settings" + } + ], + thumbnail: { + id: "thumb-video-002", + sourceChecksum: "sha256:protocol-video-002", + generatedAt: "2026-05-29" + }, + accessibilityMetadata: { + accessMode: "visual", + accessibilityFeature: "transcript", + encodingFormat: "video/mp4", + schemaOrgType: "VideoObject" + } + } + ] +}; + +const riskyPacket = { + projectId: "synthetic-embargoed-behavior-video-v1", + releaseDate: "2026-06-01", + mediaArtifacts: [ + { + id: "embargoed-protocol-video", + type: "protocol_video", + checksum: "sha256:protocol-video-current", + previewChecksum: "sha256:protocol-video-preview-old", + previewSourceChecksum: "sha256:protocol-video-previous", + publicPreview: true, + accessState: "restricted", + embargoUntil: "2026-06-25", + durationSeconds: 180, + altText: "", + caption: "Short clip.", + transcriptSegments: [ + { + startSeconds: 0, + endSeconds: 38, + label: "Task setup" + }, + { + startSeconds: 38, + endSeconds: 75, + label: "" + } + ], + thumbnail: { + id: "thumb-video-risky", + sourceChecksum: "sha256:protocol-video-previous", + generatedAt: "2026-05-01" + }, + accessibilityMetadata: { + accessMode: "", + accessibilityFeature: "", + encodingFormat: "video/mp4", + schemaOrgType: "" + } + }, + { + id: "microscopy-panel-no-alt", + type: "microscopy_image", + checksum: "sha256:microscopy-image-current", + previewChecksum: "sha256:microscopy-image-preview", + previewSourceChecksum: "sha256:microscopy-image-current", + publicPreview: true, + accessState: "public", + altText: "cells", + caption: "", + thumbnail: { + id: "thumb-image-risky", + sourceChecksum: "sha256:microscopy-image-current", + generatedAt: "2026-05-30" + }, + accessibilityMetadata: { + accessMode: "visual", + accessibilityFeature: "", + encodingFormat: "image/png", + schemaOrgType: "ImageObject" + } + } + ] +}; + +module.exports = { + cleanPacket, + riskyPacket +}; diff --git a/supplementary-media-accessibility-guard/test.js b/supplementary-media-accessibility-guard/test.js new file mode 100644 index 00000000..7fb9d845 --- /dev/null +++ b/supplementary-media-accessibility-guard/test.js @@ -0,0 +1,43 @@ +const assert = require("node:assert/strict"); +const { evaluateMediaPacket, renderMarkdownReport, renderSvgSummary } = require("./index"); +const { cleanPacket, riskyPacket } = require("./sample-data"); + +function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + +function codes(evaluation) { + return new Set(evaluation.findings.map((finding) => finding.code)); +} + +const clean = evaluateMediaPacket(cleanPacket); +assert.equal(clean.summary.decision, "release_media_preview_packet"); +assert.equal(clean.findings.length, 0); + +const risky = evaluateMediaPacket(riskyPacket); +assert.equal(risky.summary.decision, "hold_media_preview_release"); +assert.equal(codes(risky).has("EMBARGOED_MEDIA_PUBLIC_PREVIEW"), true); +assert.equal(codes(risky).has("RESTRICTED_MEDIA_PUBLIC_PREVIEW"), true); +assert.equal(codes(risky).has("TRANSCRIPT_COVERAGE_INCOMPLETE"), true); +assert.equal(codes(risky).has("TIMECODE_LABEL_MISSING"), true); +assert.equal(codes(risky).has("THUMBNAIL_PROVENANCE_DRIFT"), true); +assert.equal(codes(risky).has("PREVIEW_SOURCE_CHECKSUM_DRIFT"), true); +assert.equal(codes(risky).has("ALT_TEXT_MISSING_OR_TOO_SHORT"), true); +assert.equal(codes(risky).has("CAPTION_MISSING_OR_TOO_SHORT"), true); +assert.equal(codes(risky).has("ACCESSIBILITY_METADATA_INCOMPLETE"), true); + +const reviseOnly = clone(cleanPacket); +reviseOnly.mediaArtifacts[0].caption = ""; +const revise = evaluateMediaPacket(reviseOnly); +assert.equal(revise.summary.decision, "revise_media_accessibility_packet"); +assert.equal(codes(revise).has("CAPTION_MISSING_OR_TOO_SHORT"), true); + +const markdown = renderMarkdownReport(riskyPacket, risky); +assert.match(markdown, /Supplementary Media Accessibility Review/); +assert.match(markdown, /hold_media_preview_release/); + +const svg = renderSvgSummary(risky); +assert.match(svg, /