docrev 0.9.11 → 0.9.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/.claude/settings.local.json +9 -9
  2. package/.gitattributes +1 -1
  3. package/CHANGELOG.md +149 -149
  4. package/PLAN-tables-and-postprocess.md +850 -850
  5. package/README.md +391 -391
  6. package/bin/rev.js +11 -11
  7. package/bin/rev.ts +145 -145
  8. package/completions/rev.bash +127 -127
  9. package/completions/rev.ps1 +210 -210
  10. package/completions/rev.zsh +207 -207
  11. package/dev_notes/stress2/build_adversarial.ts +186 -186
  12. package/dev_notes/stress2/drift_matcher.ts +62 -62
  13. package/dev_notes/stress2/probe_anchors.ts +35 -35
  14. package/dev_notes/stress2/project/discussion.before.md +3 -3
  15. package/dev_notes/stress2/project/discussion.md +3 -3
  16. package/dev_notes/stress2/project/methods.before.md +20 -20
  17. package/dev_notes/stress2/project/methods.md +20 -20
  18. package/dev_notes/stress2/project/rev.yaml +5 -5
  19. package/dev_notes/stress2/project/sections.yaml +4 -4
  20. package/dev_notes/stress2/sections.yaml +5 -5
  21. package/dev_notes/stress2/trace_placement.ts +50 -50
  22. package/dev_notes/stresstest_boundaries.ts +27 -27
  23. package/dev_notes/stresstest_drift_apply.ts +43 -43
  24. package/dev_notes/stresstest_drift_compare.ts +43 -43
  25. package/dev_notes/stresstest_drift_v2.ts +54 -54
  26. package/dev_notes/stresstest_inspect.ts +54 -54
  27. package/dev_notes/stresstest_pstyle.ts +55 -55
  28. package/dev_notes/stresstest_section_debug.ts +23 -23
  29. package/dev_notes/stresstest_split.ts +70 -70
  30. package/dev_notes/stresstest_trace.ts +19 -19
  31. package/dev_notes/stresstest_verify_no_overwrite.ts +40 -40
  32. package/dist/lib/build.d.ts +50 -1
  33. package/dist/lib/build.d.ts.map +1 -1
  34. package/dist/lib/build.js +80 -30
  35. package/dist/lib/build.js.map +1 -1
  36. package/dist/lib/commands/build.d.ts.map +1 -1
  37. package/dist/lib/commands/build.js +38 -5
  38. package/dist/lib/commands/build.js.map +1 -1
  39. package/dist/lib/commands/utilities.js +164 -164
  40. package/dist/lib/commands/word-tools.js +8 -8
  41. package/dist/lib/grammar.js +3 -3
  42. package/dist/lib/import.d.ts.map +1 -1
  43. package/dist/lib/import.js +146 -24
  44. package/dist/lib/import.js.map +1 -1
  45. package/dist/lib/pdf-comments.js +44 -44
  46. package/dist/lib/plugins.js +57 -57
  47. package/dist/lib/pptx-themes.js +115 -115
  48. package/dist/lib/spelling.js +2 -2
  49. package/dist/lib/templates.js +387 -387
  50. package/dist/lib/themes.js +51 -51
  51. package/dist/lib/types.d.ts +20 -0
  52. package/dist/lib/types.d.ts.map +1 -1
  53. package/dist/lib/word-extraction.d.ts +6 -0
  54. package/dist/lib/word-extraction.d.ts.map +1 -1
  55. package/dist/lib/word-extraction.js +46 -3
  56. package/dist/lib/word-extraction.js.map +1 -1
  57. package/dist/lib/wordcomments.d.ts.map +1 -1
  58. package/dist/lib/wordcomments.js +23 -5
  59. package/dist/lib/wordcomments.js.map +1 -1
  60. package/eslint.config.js +27 -27
  61. package/lib/anchor-match.ts +276 -276
  62. package/lib/annotations.ts +644 -644
  63. package/lib/build.ts +1300 -1227
  64. package/lib/citations.ts +160 -160
  65. package/lib/commands/build.ts +833 -801
  66. package/lib/commands/citations.ts +515 -515
  67. package/lib/commands/comments.ts +1050 -1050
  68. package/lib/commands/context.ts +174 -174
  69. package/lib/commands/core.ts +309 -309
  70. package/lib/commands/doi.ts +435 -435
  71. package/lib/commands/file-ops.ts +372 -372
  72. package/lib/commands/history.ts +320 -320
  73. package/lib/commands/index.ts +87 -87
  74. package/lib/commands/init.ts +259 -259
  75. package/lib/commands/merge-resolve.ts +378 -378
  76. package/lib/commands/preview.ts +178 -178
  77. package/lib/commands/project-info.ts +244 -244
  78. package/lib/commands/quality.ts +517 -517
  79. package/lib/commands/response.ts +454 -454
  80. package/lib/commands/section-boundaries.ts +82 -82
  81. package/lib/commands/sections.ts +451 -451
  82. package/lib/commands/sync.ts +706 -706
  83. package/lib/commands/text-ops.ts +449 -449
  84. package/lib/commands/utilities.ts +448 -448
  85. package/lib/commands/verify-anchors.ts +272 -272
  86. package/lib/commands/word-tools.ts +340 -340
  87. package/lib/comment-realign.ts +517 -517
  88. package/lib/config.ts +84 -84
  89. package/lib/crossref.ts +781 -781
  90. package/lib/csl.ts +191 -191
  91. package/lib/dependencies.ts +98 -98
  92. package/lib/diff-engine.ts +465 -465
  93. package/lib/doi-cache.ts +115 -115
  94. package/lib/doi.ts +897 -897
  95. package/lib/equations.ts +506 -506
  96. package/lib/errors.ts +346 -346
  97. package/lib/format.ts +541 -541
  98. package/lib/git.ts +326 -326
  99. package/lib/grammar.ts +303 -303
  100. package/lib/image-registry.ts +180 -180
  101. package/lib/import.ts +911 -792
  102. package/lib/journals.ts +543 -543
  103. package/lib/merge.ts +633 -633
  104. package/lib/orcid.ts +144 -144
  105. package/lib/pdf-comments.ts +263 -263
  106. package/lib/pdf-import.ts +524 -524
  107. package/lib/plugins.ts +362 -362
  108. package/lib/postprocess.ts +188 -188
  109. package/lib/pptx-color-filter.lua +37 -37
  110. package/lib/pptx-template.ts +469 -469
  111. package/lib/pptx-themes.ts +483 -483
  112. package/lib/protect-restore.ts +520 -520
  113. package/lib/rate-limiter.ts +94 -94
  114. package/lib/response.ts +197 -197
  115. package/lib/restore-references.ts +240 -240
  116. package/lib/review.ts +327 -327
  117. package/lib/schema.ts +417 -417
  118. package/lib/scientific-words.ts +73 -73
  119. package/lib/sections.ts +335 -335
  120. package/lib/slides.ts +756 -756
  121. package/lib/spelling.ts +334 -334
  122. package/lib/templates.ts +526 -526
  123. package/lib/themes.ts +742 -742
  124. package/lib/trackchanges.ts +247 -247
  125. package/lib/tui.ts +450 -450
  126. package/lib/types.ts +550 -530
  127. package/lib/undo.ts +250 -250
  128. package/lib/utils.ts +69 -69
  129. package/lib/variables.ts +179 -179
  130. package/lib/word-extraction.ts +806 -759
  131. package/lib/word.ts +643 -643
  132. package/lib/wordcomments.ts +817 -798
  133. package/package.json +137 -137
  134. package/scripts/postbuild.js +28 -28
  135. package/skill/REFERENCE.md +431 -431
  136. package/skill/SKILL.md +258 -258
  137. package/tsconfig.json +26 -26
  138. package/types/index.d.ts +525 -525
