From 1d77401474e11b9f53c1a3ddb9a211528108bdac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E9=93=AD=E9=94=8B?= Date: Fri, 15 May 2026 13:47:40 +0800 Subject: [PATCH] Add scientific reference formatting workbench --- scientific-reference-workbench/README.md | 35 ++ .../data/sample-manuscript.json | 93 ++++++ scientific-reference-workbench/docs/demo.mp4 | Bin 0 -> 41220 bytes scientific-reference-workbench/docs/demo.svg | 34 ++ .../docs/requirement-map.md | 31 ++ scientific-reference-workbench/package.json | 18 ++ .../scripts/demo.js | 16 + .../src/reference-workbench.js | 305 ++++++++++++++++++ .../test/reference-workbench.test.js | 55 ++++ 9 files changed, 587 insertions(+) create mode 100644 scientific-reference-workbench/README.md create mode 100644 scientific-reference-workbench/data/sample-manuscript.json create mode 100644 scientific-reference-workbench/docs/demo.mp4 create mode 100644 scientific-reference-workbench/docs/demo.svg create mode 100644 scientific-reference-workbench/docs/requirement-map.md create mode 100644 scientific-reference-workbench/package.json create mode 100644 scientific-reference-workbench/scripts/demo.js create mode 100644 scientific-reference-workbench/src/reference-workbench.js create mode 100644 scientific-reference-workbench/test/reference-workbench.test.js diff --git a/scientific-reference-workbench/README.md b/scientific-reference-workbench/README.md new file mode 100644 index 0000000..24de160 --- /dev/null +++ b/scientific-reference-workbench/README.md @@ -0,0 +1,35 @@ +# Scientific Reference Workbench + +Dependency-free citation and cross-reference checks for a real-time scientific editor. + +This module focuses on the publication-formatting layer of issue #12. It turns manuscript sections, Markdown text, equation blocks, figures, tables, and bibliography entries into a deterministic export-readiness packet. + +## Run + +```bash +npm run check +npm test +npm run demo +``` + +## Demo Output + +```text +Manuscript: Protocol-Guided Perturbation Screening +Status: format-review-needed +Citations discovered: 3 +Missing citations: missing2026 +Cross references: 3 +Formatting tasks: 1 +Top action: Add bibliography entry for @missing2026 or remove the citation. +``` + +## Files + +- `src/reference-workbench.js` extracts citations and cross references, builds numbering, formats blocks, validates style requirements, emits reviewer tasks, and signs the packet with a stable digest. +- `data/sample-manuscript.json` contains synthetic manuscript sections, bibliography entries, equation/figure/table labels, and references. +- `test/reference-workbench.test.js` verifies citation coverage, label numbering, formatted text, export blocking, and resolved-export behavior. +- `docs/requirement-map.md` maps the slice to issue #12. +- `docs/demo.svg` and `docs/demo.mp4` provide a short visual artifact for review. + +No external reference-manager credential, private manuscript, or third-party API is used. diff --git a/scientific-reference-workbench/data/sample-manuscript.json b/scientific-reference-workbench/data/sample-manuscript.json new file mode 100644 index 0000000..549c1b2 --- /dev/null +++ b/scientific-reference-workbench/data/sample-manuscript.json @@ -0,0 +1,93 @@ +{ + "manuscriptId": "collab-editor-reference-demo", + "title": "Protocol-Guided Perturbation Screening", + "style": { + "name": "SCIBASE short article", + "citationMode": "numeric", + "requiredSections": ["abstract", "methods", "results", "data-availability"] + }, + "sections": [ + { + "id": "abstract", + "heading": "Abstract", + "blocks": [ + { + "id": "abs-1", + "type": "markdown", + "text": "We combine registered protocols with live analysis notebooks [@nguyen2025] and summarize the reproducibility gates in {{fig:workflow}}." + } + ] + }, + { + "id": "methods", + "heading": "Methods", + "blocks": [ + { + "id": "meth-1", + "type": "markdown", + "text": "The loss function follows {{eq:weighted-loss}} and the metadata schema extends prior collaborative notebook work [@park2024; @missing2026]." + }, + { + "id": "meth-eq-1", + "type": "equation", + "label": "eq:weighted-loss", + "text": "L = alpha * error + beta * drift" + } + ] + }, + { + "id": "results", + "heading": "Results", + "blocks": [ + { + "id": "res-1", + "type": "markdown", + "text": "A blinded reviewer can inspect {{tbl:quality-gates}} before accepting a release candidate." + }, + { + "id": "res-fig-1", + "type": "figure", + "label": "fig:workflow", + "caption": "Reference-aware collaboration workflow." + }, + { + "id": "res-table-1", + "type": "table", + "label": "tbl:quality-gates", + "caption": "Formatting gates for reviewer-ready manuscripts." + } + ] + }, + { + "id": "data-availability", + "heading": "Data Availability", + "blocks": [ + { + "id": "data-1", + "type": "markdown", + "text": "Synthetic editor traces and bibliography fixtures are included with this module." + } + ] + } + ], + "bibliography": [ + { + "key": "nguyen2025", + "title": "Live Scientific Editing with Structured Protocols", + "authors": ["Nguyen", "Sato"], + "year": 2025 + }, + { + "key": "park2024", + "title": "Notebook Review Pipelines for Collaborative Research", + "authors": ["Park"], + "year": 2024 + }, + { + "key": "unused2023", + "title": "Unused Background Citation", + "authors": ["Mendez"], + "year": 2023 + } + ] +} diff --git a/scientific-reference-workbench/docs/demo.mp4 b/scientific-reference-workbench/docs/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..30ddbe57a3e7f731f9771719a450327d48cdb355 GIT binary patch literal 41220 zcmX`SV{|1=*8qBA+nSu%wr$(S#I|ia6K7)Eb}}(0wkNieo9DgvyK8lKZP!Nq=&srg z004mIE}jmS&i1wd04U(U@%v^rax-DFbzo%z007YDPG)8RKqi!}sgcXKOf4kD*H`6+ z*lG9Cs$^>#?Fw*}$IQsg_D!^KadF^gU~qSLr+2e7HM6%d zvZJ?m`pNL$Ec6yGwl?1~_6{zV_IA$PKocWlBNIL*pp%(79}Cdb%-F`>#F~$Zn~|Fl zXk=$($31_XB8a z>0xGS@SjDdZw&({BfFnwd`#>>6ALGMTO)&SRVJW|lbMZ;rSmt$>A`7g;_?lcINI_t ze!E~~>Sb?d#>dP=%ftjUH*$6{aB#M^bofu=|25#~U|?@3wv2gm<_%_4_ zw6V9hHnRA}4E~SE40N`!H2HSs{|JmgJE#9mVq$4)?PBup0Lf>>_8&@YI zPXiNsTL&YTZ`|ZNM=nlAmUiDJz6G6({*y6xGO{&u{?3-MfrIBaY-!5(T`U75QzM7} z!Z0>4wls47ZxTxadqKiW&F=gC!_yz>SX3@@vZJ;V(|Zm`yb!QgwMpu9B6C&o!I}v z`bPMeS?C#oj{gP2$4Jlq4Lba1_rSBE-UBqu&MvmVL z;J*8nO1jY63tqDFPtLHvrgD*Ms;p z`r+ph_12%$Z|sMHueT0r*1fYQ6cbCYcr83pWbBnQ(MS1d_rvw96dr_ZngbU8h)oxNzRz*~~(7L+NMbRB*Q>aK;)7eEG zJ^2&6=V?v)m&treB(dD^5Ko>fVV#&zM)wR?CByrA;j+X}7u3c5DY4@bLB6`YrLeJ^ zDrxb?rxEAG)B*rVMg%b-p87Qhs?C%94)1o;VO=Ly6szwTOWf4Ur-+93F9Wz!fKveh zDC!GX_mcG;*Sy_2%dMz}^5S`5z9b@-JnU~Vju%gR;HN$Bptu_*#)mcCsT8bt79Tcr z-z08t!X>lwMoH}4Zw5`Bn@o8pc@*v-TP3^uo@u%6$Zg1=QC^c7WQ5;blaqIeIe#um zc!ot0Wgv0?(P=iaCaOyMRoN0vro=F$%Q2rq+qZE51VMrtEU31l>5$p!3M%$+&uW{4 zbq{Hc0;i;tV{bZK6;op2ai!ce)XN?iCnaP-;9?A*VsZ^L z3NrslNXJ8#&Gri%*}F90kgH^N$=UL}fM<75%bDfSs9-o0{v=f(vzLH-1nc;~Y}*Dm z1x0Pvf-MpNI~@=RQ1fZ3qCcsS?P$2Y;w*>ybyG}w!@)8 z=x`VK^DoI~hz0^&Or~zx`1yVP2-!>`DRDOJ);S)70IO>tH$gdrxAqfd?KhdN?hIFA zi0+rn-SR3Y-i_UA28{by^6jlAp%c-Q99TbnlYf_6u6!nhuahX(qa{GjVJOa5cePkY zlb+z`wO6fyye||`QseD($};jx?lHh*ABjfu6@!v0kWkiM1o$NDOsZM-Fz_JffwP>3 zfE}g>W2rbUu1FVHX;tUw_!rE|k_DJOEkdbZKr5qx6G=+Vy&7D7ZBRqqVjyt2KU1&W z#>mH!QD<0~%L}yn{H)4*=O98eDmgTgOuc=r=@hf<`B!Zu_dKL*tw=-(otfOsY_i!6 z-Jy?CQ>SS9WL{A6;D`z|LobKnGFvruBIs zC9qL0Yg8PDM(Hx(rdMVouc__NnAjm6qYiQH9S!?z5nH{F|60xo=!sPRE zyE!4wM%)8!w^%OW1S_|8oVg~8fJ>31wRF8Tlf*t3>sIlRo=55drccxI`Yo%KVWO*B zi;ZCP0yI;7kYrvLX@b#8c+%mIV65cw9ObyyFe3Sp_~Srs{OFoHTK_<_v)jzVUsmA!K_-mBHpRU`#ex3z18 zIZsT4c!YyCYeKWTMw*#z)Ai->DgBc}c7M7lx|kREIPQH+ytDE3N=JgUk;IPk;d^&* z`B8<#g_oDZyQ)e--KvH`510dQt2UyjH!$~i6RwkW<1mNc=e5RKKh$$xZHQUK_(2Pn zyhWc-V{yqLMKxP*VSaZ!v&BR)=(U%II+tSVWv9~0ZV4!^dsp)%e`%=Tct@0F9RIxO z%4XkpbDT)m*&;+<(@a%C%+I(d18Lbo`Sb)lw+pZ`cz>pm*`*)@qxmulztnt`Z z4umW|Hy3WP*O%dM2MX;w9U(U5pI)ZJTJ@~zFf#0(t~uooF7j8*p%`wd2W~ac zI*%6le~6COiIpzT7$>cF{o1ym?S!e#fM=iVU(sUsqd08@zGY803kNf`WAjU89|+YB z$BXQ>nkY$`gI0ap5%WbHJoSRCm6{PLWUm|2M5&sI15byMTQxp{<|@VL+FWUg5YpHX zDPd|FD#9V+8AZ_8tG})?>gl;WDI~YqZmT$)l=f21Av`IRqzJC$6GkS0KQO%>uGz63 z63hHl+OLejzIqS+mH}A4&bKm7`fxum!j^jZYBua3CKBzQ7EfS7NQ?# z+GR*y5n-n@hP|$F5JprO0c2S|?dO(v+AR0Hc7L>>-f8o2J0!-7T25_9`$-MP}?GZv6QD57z#Odsop zkhc|Q=L-Fr#gY8%b(`BlX@H~FQ8sf}xYwQiznR41E@~TH5x&OXAds(qse4ryRu)$Y zco`?zNNw}Fd>j+Cbztr`mE$jYfgH{Y7+==Fy3TBlzjO;nXbcS8d z3tspUuq(ZDj;!pccRfl1ls9Ut_l3if4ppxi>v7~EJ)Q|P>uEzB-Geol-99}3;lRlJ zk|$;-{DvfXpE}xAEjw;PP=UR(E69U8)*!iF(B8ee`oU}}YyxwxUkw=EOB(j7pGt7n zkuHW3#IL>z{?gHCNxry!Y<}8QPt#x>W?%X|=zg>tY(ESFW}H+r_6|92_0bm-8wOAs za+VhWn+Rg>I|Gdzi<<*~TcKl*aZDb}wQr=QP!qps4dMMPBLhwxzTiS4j!t%g1wBpE zW|IPY44f>LgcXW`EX*pnInmgd4?lBbXUxJ-i{0J`dH3dtAb-J%6eG_EflKIPH*FwY zk?uTMMQ`YojP%ck%L0KAzvxu(%WIR13 zu4Y%39n$nvfhF?vorAW5@X6D7SKo-fb(0;GfhfA>m$DZv<4@vG7*fp1>Q)!2dW;GL zqg|+2LW58ugV#8C1D8`x9+JV4XKbalme1lYpqy$J!Jk{z4@qyLhSY=|Bl%xom!$UJ z%r|d%XehVe^8YDw5f>L*kC>S+AsbjBtHg2*Y5)e~z>*XREzJy})vA`&O4R@DQ3IL^ zH5v>LxOr{N_)2dL{pRQlOM#v?fsD4sW%T|6@+2?ngIW9vJ-A-RF+4E!%fDWz9Z^*_ zsT3QU;%{wtsPv;MW{B(%0^8#rHm&L|j>WB^7T8toIKdtZh-xIa6e!~uc{4F9*x?se zTqP=ux^AD5S1JY_sqA%D?s{QwKY2O%PTnRV!jZsEbnIm1ma*jJYV=9Cd6MGt)$lwB z9yX&3*Px)v`<0kR(>gK%tOpmqn?U`zHiNMB)6+5f;0?jnl!1HRhU!wW(LCp0^ z{}qS9A-%O*DEfM*v1?gZMLOHi`KjrUv_;R6N&nako}keWk)~0<;sfap+rhTSl#Y;r zk`o7VDzB!$2xKbbzwcL>YDg~lvv5}&2SxsGu;v;&?bX9CYt&QOrr41%m-o*0iJtPV zpFR>2O@}l3;;kvW`*U5a+>WM^3fnL(jthT1<}RuMNim#wg2zOK0AD{S>Apt>vtGka zj1X5GxF*%MR|?kbcqwzrx5zkX^2%7ivG4O&gDeVs3l=}~njv32BCpI3y`vp$TdgTNWYK66X$T zUY088DqQs2>>p`XsdImcL4~isTjokxpK1GOGsd!o!n)D~x$R&}8kIiL@@>&>Dh(id zAO`f)be<}76?A6KAaU9K*~pqM0+%j&Uarv#;gix|KV@GXFE&#IK#Yn2^ zTbm{y!YfAwE_A|eiCprbDp-JkUIs%tc_Rz%O%G}HSof=i#+*6(z0=?1`mXvPDk#r+ zrVR2Lp<^$mct5Yxt9~kXh;=*WkF`G82g@>LH$PdtXqZh_zPbOsf8N%a<2-FGn0-^k`1?PsG_pv|M@DF zohwZZ*4T*yHmD})w;u$BvS4?3>@FE8zdu-mO!2E`4Y(<;_Inwh=pJn52c|DU^C@?a z|8!7&KDt_KZ^S?5K608}Qev5ljFaf>G5~xGPs{yIK58bOCT{wZSW>X3$7CaLa zn3B%Xw0EZbadm&>D8nHYFHza|V1l*f1d6}E8)Z_K>}RvD9#tdh9w;sTtm$5nsm0~- zV;mp&d*HGYDh*Z;Ha9rr-pITfiW^}!_T!Nyi(?_;+6tY`8wZaT=7(vjkQRF1=Sm~? zc=tNy!{p8AP?7%#NA`0Z8Jy~tUYM{;JEjv;XrlxCfk945HY{rRi!+%LlHJi?6?nZ> zv|}L;=c3b4j71!+~0eA1D z`>`-y#NGCzCe~0;Ky{t<1)lH*atwj>eYW5#2S$RC-g0;zXa4s zZ{vfjyedvMN8^WAHsya`(v@N~tAMFu10CC_K0|!~t}tq3Y}1tK%YhBIFyJ1EZL0un=Z&* zCAx*C*hB}`y6v()Uop9Cuy!J0zVHdg_4xYF`Kk3c6DwvVod|trt5XVy=FQCYm@#b` zxhpo56l`)Dz!bI--`Y}yypPlKh_?{G14l!B$B6xM?35UlvqaOGU*l~HP1%)@QC5?f zEB&jnG$*y_(^u9|{Vcum0efKvKU|Z}7k^!k`nh-=-V-1aPSflRL7Z^zT=&1X#5#+ZUx%wG(h#9CK2oc zq0A+;HMO>$F_jh-{G($yS)S{Z9OJ?&a=CRPW&1pK}8kz{y(W|tD_zg!ot~`FwZz8dpBz36N_^JY0Q;n6w zYSw&_erCbCwV^vRDRHW>LBj?-j)L)88U3P2b2ZMyDpnQLBMfv<(h2N+p@hSvc7sS1 zK_vRn%q5=$l9Lme_=RUriX$2t!Eb_3@yz;ttiis!rh-Dmuahk-%%qUa><8UMBql5& z(n|2r+4Y2-9Fe3I70ce}azcXl{2TQJ^esIQ{~c5Htzus4DxQsnOZuipQ!4%_P3O9~ zv9#1>GX~1kK$6j0utoQiE|>v)RHTMFplp@aZP#z0cW9{(Jw?`|KtP}ari~mctPqyk zuQlFV+3FT_#UnrW_3${OefS24mMQ|rQAgT3tPxHuxy{MmxUyp;jteq}%yK&P7_r%C z_rCrtb*lR4_$QLnsb(DOE$WRAw5qhNhxX$>uBQ>%aq$NT60zh%4-m#Oau>)^(0cGQ z#`s?D0lvU&5aH)`u-TZxia?kj=f}u2Q=W`z9(i=k{ge1PYRB$!Bs6ay=>pGHuVv)l z(I4myt-O+^xvDnG@%glVi(&77(igqys`#=$-S8)Vsv!T(87Uup=am(M%?l2Jw57K) zW>|y1Zv8c0|L-glD4B5O_V?Q5FQXCEaCypShkhaBf|y%!ZG^8!-2FublqHnksyhxC5wrGEz+j~vs?6I%cC55)oiAxe;Z5b?RX`P74{84k|RfsTa zu-3$0kbG9je9Bz9KrfNhue*D2?6>~*b<^XC$42?}vLAuwRoj*lA79pd{o|^wzgVZh zy>j3hHP?Z46O3@u@))s%XBQFl11!d2b>m0pf^eM(5}iU~Fc7IeR?!^Rv0eizW#tP` z4DSLz0d{)H0uPZi^g6g2f0mUayOW@|rLk0R8gH51Bb>R~WI^0u$LQQDN)_-dv054n z(jSu%IjM&18y2mv+5j=!9;KjIGr|9=YtV%N7o=6MPd--{K&@Ylkis*3hW=ZL9SpH3 zlO!dfx-+%WIHA8?_^j_%bM6of(2GAL&`f;qGGpOK@zrmOWe^+_yJAl!Loh|wmg!hl ze{mT_Bj`5C>Yjgj(Lt5@b*&u@rHC~vm@2FT40prRy2NezyD*>`#n4(0>BReHUKZqv zGom4WLPEm=iMlp?B*0r)T`9Fw%$NM=o6T0B;k|G5J!{xVqr)XQ&6G0{pLn;~Ji}QM zjO2H)jf98koI;LY!(4KG4fY95|711a_EL%|VV`Ts)=t2N+u~FNas@ji`@jF-F9soD z6cE0iti!V@CBputU`31piDbdfC&SpZlsM`(IB7D*sP`676eW z%7e1V`WiNc`+0E=(GHBV$o^)Kp5~QD6ttT`OS~ih+!#CJ=9Vs<;|rj>ALB3p*r>di zkoH#0OC2fZllX)2kKl2XF=Bu<|HebWW9}b=Fyx!_-m}+|dMIfV0*%(teQnG2@%#P( z7w+HH+yp@M7Tra{*#XqNCuR(|}iD+e&$r^{V% zOEwF38B%-icM~$)t|J38U=&_iT5RnCmsd#0iD*A{?a znKLuc=}a8UKHn%i;k2ky?^OsVBMl#44i6I7#)IUJijMkNVY z@fz~A|BN3?DeKX%Dm^6OC&gOTDBuqjPKenqp!WOUw52q4s;{{`WK*6K4onr1`TdjaaJ%^q&TE?}O>S;XPXE(_|lP z?UCgwu$o!d5jG*eBU}W@7O_1!`PirEbDf%LL}`dt(~xod`f-|67~k3&p^F}{n^y(A zhi7C4UG)ZSVg3kPVCVSu9<6Pfob$){{ON%<>0G$MZqYd#N7tB;(HUQ!MX_Y6qm~bd z`s-H28}wUt=7=;@r%*Wk5+SXtLw$W^_!yʯDxKS6}=O$sx86 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 fE|T(NqmRg>nBZ`cfE!{z=9kSTkj literal 0 HcmV?d00001 diff --git a/scientific-reference-workbench/docs/demo.svg b/scientific-reference-workbench/docs/demo.svg new file mode 100644 index 0000000..f71b897 --- /dev/null +++ b/scientific-reference-workbench/docs/demo.svg @@ -0,0 +1,34 @@ + + Scientific reference workbench demo + Dashboard-style summary of citation coverage, cross references, formatting tasks, and export readiness. + + + Scientific Reference Workbench + Citation, figure, table, equation, and publication-style checks + + + Citations + 3 found + + + + Missing + 1 key + + + + Cross refs + 3 valid + + + + Sections + passed + + + + Top formatting task + Add bibliography entry for @missing2026 + Deterministic numbering ยท export blocked until references are clean + + diff --git a/scientific-reference-workbench/docs/requirement-map.md b/scientific-reference-workbench/docs/requirement-map.md new file mode 100644 index 0000000..cdba634 --- /dev/null +++ b/scientific-reference-workbench/docs/requirement-map.md @@ -0,0 +1,31 @@ +# Requirement Map + +This module contributes a focused rich-formatting slice for issue #12, "Real-time collaborative research editor & interface." + +| Issue area | Covered by this module | +| --- | --- | +| Markdown and LaTeX support | Tracks equation blocks and deterministic equation numbering | +| Cross-referencing figures, tables, and citations | Extracts citation keys and `{{label}}` references, validates coverage, and renders deterministic display labels | +| Reference manager integration | Verifies bibliography coverage for citation keys and surfaces unused bibliography entries | +| Publication-style templates | Checks required publication sections and blocks export when style requirements fail | +| Inline reviewer workflow | Emits owner-assigned formatting tasks for missing citations, missing cross-references, duplicate labels, and missing sections | + +## Distinctness + +Existing #12 submissions focus on broad collaborative editors, operation replay, governance, offline conflict resolution, and notebook collaboration. This module focuses on the publication-formatting layer that a scientific editor needs before export: + +- citation-key extraction from manuscript text +- bibliography coverage checks +- figure/table/equation label numbering +- unresolved cross-reference detection +- style-specific required-section checks +- reviewer-ready formatting task output + +## Verification + +```bash +cd scientific-reference-workbench +npm run check +npm test +npm run demo +``` diff --git a/scientific-reference-workbench/package.json b/scientific-reference-workbench/package.json new file mode 100644 index 0000000..8614f5f --- /dev/null +++ b/scientific-reference-workbench/package.json @@ -0,0 +1,18 @@ +{ + "name": "scientific-reference-workbench", + "version": "1.0.0", + "private": true, + "description": "Dependency-free scientific citation and cross-reference formatting workbench.", + "scripts": { + "check": "node --check src/reference-workbench.js && node --check scripts/demo.js && node --check test/reference-workbench.test.js", + "demo": "node scripts/demo.js", + "test": "node test/reference-workbench.test.js" + }, + "keywords": [ + "scientific-editor", + "citations", + "cross-references", + "publication-formatting" + ], + "license": "MIT" +} diff --git a/scientific-reference-workbench/scripts/demo.js b/scientific-reference-workbench/scripts/demo.js new file mode 100644 index 0000000..daa522c --- /dev/null +++ b/scientific-reference-workbench/scripts/demo.js @@ -0,0 +1,16 @@ +const fs = require("node:fs"); +const path = require("node:path"); +const { buildReferenceWorkbench } = require("../src/reference-workbench"); + +const samplePath = path.join(__dirname, "..", "data", "sample-manuscript.json"); +const manuscript = JSON.parse(fs.readFileSync(samplePath, "utf8")); +const report = buildReferenceWorkbench(manuscript); + +console.log(`Manuscript: ${report.title}`); +console.log(`Status: ${report.dashboard.status}`); +console.log(`Citations discovered: ${report.citations.uniqueKeys.length}`); +console.log(`Missing citations: ${report.citations.missing.join(", ") || "none"}`); +console.log(`Cross references: ${report.crossReferences.occurrences.length}`); +console.log(`Formatting tasks: ${report.tasks.length}`); +console.log(`Top action: ${report.dashboard.topAction}`); +console.log(`Digest: ${report.digest}`); diff --git a/scientific-reference-workbench/src/reference-workbench.js b/scientific-reference-workbench/src/reference-workbench.js new file mode 100644 index 0000000..05b4d5c --- /dev/null +++ b/scientific-reference-workbench/src/reference-workbench.js @@ -0,0 +1,305 @@ +const crypto = require("node:crypto"); + +function buildReferenceWorkbench(manuscript) { + const validation = validateManuscript(manuscript); + const labels = collectLabels(manuscript.sections || []); + const citations = collectCitations(manuscript.sections || [], manuscript.bibliography || []); + const crossReferences = collectCrossReferences(manuscript.sections || [], labels); + const numbering = buildNumbering(labels, citations); + const formattedBlocks = formatBlocks(manuscript.sections || [], numbering); + const styleChecklist = buildStyleChecklist(manuscript, citations, crossReferences, labels); + const tasks = buildFormattingTasks(citations, crossReferences, labels, styleChecklist); + const outline = buildPublicationOutline(manuscript, numbering, styleChecklist); + + const report = { + manuscriptId: manuscript.manuscriptId, + title: manuscript.title, + style: manuscript.style || {}, + validation, + numbering, + citations, + crossReferences, + styleChecklist, + tasks, + formattedBlocks, + outline, + dashboard: { + status: tasks.length === 0 ? "ready-to-export" : "format-review-needed", + taskCount: tasks.length, + missingCitationCount: citations.missing.length, + missingCrossReferenceCount: crossReferences.missing.length, + duplicateLabelCount: labels.duplicates.length, + topAction: tasks[0] ? tasks[0].message : "Export formatted manuscript packet." + } + }; + + report.digest = stableDigest(report); + return report; +} + +function validateManuscript(manuscript) { + const required = [ + ["manuscriptId", manuscript.manuscriptId], + ["title", manuscript.title], + ["sections", (manuscript.sections || []).length], + ["bibliography", (manuscript.bibliography || []).length] + ]; + const missing = required.filter(([, value]) => !value).map(([field]) => field); + const malformedBlocks = (manuscript.sections || []).flatMap((section) => + (section.blocks || []).filter((block) => !block.id || !block.type).map((block) => ({ + sectionId: section.id, + blockId: block.id || "unknown" + })) + ); + + return { + status: missing.length === 0 && malformedBlocks.length === 0 ? "passed" : "incomplete", + score: Math.max(0, 100 - missing.length * 15 - malformedBlocks.length * 5), + missing, + malformedBlocks + }; +} + +function collectLabels(sections) { + const all = []; + for (const section of sections) { + for (const block of section.blocks || []) { + if (block.label) { + all.push({ + label: block.label, + type: block.type, + blockId: block.id, + sectionId: section.id, + caption: block.caption || block.text || "" + }); + } + } + } + const counts = countBy(all, (item) => item.label); + return { + all, + byLabel: new Map(all.map((item) => [item.label, item])), + duplicates: all.filter((item) => counts.get(item.label) > 1) + }; +} + +function collectCitations(sections, bibliography) { + const bibliographyKeys = new Set(bibliography.map((entry) => entry.key)); + const occurrences = []; + for (const section of sections) { + for (const block of section.blocks || []) { + for (const key of extractCitationKeys(block.text || "")) { + occurrences.push({ key, sectionId: section.id, blockId: block.id }); + } + } + } + const uniqueKeys = Array.from(new Set(occurrences.map((item) => item.key))); + const missing = uniqueKeys.filter((key) => !bibliographyKeys.has(key)); + const unused = bibliography.map((entry) => entry.key).filter((key) => !uniqueKeys.includes(key)); + + return { + occurrences, + uniqueKeys, + missing, + unused + }; +} + +function collectCrossReferences(sections, labels) { + const occurrences = []; + for (const section of sections) { + for (const block of section.blocks || []) { + for (const label of extractCrossReferenceLabels(block.text || "")) { + occurrences.push({ label, sectionId: section.id, blockId: block.id }); + } + } + } + const missing = occurrences.filter((item) => !labels.byLabel.has(item.label)); + return { + occurrences, + missing + }; +} + +function buildNumbering(labels, citations) { + const citationNumbers = new Map(citations.uniqueKeys.map((key, index) => [key, index + 1])); + const grouped = { + figure: labels.all.filter((item) => item.type === "figure"), + table: labels.all.filter((item) => item.type === "table"), + equation: labels.all.filter((item) => item.type === "equation") + }; + const crossReferenceNumbers = new Map(); + for (const [type, items] of Object.entries(grouped)) { + items.forEach((item, index) => { + crossReferenceNumbers.set(item.label, { + type, + number: index + 1, + display: displayCrossReference(type, index + 1) + }); + }); + } + return { + citationNumbers: Object.fromEntries(citationNumbers), + crossReferenceNumbers: Object.fromEntries(crossReferenceNumbers) + }; +} + +function formatBlocks(sections, numbering) { + return sections.map((section) => ({ + id: section.id, + heading: section.heading, + blocks: (section.blocks || []).map((block) => ({ + id: block.id, + type: block.type, + text: formatText(block.text || block.caption || "", numbering) + })) + })); +} + +function buildStyleChecklist(manuscript, citations, crossReferences, labels) { + const presentSections = new Set((manuscript.sections || []).map((section) => section.id)); + const requiredSections = (manuscript.style && manuscript.style.requiredSections) || []; + const missingSections = requiredSections.filter((sectionId) => !presentSections.has(sectionId)); + + return { + requiredSections: { + status: missingSections.length === 0 ? "passed" : "failed", + missingSections + }, + bibliographyCoverage: { + status: citations.missing.length === 0 ? "passed" : "failed", + missingKeys: citations.missing, + unusedKeys: citations.unused + }, + crossReferenceIntegrity: { + status: crossReferences.missing.length === 0 && labels.duplicates.length === 0 ? "passed" : "failed", + missingLabels: crossReferences.missing.map((item) => item.label), + duplicateLabels: labels.duplicates.map((item) => item.label) + } + }; +} + +function buildFormattingTasks(citations, crossReferences, labels, checklist) { + const tasks = []; + for (const key of citations.missing) { + tasks.push({ + type: "missing-citation", + owner: "author", + target: key, + message: `Add bibliography entry for @${key} or remove the citation.` + }); + } + for (const item of crossReferences.missing) { + tasks.push({ + type: "missing-cross-reference", + owner: "editor", + target: item.label, + message: `Resolve ${item.label} referenced in block ${item.blockId}.` + }); + } + for (const label of new Set(labels.duplicates.map((item) => item.label))) { + tasks.push({ + type: "duplicate-label", + owner: "editor", + target: label, + message: `Rename duplicate label ${label} before export.` + }); + } + for (const sectionId of checklist.requiredSections.missingSections) { + tasks.push({ + type: "missing-required-section", + owner: "author", + target: sectionId, + message: `Add required ${sectionId} section for the selected publication style.` + }); + } + return tasks; +} + +function buildPublicationOutline(manuscript, numbering, checklist) { + return { + title: manuscript.title, + styleName: manuscript.style && manuscript.style.name, + sections: (manuscript.sections || []).map((section) => ({ + id: section.id, + heading: section.heading, + blockCount: (section.blocks || []).length + })), + citationCount: Object.keys(numbering.citationNumbers).length, + crossReferenceCount: Object.keys(numbering.crossReferenceNumbers).length, + exportBlocked: Object.values(checklist).some((item) => item.status === "failed") + }; +} + +function formatText(text, numbering) { + return text + .replace(/\[@([^\]]+)\]/g, (_match, group) => { + const keys = group.split(";").map((item) => item.trim().replace(/^@/, "")); + const rendered = keys.map((key) => numbering.citationNumbers[key] || "?").join(","); + return `[${rendered}]`; + }) + .replace(/\{\{([^}]+)\}\}/g, (_match, label) => { + const crossRef = numbering.crossReferenceNumbers[label]; + return crossRef ? crossRef.display : `UNRESOLVED(${label})`; + }); +} + +function extractCitationKeys(text) { + const keys = []; + const citationPattern = /\[@([^\]]+)\]/g; + let match; + while ((match = citationPattern.exec(text)) !== null) { + for (const item of match[1].split(";")) { + const key = item.trim().replace(/^@/, ""); + if (key) keys.push(key); + } + } + return keys; +} + +function extractCrossReferenceLabels(text) { + const labels = []; + const labelPattern = /\{\{([^}]+)\}\}/g; + let match; + while ((match = labelPattern.exec(text)) !== null) { + labels.push(match[1].trim()); + } + return labels; +} + +function displayCrossReference(type, number) { + if (type === "figure") return `Figure ${number}`; + if (type === "table") return `Table ${number}`; + if (type === "equation") return `Equation ${number}`; + return `${type} ${number}`; +} + +function countBy(items, select) { + const counts = new Map(); + for (const item of items) { + const key = select(item); + counts.set(key, (counts.get(key) || 0) + 1); + } + return counts; +} + +function stableDigest(value) { + return crypto.createHash("sha256").update(stableStringify(value)).digest("hex"); +} + +function stableStringify(value) { + if (value instanceof Map) return stableStringify(Object.fromEntries(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 = { + buildReferenceWorkbench, + validateManuscript, + extractCitationKeys, + extractCrossReferenceLabels, + stableDigest +}; diff --git a/scientific-reference-workbench/test/reference-workbench.test.js b/scientific-reference-workbench/test/reference-workbench.test.js new file mode 100644 index 0000000..7e64f4d --- /dev/null +++ b/scientific-reference-workbench/test/reference-workbench.test.js @@ -0,0 +1,55 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const { + buildReferenceWorkbench, + extractCitationKeys, + extractCrossReferenceLabels, + validateManuscript +} = require("../src/reference-workbench"); + +const samplePath = path.join(__dirname, "..", "data", "sample-manuscript.json"); +const manuscript = JSON.parse(fs.readFileSync(samplePath, "utf8")); +const report = buildReferenceWorkbench(manuscript); + +assert.equal(report.validation.status, "passed"); +assert.equal(report.dashboard.status, "format-review-needed"); +assert.equal(report.citations.uniqueKeys.length, 3); +assert.deepEqual(report.citations.missing, ["missing2026"]); +assert.deepEqual(report.citations.unused, ["unused2023"]); +assert.equal(report.crossReferences.missing.length, 0); +assert.equal(report.styleChecklist.requiredSections.status, "passed"); +assert.equal(report.styleChecklist.bibliographyCoverage.status, "failed"); +assert.equal(report.styleChecklist.crossReferenceIntegrity.status, "passed"); +assert.equal(report.tasks.length, 1); +assert.equal(report.tasks[0].type, "missing-citation"); +assert.equal(report.numbering.citationNumbers.nguyen2025, 1); +assert.equal(report.numbering.citationNumbers.park2024, 2); +assert.equal(report.numbering.crossReferenceNumbers["fig:workflow"].display, "Figure 1"); +assert.equal(report.numbering.crossReferenceNumbers["tbl:quality-gates"].display, "Table 1"); +assert.equal(report.numbering.crossReferenceNumbers["eq:weighted-loss"].display, "Equation 1"); +assert.ok(report.formattedBlocks[0].blocks[0].text.includes("[1]")); +assert.ok(report.formattedBlocks[1].blocks[0].text.includes("Equation 1")); +assert.equal(report.outline.exportBlocked, true); +assert.equal(report.digest, buildReferenceWorkbench(manuscript).digest); + +assert.deepEqual(extractCitationKeys("See [@a; @b] and [@c]."), ["a", "b", "c"]); +assert.deepEqual(extractCrossReferenceLabels("Use {{fig:a}} and {{eq:b}}."), ["fig:a", "eq:b"]); + +const incomplete = validateManuscript({ manuscriptId: "draft" }); +assert.equal(incomplete.status, "incomplete"); +assert.ok(incomplete.missing.includes("title")); + +const resolved = JSON.parse(JSON.stringify(manuscript)); +resolved.bibliography.push({ + key: "missing2026", + title: "Registered Manuscript Formatting", + authors: ["Ibrahim"], + year: 2026 +}); +const resolvedReport = buildReferenceWorkbench(resolved); +assert.equal(resolvedReport.dashboard.status, "ready-to-export"); +assert.equal(resolvedReport.tasks.length, 0); +assert.equal(resolvedReport.outline.exportBlocked, false); + +console.log("scientific-reference-workbench tests passed");