package/lib/journals.ts CHANGED
@@ -1,543 +1,543 @@
1
- /**
2
- * Journal validation profiles
3
- * Check manuscripts against journal-specific requirements
4
- */
5
-
6
- import * as fs from 'fs';
7
- import * as path from 'path';
8
- import type { JournalProfile, JournalRequirements, JournalFormatting, ValidationResult } from './types.js';
9
- import { loadCustomProfiles } from './plugins.js';
10
- import { countWords } from './utils.js';
11
-
12
- /**
13
- * Journal requirement profiles
14
- * Based on publicly available author guidelines
15
- */
16
- export const JOURNAL_PROFILES: Record<string, JournalProfile> = {
17
- nature: {
18
- name: 'Nature',
19
- url: 'https://www.nature.com/nature/for-authors',
20
- requirements: {
21
- wordLimit: { main: 3000, abstract: 150 },
22
- references: { max: 50, doiRequired: true },
23
- figures: { max: 6 },
24
- sections: ['Abstract', 'Introduction', 'Results', 'Discussion', 'Methods'],
25
- },
26
- formatting: {
27
- csl: 'nature',
28
- pdf: { fontsize: '11pt', geometry: 'margin=2.5cm', linestretch: 2 },
29
- },
30
- },
31
-
32
- science: {
33
- name: 'Science',
34
- url: 'https://www.science.org/content/page/instructions-preparing-initial-manuscript',
35
- requirements: {
36
- wordLimit: { main: 2500, abstract: 125 },
37
- references: { max: 40, doiRequired: true },
38
- figures: { max: 4 },
39
- sections: ['Abstract', 'Introduction', 'Results', 'Discussion'],
40
- },
41
- formatting: {
42
- csl: 'science',
43
- pdf: { fontsize: '12pt', geometry: 'margin=1in', linestretch: 2 },
44
- },
45
- },
46
-
47
- 'plos-one': {
48
- name: 'PLOS ONE',
49
- url: 'https://journals.plos.org/plosone/s/submission-guidelines',
50
- requirements: {
51
- wordLimit: { abstract: 300 },
52
- references: { doiRequired: false },
53
- sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
54
- },
55
- formatting: {
56
- csl: 'plos',
57
- pdf: { fontsize: '12pt', geometry: 'margin=1in', linestretch: 2 },
58
- },
59
- },
60
-
61
- 'pnas': {
62
- name: 'PNAS',
63
- url: 'https://www.pnas.org/author-center/submitting-your-manuscript',
64
- requirements: {
65
- wordLimit: { main: 4500, abstract: 250 },
66
- references: { max: 50, doiRequired: true },
67
- figures: { max: 6 },
68
- sections: ['Abstract', 'Introduction', 'Results', 'Discussion'],
69
- },
70
- formatting: {
71
- csl: 'pnas',
72
- pdf: { documentclass: 'article', fontsize: '9pt', geometry: 'margin=2cm', linestretch: 1.2, numbersections: false },
73
- },
74
- },
75
-
76
- 'ecology-letters': {
77
- name: 'Ecology Letters',
78
- url: 'https://onlinelibrary.wiley.com/page/journal/14610248/homepage/forauthors.html',
79
- requirements: {
80
- wordLimit: { main: 5000, abstract: 150 },
81
- references: { max: 50, doiRequired: true },
82
- figures: { max: 6 },
83
- sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
84
- },
85
- },
86
-
87
- 'ecological-applications': {
88
- name: 'Ecological Applications',
89
- url: 'https://esajournals.onlinelibrary.wiley.com/hub/journal/19395582/author-guidelines',
90
- requirements: {
91
- wordLimit: { main: 7000, abstract: 350 },
92
- references: { doiRequired: true },
93
- sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
94
- },
95
- },
96
-
97
- 'molecular-ecology': {
98
- name: 'Molecular Ecology',
99
- url: 'https://onlinelibrary.wiley.com/page/journal/1365294x/homepage/forauthors.html',
100
- requirements: {
101
- wordLimit: { main: 8000, abstract: 250 },
102
- references: { doiRequired: true },
103
- figures: { max: 8 },
104
- sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
105
- },
106
- },
107
-
108
- 'elife': {
109
- name: 'eLife',
110
- url: 'https://reviewer.elifesciences.org/author-guide/full',
111
- requirements: {
112
- wordLimit: { abstract: 150 },
113
- references: { doiRequired: true },
114
- sections: ['Abstract', 'Introduction', 'Results', 'Discussion', 'Methods'],
115
- },
116
- formatting: {
117
- csl: 'elife',
118
- pdf: { fontsize: '11pt', geometry: 'margin=2.5cm', linestretch: 1.5 },
119
- },
120
- },
121
-
122
- 'cell': {
123
- name: 'Cell',
124
- url: 'https://www.cell.com/cell/authors',
125
- requirements: {
126
- wordLimit: { main: 7000, abstract: 150 },
127
- references: { max: 100, doiRequired: true },
128
- figures: { max: 7 },
129
- sections: ['Abstract', 'Introduction', 'Results', 'Discussion'],
130
- },
131
- formatting: {
132
- csl: 'cell',
133
- pdf: { fontsize: '12pt', geometry: 'margin=2.5cm', linestretch: 2 },
134
- },
135
- },
136
-
137
- 'current-biology': {
138
- name: 'Current Biology',
139
- url: 'https://www.cell.com/current-biology/authors',
140
- requirements: {
141
- wordLimit: { main: 5000, abstract: 150 },
142
- references: { max: 60, doiRequired: true },
143
- figures: { max: 4 },
144
- sections: ['Summary', 'Results', 'Discussion'],
145
- },
146
- },
147
-
148
- 'conservation-biology': {
149
- name: 'Conservation Biology',
150
- url: 'https://conbio.onlinelibrary.wiley.com/hub/journal/15231739/homepage/forauthors.html',
151
- requirements: {
152
- wordLimit: { main: 7000, abstract: 300 },
153
- references: { doiRequired: true },
154
- figures: { max: 6 },
155
- sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
156
- },
157
- },
158
-
159
- 'biological-conservation': {
160
- name: 'Biological Conservation',
161
- url: 'https://www.elsevier.com/journals/biological-conservation/0006-3207/guide-for-authors',
162
- requirements: {
163
- wordLimit: { main: 8000, abstract: 400 },
164
- references: { doiRequired: true },
165
- sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
166
- },
167
- },
168
-
169
- 'journal-of-ecology': {
170
- name: 'Journal of Ecology',
171
- url: 'https://besjournals.onlinelibrary.wiley.com/hub/journal/13652745/author-guidelines',
172
- requirements: {
173
- wordLimit: { main: 7000, abstract: 350 },
174
- references: { doiRequired: true },
175
- sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
176
- },
177
- },
178
-
179
- 'functional-ecology': {
180
- name: 'Functional Ecology',
181
- url: 'https://besjournals.onlinelibrary.wiley.com/hub/journal/13652435/author-guidelines',
182
- requirements: {
183
- wordLimit: { main: 7000, abstract: 350 },
184
- references: { doiRequired: true },
185
- sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
186
- },
187
- },
188
-
189
- 'global-change-biology': {
190
- name: 'Global Change Biology',
191
- url: 'https://onlinelibrary.wiley.com/page/journal/13652486/homepage/forauthors.html',
192
- requirements: {
193
- wordLimit: { main: 7000, abstract: 300 },
194
- references: { doiRequired: true },
195
- figures: { max: 8 },
196
- sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
197
- },
198
- },
199
-
200
- 'oikos': {
201
- name: 'Oikos',
202
- url: 'https://nsojournals.onlinelibrary.wiley.com/hub/journal/16000706/author-guidelines',
203
- requirements: {
204
- wordLimit: { main: 8000, abstract: 350 },
205
- references: { doiRequired: true },
206
- sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
207
- },
208
- },
209
-
210
- 'oecologia': {
211
- name: 'Oecologia',
212
- url: 'https://www.springer.com/journal/442/submission-guidelines',
213
- requirements: {
214
- wordLimit: { main: 8000, abstract: 250 },
215
- references: { doiRequired: true },
216
- sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
217
- },
218
- },
219
-
220
- 'biological-invasions': {
221
- name: 'Biological Invasions',
222
- url: 'https://www.springer.com/journal/10530/submission-guidelines',
223
- requirements: {
224
- wordLimit: { abstract: 250 },
225
- references: { doiRequired: true },
226
- sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
227
- },
228
- },
229
-
230
- 'diversity-distributions': {
231
- name: 'Diversity and Distributions',
232
- url: 'https://onlinelibrary.wiley.com/page/journal/14724642/homepage/forauthors.html',
233
- requirements: {
234
- wordLimit: { main: 6000, abstract: 300 },
235
- references: { doiRequired: true },
236
- figures: { max: 6 },
237
- sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
238
- },
239
- },
240
-
241
- 'neobiota': {
242
- name: 'NeoBiota',
243
- url: 'https://neobiota.pensoft.net/about#Author_Guidelines',
244
- requirements: {
245
- wordLimit: { abstract: 350 },
246
- references: { doiRequired: true },
247
- sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
248
- },
249
- },
250
-
251
- 'peerj': {
252
- name: 'PeerJ',
253
- url: 'https://peerj.com/about/author-instructions/',
254
- requirements: {
255
- wordLimit: { abstract: 500 },
256
- references: { doiRequired: false },
257
- sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
258
- },
259
- },
260
-
261
- 'methods-ecology-evolution': {
262
- name: 'Methods in Ecology and Evolution',
263
- url: 'https://besjournals.onlinelibrary.wiley.com/hub/journal/2041210x/author-guidelines',
264
- requirements: {
265
- wordLimit: { main: 7000, abstract: 350 },
266
- references: { doiRequired: true },
267
- sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
268
- },
269
- formatting: {
270
- csl: 'methods-in-ecology-and-evolution',
271
- pdf: { fontsize: '12pt', geometry: 'margin=2.5cm', linestretch: 2 },
272
- },
273
- },
274
- };
275
-
276
- /**
277
- * Get all profiles (built-in + custom)
278
- * Custom profiles override built-in ones with the same ID
279
- */
280
- function getAllProfiles(): Record<string, JournalProfile> {
281
- const customProfiles = loadCustomProfiles() as Record<string, JournalProfile>;
282
- return { ...JOURNAL_PROFILES, ...customProfiles };
283
- }
284
-
285
- interface ListJournalsOptions {
286
- includeCustom?: boolean;
287
- customOnly?: boolean;
288
- }
289
-
290
- interface JournalListItem {
291
- id: string;
292
- name: string;
293
- url: string;
294
- custom?: boolean;
295
- }
296
-
297
- /**
298
- * List all available journal profiles
299
- */
300
- export function listJournals(options: ListJournalsOptions = {}): JournalListItem[] {
301
- const { includeCustom = true, customOnly = false } = options;
302
-
303
- const profiles = customOnly
304
- ? (loadCustomProfiles() as Record<string, JournalProfile>)
305
- : includeCustom
306
- ? getAllProfiles()
307
- : JOURNAL_PROFILES;
308
-
309
- return Object.entries(profiles).map(([id, profile]) => ({
310
- id,
311
- name: profile.name,
312
- url: profile.url,
313
- custom: (profile as any).custom || false,
314
- }));
315
- }
316
-
317
- /**
318
- * Get a specific journal profile
319
- */
320
- export function getJournalProfile(journalId: string): JournalProfile | null {
321
- const normalized = journalId.toLowerCase().replace(/\s+/g, '-');
322
- const profiles = getAllProfiles();
323
- return profiles[normalized] || null;
324
- }
325
-
326
-
327
- /**
328
- * Extract abstract from markdown
329
- */
330
- function extractAbstract(text: string): string | null {
331
- // Try to find abstract section
332
- const patterns = [
333
- /^#+\s*Abstract\s*\n([\s\S]*?)(?=^#+|\Z)/mi,
334
- /^Abstract[:\s]*\n([\s\S]*?)(?=^#+|\n\n)/mi,
335
- ];
336
-
337
- for (const pattern of patterns) {
338
- const match = text.match(pattern);
339
- if (match && match[1]) {
340
- return match[1].trim();
341
- }
342
- }
343
-
344
- return null;
345
- }
346
-
347
- /**
348
- * Extract title from markdown
349
- */
350
- function extractTitle(text: string): string | null {
351
- // Try YAML frontmatter
352
- const yamlMatch = text.match(/^---\n[\s\S]*?title:\s*["']?([^"'\n]+)["']?[\s\S]*?\n---/m);
353
- if (yamlMatch && yamlMatch[1]) {
354
- return yamlMatch[1].trim();
355
- }
356
-
357
- // Try first H1
358
- const h1Match = text.match(/^#\s+(.+)$/m);
359
- if (h1Match && h1Match[1]) {
360
- return h1Match[1].trim();
361
- }
362
-
363
- return null;
364
- }
365
-
366
- /**
367
- * Extract sections from markdown
368
- */
369
- function extractSections(text: string): string[] {
370
- const sections: string[] = [];
371
- const headerPattern = /^#+\s+(.+)$/gm;
372
- let match: RegExpExecArray | null;
373
-
374
- while ((match = headerPattern.exec(text)) !== null) {
375
- if (match[1]) {
376
- sections.push(match[1].trim());
377
- }
378
- }
379
-
380
- return sections;
381
- }
382
-
383
- /**
384
- * Count figures in markdown
385
- */
386
- function countFigures(text: string): number {
387
- // Count images with figure captions
388
- const figurePattern = /!\[.*?\]\(.*?\)(\{#fig:[^}]+\})?/g;
389
- const matches = text.match(figurePattern) || [];
390
- return matches.length;
391
- }
392
-
393
- /**
394
- * Count tables in markdown
395
- */
396
- function countTables(text: string): number {
397
- // Count tables (lines starting with |)
398
- const tablePattern = /^\|[^|]+\|/gm;
399
- const matches = text.match(tablePattern) || [];
400
- // Divide by approximate rows per table
401
- return Math.ceil(matches.length / 5);
402
- }
403
-
404
- /**
405
- * Count references/citations in markdown
406
- */
407
- function countReferences(text: string): number {
408
- // Count unique citation keys
409
- const citationPattern = /@(\w+)/g;
410
- const citations = new Set<string>();
411
- let match: RegExpExecArray | null;
412
-
413
- while ((match = citationPattern.exec(text)) !== null) {
414
- // Exclude cross-refs like @fig:label
415
- if (match[1] && !match[0].includes(':')) {
416
- citations.add(match[1]);
417
- }
418
- }
419
-
420
- return citations.size;
421
- }
422
-
423
- interface ManuscriptStats {
424
- wordCount: number;
425
- abstractWords: number;
426
- titleChars: number;
427
- figures: number;
428
- tables: number;
429
- references: number;
430
- sections: number;
431
- }
432
-
433
- interface ManuscriptValidationResult {
434
- valid: boolean;
435
- errors: string[];
436
- warnings: string[];
437
- stats: ManuscriptStats | null;
438
- journal?: string;
439
- url?: string;
440
- }
441
-
442
- /**
443
- * Validate manuscript against journal requirements
444
- */
445
- export function validateManuscript(text: string, journalId: string): ManuscriptValidationResult {
446
- const profile = getJournalProfile(journalId);
447
-
448
- if (!profile) {
449
- return {
450
- valid: false,
451
- errors: [`Unknown journal: ${journalId}`],
452
- warnings: [],
453
- stats: null,
454
- };
455
- }
456
-
457
- const req = profile.requirements;
458
- const errors: string[] = [];
459
- const warnings: string[] = [];
460
-
461
- // Extract content
462
- const abstract = extractAbstract(text);
463
- const title = extractTitle(text);
464
- const sections = extractSections(text);
465
- const mainWordCount = countWords(text);
466
- const figureCount = countFigures(text);
467
- const tableCount = countTables(text);
468
- const refCount = countReferences(text);
469
-
470
- const stats: ManuscriptStats = {
471
- wordCount: mainWordCount,
472
- abstractWords: abstract ? countWords(abstract) : 0,
473
- titleChars: title ? title.length : 0,
474
- figures: figureCount,
475
- tables: tableCount,
476
- references: refCount,
477
- sections: sections.length,
478
- };
479
-
480
- // Word limits
481
- if (req.wordLimit) {
482
- if (req.wordLimit.main && mainWordCount > req.wordLimit.main) {
483
- errors.push(`Main text exceeds ${req.wordLimit.main} words (current: ${mainWordCount})`);
484
- }
485
- if (req.wordLimit.abstract && abstract) {
486
- const absWords = countWords(abstract);
487
- if (absWords > req.wordLimit.abstract) {
488
- errors.push(`Abstract exceeds ${req.wordLimit.abstract} words (current: ${absWords})`);
489
- }
490
- }
491
- }
492
-
493
- // References
494
- if (req.references) {
495
- if (req.references.max && refCount > req.references.max) {
496
- errors.push(`References exceed ${req.references.max} (current: ${refCount})`);
497
- }
498
- if (req.references.doiRequired) {
499
- warnings.push('DOI required for all references - run "rev doi check" to verify');
500
- }
501
- }
502
-
503
- // Figures/tables
504
- if (req.figures) {
505
- if (req.figures.max && figureCount > req.figures.max) {
506
- errors.push(`Figures exceed ${req.figures.max} (current: ${figureCount})`);
507
- }
508
- }
509
-
510
- // Required sections
511
- if (req.sections) {
512
- for (const reqSection of req.sections) {
513
- const found = sections.some(s =>
514
- s.toLowerCase().includes(reqSection.toLowerCase())
515
- );
516
- if (!found) {
517
- warnings.push(`Missing required section: ${reqSection}`);
518
- }
519
- }
520
- }
521
-
522
- return {
523
- valid: errors.length === 0,
524
- errors,
525
- warnings,
526
- stats,
527
- journal: profile.name,
528
- url: profile.url,
529
- };
530
- }
531
-
532
- /**
533
- * Validate multiple files against journal requirements
534
- */
535
- export function validateProject(files: string[], journalId: string): ManuscriptValidationResult {
536
- // Combine all file contents
537
- const combined = files
538
- .filter(f => fs.existsSync(f))
539
- .map(f => fs.readFileSync(f, 'utf-8'))
540
- .join('\n\n');
541
-
542
- return validateManuscript(combined, journalId);
543
- }
1
+ /**
2
+ * Journal validation profiles
3
+ * Check manuscripts against journal-specific requirements
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import type { JournalProfile, JournalRequirements, JournalFormatting, ValidationResult } from './types.js';
9
+ import { loadCustomProfiles } from './plugins.js';
10
+ import { countWords } from './utils.js';
11
+
12
+ /**
13
+ * Journal requirement profiles
14
+ * Based on publicly available author guidelines
15
+ */
16
+ export const JOURNAL_PROFILES: Record<string, JournalProfile> = {
17
+ nature: {
18
+ name: 'Nature',
19
+ url: 'https://www.nature.com/nature/for-authors',
20
+ requirements: {
21
+ wordLimit: { main: 3000, abstract: 150 },
22
+ references: { max: 50, doiRequired: true },
23
+ figures: { max: 6 },
24
+ sections: ['Abstract', 'Introduction', 'Results', 'Discussion', 'Methods'],
25
+ },
26
+ formatting: {
27
+ csl: 'nature',
28
+ pdf: { fontsize: '11pt', geometry: 'margin=2.5cm', linestretch: 2 },
29
+ },
30
+ },
31
+
32
+ science: {
33
+ name: 'Science',
34
+ url: 'https://www.science.org/content/page/instructions-preparing-initial-manuscript',
35
+ requirements: {
36
+ wordLimit: { main: 2500, abstract: 125 },
37
+ references: { max: 40, doiRequired: true },
38
+ figures: { max: 4 },
39
+ sections: ['Abstract', 'Introduction', 'Results', 'Discussion'],
40
+ },
41
+ formatting: {
42
+ csl: 'science',
43
+ pdf: { fontsize: '12pt', geometry: 'margin=1in', linestretch: 2 },
44
+ },
45
+ },
46
+
47
+ 'plos-one': {
48
+ name: 'PLOS ONE',
49
+ url: 'https://journals.plos.org/plosone/s/submission-guidelines',
50
+ requirements: {
51
+ wordLimit: { abstract: 300 },
52
+ references: { doiRequired: false },
53
+ sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
54
+ },
55
+ formatting: {
56
+ csl: 'plos',
57
+ pdf: { fontsize: '12pt', geometry: 'margin=1in', linestretch: 2 },
58
+ },
59
+ },
60
+
61
+ 'pnas': {
62
+ name: 'PNAS',
63
+ url: 'https://www.pnas.org/author-center/submitting-your-manuscript',
64
+ requirements: {
65
+ wordLimit: { main: 4500, abstract: 250 },
66
+ references: { max: 50, doiRequired: true },
67
+ figures: { max: 6 },
68
+ sections: ['Abstract', 'Introduction', 'Results', 'Discussion'],
69
+ },
70
+ formatting: {
71
+ csl: 'pnas',
72
+ pdf: { documentclass: 'article', fontsize: '9pt', geometry: 'margin=2cm', linestretch: 1.2, numbersections: false },
73
+ },
74
+ },
75
+
76
+ 'ecology-letters': {
77
+ name: 'Ecology Letters',
78
+ url: 'https://onlinelibrary.wiley.com/page/journal/14610248/homepage/forauthors.html',
79
+ requirements: {
80
+ wordLimit: { main: 5000, abstract: 150 },
81
+ references: { max: 50, doiRequired: true },
82
+ figures: { max: 6 },
83
+ sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
84
+ },
85
+ },
86
+
87
+ 'ecological-applications': {
88
+ name: 'Ecological Applications',
89
+ url: 'https://esajournals.onlinelibrary.wiley.com/hub/journal/19395582/author-guidelines',
90
+ requirements: {
91
+ wordLimit: { main: 7000, abstract: 350 },
92
+ references: { doiRequired: true },
93
+ sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
94
+ },
95
+ },
96
+
97
+ 'molecular-ecology': {
98
+ name: 'Molecular Ecology',
99
+ url: 'https://onlinelibrary.wiley.com/page/journal/1365294x/homepage/forauthors.html',
100
+ requirements: {
101
+ wordLimit: { main: 8000, abstract: 250 },
102
+ references: { doiRequired: true },
103
+ figures: { max: 8 },
104
+ sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
105
+ },
106
+ },
107
+
108
+ 'elife': {
109
+ name: 'eLife',
110
+ url: 'https://reviewer.elifesciences.org/author-guide/full',
111
+ requirements: {
112
+ wordLimit: { abstract: 150 },
113
+ references: { doiRequired: true },
114
+ sections: ['Abstract', 'Introduction', 'Results', 'Discussion', 'Methods'],
115
+ },
116
+ formatting: {
117
+ csl: 'elife',
118
+ pdf: { fontsize: '11pt', geometry: 'margin=2.5cm', linestretch: 1.5 },
119
+ },
120
+ },
121
+
122
+ 'cell': {
123
+ name: 'Cell',
124
+ url: 'https://www.cell.com/cell/authors',
125
+ requirements: {
126
+ wordLimit: { main: 7000, abstract: 150 },
127
+ references: { max: 100, doiRequired: true },
128
+ figures: { max: 7 },
129
+ sections: ['Abstract', 'Introduction', 'Results', 'Discussion'],
130
+ },
131
+ formatting: {
132
+ csl: 'cell',
133
+ pdf: { fontsize: '12pt', geometry: 'margin=2.5cm', linestretch: 2 },
134
+ },
135
+ },
136
+
137
+ 'current-biology': {
138
+ name: 'Current Biology',
139
+ url: 'https://www.cell.com/current-biology/authors',
140
+ requirements: {
141
+ wordLimit: { main: 5000, abstract: 150 },
142
+ references: { max: 60, doiRequired: true },
143
+ figures: { max: 4 },
144
+ sections: ['Summary', 'Results', 'Discussion'],
145
+ },
146
+ },
147
+
148
+ 'conservation-biology': {
149
+ name: 'Conservation Biology',
150
+ url: 'https://conbio.onlinelibrary.wiley.com/hub/journal/15231739/homepage/forauthors.html',
151
+ requirements: {
152
+ wordLimit: { main: 7000, abstract: 300 },
153
+ references: { doiRequired: true },
154
+ figures: { max: 6 },
155
+ sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
156
+ },
157
+ },
158
+
159
+ 'biological-conservation': {
160
+ name: 'Biological Conservation',
161
+ url: 'https://www.elsevier.com/journals/biological-conservation/0006-3207/guide-for-authors',
162
+ requirements: {
163
+ wordLimit: { main: 8000, abstract: 400 },
164
+ references: { doiRequired: true },
165
+ sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
166
+ },
167
+ },
168
+
169
+ 'journal-of-ecology': {
170
+ name: 'Journal of Ecology',
171
+ url: 'https://besjournals.onlinelibrary.wiley.com/hub/journal/13652745/author-guidelines',
172
+ requirements: {
173
+ wordLimit: { main: 7000, abstract: 350 },
174
+ references: { doiRequired: true },
175
+ sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
176
+ },
177
+ },
178
+
179
+ 'functional-ecology': {
180
+ name: 'Functional Ecology',
181
+ url: 'https://besjournals.onlinelibrary.wiley.com/hub/journal/13652435/author-guidelines',
182
+ requirements: {
183
+ wordLimit: { main: 7000, abstract: 350 },
184
+ references: { doiRequired: true },
185
+ sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
186
+ },
187
+ },
188
+
189
+ 'global-change-biology': {
190
+ name: 'Global Change Biology',
191
+ url: 'https://onlinelibrary.wiley.com/page/journal/13652486/homepage/forauthors.html',
192
+ requirements: {
193
+ wordLimit: { main: 7000, abstract: 300 },
194
+ references: { doiRequired: true },
195
+ figures: { max: 8 },
196
+ sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
197
+ },
198
+ },
199
+
200
+ 'oikos': {
201
+ name: 'Oikos',
202
+ url: 'https://nsojournals.onlinelibrary.wiley.com/hub/journal/16000706/author-guidelines',
203
+ requirements: {
204
+ wordLimit: { main: 8000, abstract: 350 },
205
+ references: { doiRequired: true },
206
+ sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
207
+ },
208
+ },
209
+
210
+ 'oecologia': {
211
+ name: 'Oecologia',
212
+ url: 'https://www.springer.com/journal/442/submission-guidelines',
213
+ requirements: {
214
+ wordLimit: { main: 8000, abstract: 250 },
215
+ references: { doiRequired: true },
216
+ sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
217
+ },
218
+ },
219
+
220
+ 'biological-invasions': {
221
+ name: 'Biological Invasions',
222
+ url: 'https://www.springer.com/journal/10530/submission-guidelines',
223
+ requirements: {
224
+ wordLimit: { abstract: 250 },
225
+ references: { doiRequired: true },
226
+ sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
227
+ },
228
+ },
229
+
230
+ 'diversity-distributions': {
231
+ name: 'Diversity and Distributions',
232
+ url: 'https://onlinelibrary.wiley.com/page/journal/14724642/homepage/forauthors.html',
233
+ requirements: {
234
+ wordLimit: { main: 6000, abstract: 300 },
235
+ references: { doiRequired: true },
236
+ figures: { max: 6 },
237
+ sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
238
+ },
239
+ },
240
+
241
+ 'neobiota': {
242
+ name: 'NeoBiota',
243
+ url: 'https://neobiota.pensoft.net/about#Author_Guidelines',
244
+ requirements: {
245
+ wordLimit: { abstract: 350 },
246
+ references: { doiRequired: true },
247
+ sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
248
+ },
249
+ },
250
+
251
+ 'peerj': {
252
+ name: 'PeerJ',
253
+ url: 'https://peerj.com/about/author-instructions/',
254
+ requirements: {
255
+ wordLimit: { abstract: 500 },
256
+ references: { doiRequired: false },
257
+ sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
258
+ },
259
+ },
260
+
261
+ 'methods-ecology-evolution': {
262
+ name: 'Methods in Ecology and Evolution',
263
+ url: 'https://besjournals.onlinelibrary.wiley.com/hub/journal/2041210x/author-guidelines',
264
+ requirements: {
265
+ wordLimit: { main: 7000, abstract: 350 },
266
+ references: { doiRequired: true },
267
+ sections: ['Abstract', 'Introduction', 'Methods', 'Results', 'Discussion'],
268
+ },
269
+ formatting: {
270
+ csl: 'methods-in-ecology-and-evolution',
271
+ pdf: { fontsize: '12pt', geometry: 'margin=2.5cm', linestretch: 2 },
272
+ },
273
+ },
274
+ };
275
+
276
+ /**
277
+ * Get all profiles (built-in + custom)
278
+ * Custom profiles override built-in ones with the same ID
279
+ */
280
+ function getAllProfiles(): Record<string, JournalProfile> {
281
+ const customProfiles = loadCustomProfiles() as Record<string, JournalProfile>;
282
+ return { ...JOURNAL_PROFILES, ...customProfiles };
283
+ }
284
+
285
+ interface ListJournalsOptions {
286
+ includeCustom?: boolean;
287
+ customOnly?: boolean;
288
+ }
289
+
290
+ interface JournalListItem {
291
+ id: string;
292
+ name: string;
293
+ url: string;
294
+ custom?: boolean;
295
+ }
296
+
297
+ /**
298
+ * List all available journal profiles
299
+ */
300
+ export function listJournals(options: ListJournalsOptions = {}): JournalListItem[] {
301
+ const { includeCustom = true, customOnly = false } = options;
302
+
303
+ const profiles = customOnly
304
+ ? (loadCustomProfiles() as Record<string, JournalProfile>)
305
+ : includeCustom
306
+ ? getAllProfiles()
307
+ : JOURNAL_PROFILES;
308
+
309
+ return Object.entries(profiles).map(([id, profile]) => ({
310
+ id,
311
+ name: profile.name,
312
+ url: profile.url,
313
+ custom: (profile as any).custom || false,
314
+ }));
315
+ }
316
+
317
+ /**
318
+ * Get a specific journal profile
319
+ */
320
+ export function getJournalProfile(journalId: string): JournalProfile | null {
321
+ const normalized = journalId.toLowerCase().replace(/\s+/g, '-');
322
+ const profiles = getAllProfiles();
323
+ return profiles[normalized] || null;
324
+ }
325
+
326
+
327
+ /**
328
+ * Extract abstract from markdown
329
+ */
330
+ function extractAbstract(text: string): string | null {
331
+ // Try to find abstract section
332
+ const patterns = [
333
+ /^#+\s*Abstract\s*\n([\s\S]*?)(?=^#+|\Z)/mi,
334
+ /^Abstract[:\s]*\n([\s\S]*?)(?=^#+|\n\n)/mi,
335
+ ];
336
+
337
+ for (const pattern of patterns) {
338
+ const match = text.match(pattern);
339
+ if (match && match[1]) {
340
+ return match[1].trim();
341
+ }
342
+ }
343
+
344
+ return null;
345
+ }
346
+
347
+ /**
348
+ * Extract title from markdown
349
+ */
350
+ function extractTitle(text: string): string | null {
351
+ // Try YAML frontmatter
352
+ const yamlMatch = text.match(/^---\n[\s\S]*?title:\s*["']?([^"'\n]+)["']?[\s\S]*?\n---/m);
353
+ if (yamlMatch && yamlMatch[1]) {
354
+ return yamlMatch[1].trim();
355
+ }
356
+
357
+ // Try first H1
358
+ const h1Match = text.match(/^#\s+(.+)$/m);
359
+ if (h1Match && h1Match[1]) {
360
+ return h1Match[1].trim();
361
+ }
362
+
363
+ return null;
364
+ }
365
+
366
+ /**
367
+ * Extract sections from markdown
368
+ */
369
+ function extractSections(text: string): string[] {
370
+ const sections: string[] = [];
371
+ const headerPattern = /^#+\s+(.+)$/gm;
372
+ let match: RegExpExecArray | null;
373
+
374
+ while ((match = headerPattern.exec(text)) !== null) {
375
+ if (match[1]) {
376
+ sections.push(match[1].trim());
377
+ }
378
+ }
379
+
380
+ return sections;
381
+ }
382
+
383
+ /**
384
+ * Count figures in markdown
385
+ */
386
+ function countFigures(text: string): number {
387
+ // Count images with figure captions
388
+ const figurePattern = /!\[.*?\]\(.*?\)(\{#fig:[^}]+\})?/g;
389
+ const matches = text.match(figurePattern) || [];
390
+ return matches.length;
391
+ }
392
+
393
+ /**
394
+ * Count tables in markdown
395
+ */
396
+ function countTables(text: string): number {
397
+ // Count tables (lines starting with |)
398
+ const tablePattern = /^\|[^|]+\|/gm;
399
+ const matches = text.match(tablePattern) || [];
400
+ // Divide by approximate rows per table
401
+ return Math.ceil(matches.length / 5);
402
+ }
403
+
404
+ /**
405
+ * Count references/citations in markdown
406
+ */
407
+ function countReferences(text: string): number {
408
+ // Count unique citation keys
409
+ const citationPattern = /@(\w+)/g;
410
+ const citations = new Set<string>();
411
+ let match: RegExpExecArray | null;
412
+
413
+ while ((match = citationPattern.exec(text)) !== null) {
414
+ // Exclude cross-refs like @fig:label
415
+ if (match[1] && !match[0].includes(':')) {
416
+ citations.add(match[1]);
417
+ }
418
+ }
419
+
420
+ return citations.size;
421
+ }
422
+
423
+ interface ManuscriptStats {
424
+ wordCount: number;
425
+ abstractWords: number;
426
+ titleChars: number;
427
+ figures: number;
428
+ tables: number;
429
+ references: number;
430
+ sections: number;
431
+ }
432
+
433
+ interface ManuscriptValidationResult {
434
+ valid: boolean;
435
+ errors: string[];
436
+ warnings: string[];
437
+ stats: ManuscriptStats | null;
438
+ journal?: string;
439
+ url?: string;
440
+ }
441
+
442
+ /**
443
+ * Validate manuscript against journal requirements
444
+ */
445
+ export function validateManuscript(text: string, journalId: string): ManuscriptValidationResult {
446
+ const profile = getJournalProfile(journalId);
447
+
448
+ if (!profile) {
449
+ return {
450
+ valid: false,
451
+ errors: [`Unknown journal: ${journalId}`],
452
+ warnings: [],
453
+ stats: null,
454
+ };
455
+ }
456
+
457
+ const req = profile.requirements;
458
+ const errors: string[] = [];
459
+ const warnings: string[] = [];
460
+
461
+ // Extract content
462
+ const abstract = extractAbstract(text);
463
+ const title = extractTitle(text);
464
+ const sections = extractSections(text);
465
+ const mainWordCount = countWords(text);
466
+ const figureCount = countFigures(text);
467
+ const tableCount = countTables(text);
468
+ const refCount = countReferences(text);
469
+
470
+ const stats: ManuscriptStats = {
471
+ wordCount: mainWordCount,
472
+ abstractWords: abstract ? countWords(abstract) : 0,
473
+ titleChars: title ? title.length : 0,
474
+ figures: figureCount,
475
+ tables: tableCount,
476
+ references: refCount,
477
+ sections: sections.length,
478
+ };
479
+
480
+ // Word limits
481
+ if (req.wordLimit) {
482
+ if (req.wordLimit.main && mainWordCount > req.wordLimit.main) {
483
+ errors.push(`Main text exceeds ${req.wordLimit.main} words (current: ${mainWordCount})`);
484
+ }
485
+ if (req.wordLimit.abstract && abstract) {
486
+ const absWords = countWords(abstract);
487
+ if (absWords > req.wordLimit.abstract) {
488
+ errors.push(`Abstract exceeds ${req.wordLimit.abstract} words (current: ${absWords})`);
489
+ }
490
+ }
491
+ }
492
+
493
+ // References
494
+ if (req.references) {
495
+ if (req.references.max && refCount > req.references.max) {
496
+ errors.push(`References exceed ${req.references.max} (current: ${refCount})`);
497
+ }
498
+ if (req.references.doiRequired) {
499
+ warnings.push('DOI required for all references - run "rev doi check" to verify');
500
+ }
501
+ }
502
+
503
+ // Figures/tables
504
+ if (req.figures) {
505
+ if (req.figures.max && figureCount > req.figures.max) {
506
+ errors.push(`Figures exceed ${req.figures.max} (current: ${figureCount})`);
507
+ }
508
+ }
509
+
510
+ // Required sections
511
+ if (req.sections) {
512
+ for (const reqSection of req.sections) {
513
+ const found = sections.some(s =>
514
+ s.toLowerCase().includes(reqSection.toLowerCase())
515
+ );
516
+ if (!found) {
517
+ warnings.push(`Missing required section: ${reqSection}`);
518
+ }
519
+ }
520
+ }
521
+
522
+ return {
523
+ valid: errors.length === 0,
524
+ errors,
525
+ warnings,
526
+ stats,
527
+ journal: profile.name,
528
+ url: profile.url,
529
+ };
530
+ }
531
+
532
+ /**
533
+ * Validate multiple files against journal requirements
534
+ */
535
+ export function validateProject(files: string[], journalId: string): ManuscriptValidationResult {
536
+ // Combine all file contents
537
+ const combined = files
538
+ .filter(f => fs.existsSync(f))
539
+ .map(f => fs.readFileSync(f, 'utf-8'))
540
+ .join('\n\n');
541
+
542
+ return validateManuscript(combined, journalId);
543
+ }