docrev 0.9.6 → 0.9.11

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 (80) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dev_notes/bug_repro_comment_parser.md +71 -0
  3. package/dev_notes/stress2/adversarial.docx +0 -0
  4. package/dev_notes/stress2/build_adversarial.ts +186 -0
  5. package/dev_notes/stress2/drift_matcher.ts +62 -0
  6. package/dev_notes/stress2/probe_anchors.ts +35 -0
  7. package/dev_notes/stress2/project/adversarial.docx +0 -0
  8. package/dev_notes/stress2/project/discussion.before.md +3 -0
  9. package/dev_notes/stress2/project/discussion.md +3 -0
  10. package/dev_notes/stress2/project/methods.before.md +20 -0
  11. package/dev_notes/stress2/project/methods.md +20 -0
  12. package/dev_notes/stress2/project/rev.yaml +5 -0
  13. package/dev_notes/stress2/project/sections.yaml +4 -0
  14. package/dev_notes/stress2/sections.yaml +5 -0
  15. package/dev_notes/stress2/trace_placement.ts +50 -0
  16. package/dev_notes/stresstest_boundaries.ts +27 -0
  17. package/dev_notes/stresstest_drift_apply.ts +43 -0
  18. package/dev_notes/stresstest_drift_compare.ts +43 -0
  19. package/dev_notes/stresstest_drift_v2.ts +54 -0
  20. package/dev_notes/stresstest_inspect.ts +54 -0
  21. package/dev_notes/stresstest_pstyle.ts +55 -0
  22. package/dev_notes/stresstest_section_debug.ts +23 -0
  23. package/dev_notes/stresstest_split.ts +70 -0
  24. package/dev_notes/stresstest_trace.ts +19 -0
  25. package/dev_notes/stresstest_verify_no_overwrite.ts +40 -0
  26. package/dist/lib/anchor-match.d.ts +51 -0
  27. package/dist/lib/anchor-match.d.ts.map +1 -0
  28. package/dist/lib/anchor-match.js +227 -0
  29. package/dist/lib/anchor-match.js.map +1 -0
  30. package/dist/lib/annotations.d.ts.map +1 -1
  31. package/dist/lib/annotations.js +24 -11
  32. package/dist/lib/annotations.js.map +1 -1
  33. package/dist/lib/commands/index.d.ts +2 -1
  34. package/dist/lib/commands/index.d.ts.map +1 -1
  35. package/dist/lib/commands/index.js +3 -1
  36. package/dist/lib/commands/index.js.map +1 -1
  37. package/dist/lib/commands/quality.js +1 -1
  38. package/dist/lib/commands/quality.js.map +1 -1
  39. package/dist/lib/commands/section-boundaries.d.ts +22 -0
  40. package/dist/lib/commands/section-boundaries.d.ts.map +1 -0
  41. package/dist/lib/commands/section-boundaries.js +63 -0
  42. package/dist/lib/commands/section-boundaries.js.map +1 -0
  43. package/dist/lib/commands/sync.d.ts.map +1 -1
  44. package/dist/lib/commands/sync.js +141 -0
  45. package/dist/lib/commands/sync.js.map +1 -1
  46. package/dist/lib/commands/verify-anchors.d.ts +17 -0
  47. package/dist/lib/commands/verify-anchors.d.ts.map +1 -0
  48. package/dist/lib/commands/verify-anchors.js +226 -0
  49. package/dist/lib/commands/verify-anchors.js.map +1 -0
  50. package/dist/lib/comment-realign.js +2 -2
  51. package/dist/lib/comment-realign.js.map +1 -1
  52. package/dist/lib/import.d.ts +26 -8
  53. package/dist/lib/import.d.ts.map +1 -1
  54. package/dist/lib/import.js +166 -187
  55. package/dist/lib/import.js.map +1 -1
  56. package/dist/lib/response.js +1 -1
  57. package/dist/lib/response.js.map +1 -1
  58. package/dist/lib/word-extraction.d.ts +23 -0
  59. package/dist/lib/word-extraction.d.ts.map +1 -1
  60. package/dist/lib/word-extraction.js +79 -0
  61. package/dist/lib/word-extraction.js.map +1 -1
  62. package/dist/lib/wordcomments.d.ts.map +1 -1
  63. package/dist/lib/wordcomments.js +165 -73
  64. package/dist/lib/wordcomments.js.map +1 -1
  65. package/lib/anchor-match.ts +276 -0
  66. package/lib/annotations.ts +25 -11
  67. package/lib/commands/index.ts +3 -0
  68. package/lib/commands/quality.ts +1 -1
  69. package/lib/commands/section-boundaries.ts +82 -0
  70. package/lib/commands/sync.ts +170 -0
  71. package/lib/commands/verify-anchors.ts +272 -0
  72. package/lib/comment-realign.ts +2 -2
  73. package/lib/import.ts +197 -209
  74. package/lib/response.ts +1 -1
  75. package/lib/word-extraction.ts +93 -0
  76. package/lib/wordcomments.ts +180 -82
  77. package/package.json +1 -1
  78. package/skill/REFERENCE.md +29 -2
  79. package/skill/SKILL.md +12 -2
  80. package/dist/package.json +0 -137
@@ -72,8 +72,35 @@ function generateParaId(commentIdx: number, paraNum: number): string {
72
72
  * - comments: array with author, text, isReply, parentIdx
73
73
  */
74
74
  export function prepareMarkdownWithMarkers(markdown: string): PrepareResult {
75
- // Match all comments with optional anchor
76
- const commentPattern = /\{>>(.+?)<<\}(?:\s*\[([^\]]+)\]\{\.mark\})?/g;
75
+ // Match the comment block first; extend manually to capture an optional
76
+ // trailing `[anchor]{.mark}` span. A regex `[^\]]+` for the anchor would
77
+ // bail on the inner `]` of nested syntax (e.g. `[[0..9]]{.mark}` or
78
+ // `[*phrase*]{.mark}` after pandoc-rewriting), so we walk the brackets
79
+ // ourselves and verify a `{.mark}` suffix.
80
+ const commentPattern = /\{>>([\s\S]+?)<<\}/g;
81
+
82
+ function tryParseTrailingAnchor(
83
+ text: string,
84
+ fromIdx: number,
85
+ ): { anchor: string; endIdx: number } | null {
86
+ let i = fromIdx;
87
+ while (i < text.length && /\s/.test(text[i] ?? '')) i++;
88
+ if (text[i] !== '[') return null;
89
+ let depth = 1;
90
+ let j = i + 1;
91
+ while (j < text.length) {
92
+ const ch = text[j];
93
+ if (ch === '[') depth++;
94
+ else if (ch === ']') {
95
+ depth--;
96
+ if (depth === 0) break;
97
+ }
98
+ j++;
99
+ }
100
+ if (depth !== 0) return null;
101
+ if (text.slice(j + 1, j + 8) !== '{.mark}') return null;
102
+ return { anchor: text.slice(i + 1, j), endIdx: j + 8 };
103
+ }
77
104
 
78
105
  const rawMatches: ParsedComment[] = [];
79
106
  let match: RegExpExecArray | null;
@@ -87,14 +114,25 @@ export function prepareMarkdownWithMarkers(markdown: string): PrepareResult {
87
114
  text = content.slice(colonIdx + 1).trim();
88
115
  }
89
116
 
117
+ const commentEnd = match.index + match[0].length;
118
+ const trailing = tryParseTrailingAnchor(markdown, commentEnd);
119
+
90
120
  rawMatches.push({
91
121
  author,
92
122
  text,
93
- anchor: match[2] || null,
123
+ anchor: trailing ? trailing.anchor : null,
94
124
  start: match.index,
95
- end: match.index + match[0].length,
96
- fullMatch: match[0]
125
+ end: trailing ? trailing.endIdx : commentEnd,
126
+ fullMatch: markdown.slice(match.index, trailing ? trailing.endIdx : commentEnd),
97
127
  });
128
+
129
+ // Advance regex lastIndex past the consumed anchor so the next iteration
130
+ // doesn't re-scan inside it (e.g. `[*emphasis*]{.mark}` would otherwise
131
+ // tempt the matcher to look for another `{>>...<<}` in the body of the
132
+ // anchor span).
133
+ if (trailing) {
134
+ commentPattern.lastIndex = trailing.endIdx;
135
+ }
98
136
  }
99
137
 
100
138
  if (rawMatches.length === 0) {
@@ -179,10 +217,11 @@ export function prepareMarkdownWithMarkers(markdown: string): PrepareResult {
179
217
 
180
218
  if (c.isReply) {
181
219
  // Reply: remove from document entirely (will be in comments.xml only)
182
- // Also consume leading whitespace to avoid double spaces
220
+ // Also consume one preceding whitespace char to avoid double spaces.
221
+ // We deliberately consume at most one — walking arbitrarily backwards
222
+ // would shift positions that lower-index comments still depend on.
183
223
  let removeStart = c.start;
184
- const charBefore = markedMarkdown[removeStart - 1];
185
- while (removeStart > 0 && charBefore && /\s/.test(charBefore)) {
224
+ if (removeStart > 0 && /\s/.test(markedMarkdown[removeStart - 1] ?? '')) {
186
225
  removeStart--;
187
226
  }
188
227
 
@@ -205,10 +244,10 @@ export function prepareMarkdownWithMarkers(markdown: string): PrepareResult {
205
244
  } else {
206
245
  // Parent comment
207
246
  if (c.anchorFromReply) {
208
- // Anchor markers are placed by the reply, just remove this comment
247
+ // Anchor markers are placed by the reply, just remove this comment.
248
+ // Consume one preceding whitespace char only (see reply branch above).
209
249
  let removeStart = c.start;
210
- const charBefore = markedMarkdown[removeStart - 1];
211
- while (removeStart > 0 && charBefore && /\s/.test(charBefore)) {
250
+ if (removeStart > 0 && /\s/.test(markedMarkdown[removeStart - 1] ?? '')) {
212
251
  removeStart--;
213
252
  }
214
253
  markedMarkdown = markedMarkdown.slice(0, removeStart) + markedMarkdown.slice(c.end);
@@ -421,93 +460,152 @@ export async function injectCommentsAtMarkers(
421
460
  const endMarker = `${MARKER_END_PREFIX}${idx}${MARKER_SUFFIX}`;
422
461
 
423
462
  const startPos = documentXml.indexOf(startMarker);
424
- const endPos = documentXml.indexOf(endMarker);
463
+ const endPos = documentXml.indexOf(endMarker, startPos + startMarker.length);
425
464
 
426
465
  if (startPos === -1 || endPos === -1) continue;
427
466
 
428
- // Find the <w:r> containing the markers
429
- const rStartBefore = documentXml.lastIndexOf('<w:r>', startPos);
430
- const rStartOpen = documentXml.lastIndexOf('<w:r ', startPos);
431
- const rStart = Math.max(rStartBefore, rStartOpen);
432
- const rEndPos = documentXml.indexOf('</w:r>', endPos);
433
-
434
- if (rStart === -1 || rEndPos === -1) continue;
435
-
436
- const rEnd = rEndPos + '</w:r>'.length;
437
- const runContent = documentXml.slice(rStart, rEnd);
438
-
439
- // Extract styling
440
- const rPrMatch = runContent.match(/<w:rPr>[\s\S]*?<\/w:rPr>/);
441
- const rPr = rPrMatch ? rPrMatch[0] : '';
442
-
443
- // Extract text
444
- const textMatch = runContent.match(/<w:t[^>]*>([\s\S]*?)<\/w:t>/);
445
- if (!textMatch) continue;
446
-
447
- const fullText = textMatch[1] ?? '';
448
- const tElementMatch = textMatch[0].match(/<w:t[^>]*>/);
449
- if (!tElementMatch) continue;
450
- const tElement = tElementMatch[0];
451
-
452
- const startInText = fullText.indexOf(startMarker);
453
- const endInText = fullText.indexOf(endMarker);
454
- if (startInText === -1 || endInText === -1) continue;
467
+ // Find the runs containing each marker. Pandoc may split a single
468
+ // markdown anchor across multiple <w:r> blocks when it applies styling
469
+ // mid-anchor (smart-quote substitution, *italic*, `code`, **bold**).
470
+ // The same-run path (current happy path) collapses into the multi-run
471
+ // path when start and end runs coincide.
472
+ const startRunOpen = Math.max(
473
+ documentXml.lastIndexOf('<w:r>', startPos),
474
+ documentXml.lastIndexOf('<w:r ', startPos),
475
+ );
476
+ const startRunCloseIdx = documentXml.indexOf('</w:r>', startPos);
477
+ const endRunOpen = Math.max(
478
+ documentXml.lastIndexOf('<w:r>', endPos),
479
+ documentXml.lastIndexOf('<w:r ', endPos),
480
+ );
481
+ const endRunCloseIdx = documentXml.indexOf('</w:r>', endPos);
482
+
483
+ if (
484
+ startRunOpen === -1 || startRunCloseIdx === -1 ||
485
+ endRunOpen === -1 || endRunCloseIdx === -1
486
+ ) continue;
487
+
488
+ const startRunClose = startRunCloseIdx + '</w:r>'.length;
489
+ const endRunClose = endRunCloseIdx + '</w:r>'.length;
490
+
491
+ const startRunFull = documentXml.slice(startRunOpen, startRunClose);
492
+ const endRunFull = documentXml.slice(endRunOpen, endRunClose);
493
+
494
+ // Extract <w:rPr> and <w:t> element shape from each run. Both pieces
495
+ // are needed verbatim so a textBefore split keeps its original styling
496
+ // and so the post-anchor textAfter render keeps the end run's styling.
497
+ function dissectRun(runXml: string, marker: string): {
498
+ rPr: string;
499
+ tElement: string;
500
+ textBefore: string;
501
+ textAfter: string;
502
+ } | null {
503
+ const rPrMatch = runXml.match(/<w:rPr>[\s\S]*?<\/w:rPr>/);
504
+ const tMatch = runXml.match(/<w:t[^>]*>([\s\S]*?)<\/w:t>/);
505
+ if (!tMatch) return null;
506
+ const tOpenMatch = tMatch[0].match(/<w:t[^>]*>/);
507
+ if (!tOpenMatch) return null;
508
+ const tContent = tMatch[1] ?? '';
509
+ const markerInT = tContent.indexOf(marker);
510
+ if (markerInT === -1) return null;
511
+ return {
512
+ rPr: rPrMatch ? rPrMatch[0] : '',
513
+ tElement: tOpenMatch[0],
514
+ textBefore: tContent.slice(0, markerInT),
515
+ textAfter: tContent.slice(markerInT + marker.length),
516
+ };
517
+ }
455
518
 
456
- let textBefore = fullText.slice(0, startInText);
457
- let anchorText = fullText.slice(startInText + startMarker.length, endInText);
458
- let textAfter = fullText.slice(endInText + endMarker.length);
519
+ let replacement = '';
520
+ const replies = commentsWithIds.filter(c => c.isReply && c.parentIdx === comment?.commentIdx);
459
521
 
460
- // When anchor is empty, use the first word from textAfter as fallback
461
- if (!anchorText && textAfter) {
462
- const wordMatch = textAfter.match(/^\s*(\S+)/);
463
- if (wordMatch) {
464
- anchorText = wordMatch[1] ?? '';
465
- textAfter = textAfter.slice(wordMatch[0].length);
522
+ const emitRangeStarts = () => {
523
+ replacement += `<w:commentRangeStart w:id="${comment.id}"/>`;
524
+ for (const reply of replies) {
525
+ replacement += `<w:commentRangeStart w:id="${reply.id}"/>`;
466
526
  }
467
- }
527
+ };
528
+
529
+ const emitRangeEnds = () => {
530
+ replacement += `<w:commentRangeEnd w:id="${comment.id}"/>`;
531
+ replacement += `<w:r><w:commentReference w:id="${comment.id}"/></w:r>`;
532
+ for (const reply of replies) {
533
+ replacement += `<w:commentRangeEnd w:id="${reply.id}"/>`;
534
+ replacement += `<w:r><w:commentReference w:id="${reply.id}"/></w:r>`;
535
+ injectedIds.add(reply.id);
536
+ }
537
+ };
538
+
539
+ if (startRunOpen === endRunOpen) {
540
+ // Same-run path: both markers live inside one <w:t>. Original logic.
541
+ const startInfo = dissectRun(startRunFull, startMarker);
542
+ if (!startInfo) continue;
543
+ const fullText = startInfo.textBefore + startMarker + startInfo.textAfter;
544
+ const endInTextRel = startInfo.textAfter.indexOf(endMarker);
545
+ if (endInTextRel === -1) continue;
546
+ const anchorTextSame = startInfo.textAfter.slice(0, endInTextRel);
547
+ let textAfter = startInfo.textAfter.slice(endInTextRel + endMarker.length);
548
+ let anchorText = anchorTextSame;
549
+ let textBefore = startInfo.textBefore;
550
+
551
+ // Empty anchor: borrow the next word so the comment has something
552
+ // to anchor on. Then normalize the trailing double space.
553
+ if (!anchorText && textAfter) {
554
+ const wordMatch = textAfter.match(/^\s*(\S+)/);
555
+ if (wordMatch) {
556
+ anchorText = wordMatch[1] ?? '';
557
+ textAfter = textAfter.slice(wordMatch[0].length);
558
+ }
559
+ }
560
+ if (!anchorText && textBefore.endsWith(' ') && textAfter.startsWith(' ')) {
561
+ textAfter = textAfter.slice(1);
562
+ }
563
+ // Suppress unused warning for pre-empty-anchor fullText var
564
+ void fullText;
468
565
 
469
- // When anchor is still empty, normalize double spaces to single space
470
- if (!anchorText && textBefore.endsWith(' ') && textAfter.startsWith(' ')) {
471
- textAfter = textAfter.slice(1); // Remove leading space from textAfter
566
+ if (textBefore) {
567
+ replacement += `<w:r>${startInfo.rPr}${startInfo.tElement}${textBefore}</w:t></w:r>`;
568
+ }
569
+ emitRangeStarts();
570
+ if (anchorText) {
571
+ replacement += `<w:r>${startInfo.rPr}${startInfo.tElement}${anchorText}</w:t></w:r>`;
572
+ }
573
+ emitRangeEnds();
574
+ if (textAfter) {
575
+ replacement += `<w:r>${startInfo.rPr}${startInfo.tElement}${textAfter}</w:t></w:r>`;
576
+ }
577
+ documentXml = documentXml.slice(0, startRunOpen) + replacement + documentXml.slice(startRunClose);
578
+ injectedIds.add(comment.id);
579
+ continue;
472
580
  }
473
581
 
474
- // Build replacement
475
- let replacement = '';
582
+ // Multi-run path: markers sit in different <w:r> blocks because pandoc
583
+ // applied mid-anchor styling. Split the start run at the start marker,
584
+ // keep all middle runs verbatim (they carry the styled anchor portions),
585
+ // split the end run at the end marker.
586
+ const startInfo = dissectRun(startRunFull, startMarker);
587
+ const endInfo = dissectRun(endRunFull, endMarker);
588
+ if (!startInfo || !endInfo) continue;
476
589
 
477
- if (textBefore) {
478
- replacement += `<w:r>${rPr}${tElement}${textBefore}</w:t></w:r>`;
479
- }
480
-
481
- // Find replies to this comment
482
- const replies = commentsWithIds.filter(c => c.isReply && c.parentIdx === comment?.commentIdx);
590
+ const middle = documentXml.slice(startRunClose, endRunOpen);
483
591
 
484
- // Start ranges for parent AND all replies (nested)
485
- replacement += `<w:commentRangeStart w:id="${comment.id}"/>`;
486
- for (const reply of replies) {
487
- replacement += `<w:commentRangeStart w:id="${reply.id}"/>`;
592
+ if (startInfo.textBefore) {
593
+ replacement += `<w:r>${startInfo.rPr}${startInfo.tElement}${startInfo.textBefore}</w:t></w:r>`;
488
594
  }
489
-
490
- // Anchor text
491
- if (anchorText) {
492
- replacement += `<w:r>${rPr}${tElement}${anchorText}</w:t></w:r>`;
595
+ emitRangeStarts();
596
+ if (startInfo.textAfter) {
597
+ replacement += `<w:r>${startInfo.rPr}${startInfo.tElement}${startInfo.textAfter}</w:t></w:r>`;
493
598
  }
494
-
495
- // End parent range and reference (NO rStyle wrapper - required for threading)
496
- replacement += `<w:commentRangeEnd w:id="${comment.id}"/>`;
497
- replacement += `<w:r><w:commentReference w:id="${comment.id}"/></w:r>`;
498
-
499
- // End reply ranges and references (same position as parent, NO rStyle wrapper)
500
- for (const reply of replies) {
501
- replacement += `<w:commentRangeEnd w:id="${reply.id}"/>`;
502
- replacement += `<w:r><w:commentReference w:id="${reply.id}"/></w:r>`;
503
- injectedIds.add(reply.id);
599
+ replacement += middle;
600
+ if (endInfo.textBefore) {
601
+ replacement += `<w:r>${endInfo.rPr}${endInfo.tElement}${endInfo.textBefore}</w:t></w:r>`;
504
602
  }
505
-
506
- if (textAfter) {
507
- replacement += `<w:r>${rPr}${tElement}${textAfter}</w:t></w:r>`;
603
+ emitRangeEnds();
604
+ if (endInfo.textAfter) {
605
+ replacement += `<w:r>${endInfo.rPr}${endInfo.tElement}${endInfo.textAfter}</w:t></w:r>`;
508
606
  }
509
607
 
510
- documentXml = documentXml.slice(0, rStart) + replacement + documentXml.slice(rEnd);
608
+ documentXml = documentXml.slice(0, startRunOpen) + replacement + documentXml.slice(endRunClose);
511
609
  injectedIds.add(comment.id);
512
610
  }
513
611
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docrev",
3
- "version": "0.9.6",
3
+ "version": "0.9.11",
4
4
  "description": "Academic paper revision workflow: Word ↔ Markdown round-trips, DOI validation, reviewer comments",
5
5
  "type": "module",
6
6
  "types": "dist/lib/types.d.ts",
@@ -21,11 +21,38 @@ rev import manuscript.docx --output ./project
21
21
  ### rev sync
22
22
  Sync feedback from a reviewed Word document into existing markdown sections.
23
23
  ```bash
24
- rev sync reviewed.docx # Updates markdown with track changes/comments
25
- rev sync # Auto-detect most recent .docx
24
+ rev sync reviewed.docx # Updates markdown with track changes/comments
25
+ rev sync # Auto-detect most recent .docx
26
26
  rev sync reviewed.docx methods # Sync only methods section
27
+ rev sync reviewed.docx --comments-only # Insert comments only; never modify prose
27
28
  ```
28
29
 
30
+ `--comments-only` skips the Word→Markdown diff entirely. Use it when the
31
+ markdown has been revised between sending the docx out for review and
32
+ receiving it back: applying track changes from a stale draft would clobber
33
+ newer edits, but comments still need to land. Comments are placed at
34
+ fuzzy-matched anchors against the current prose. Pair with
35
+ `rev verify-anchors` to see which ones won't fit before you run sync.
36
+
37
+ ### rev verify-anchors
38
+ Report drift between Word comment anchors and the current markdown.
39
+ ```bash
40
+ rev verify-anchors reviewed.docx # Print per-comment match quality
41
+ rev verify-anchors reviewed.docx --json # Machine-readable report
42
+ ```
43
+
44
+ Each comment is classified by how well its anchor still matches the current
45
+ section prose:
46
+
47
+ - `clean` – exact or whitespace-normalized hit
48
+ - `drift` – anchor only matches via stripped-CriticMarkup or partial-prefix fallbacks
49
+ - `context-only` – anchor text is gone, only surrounding context survives
50
+ - `ambiguous` – multiple candidate positions; needs context to disambiguate
51
+ - `unmatched` – nothing maps; user must place the comment manually
52
+
53
+ Useful before `rev sync --comments-only` to plan which comments will land
54
+ automatically and which need manual placement.
55
+
29
56
  ### rev build
30
57
  Build output documents from markdown sections.
31
58
  ```bash
package/skill/SKILL.md CHANGED
@@ -44,10 +44,18 @@ Send to reviewers. They add comments and track changes in Word.
44
44
  ### 3. Import feedback
45
45
 
46
46
  ```bash
47
- rev sync reviewed.docx # Updates markdown with annotations
48
- rev sync # Auto-detect most recent .docx
47
+ rev sync reviewed.docx # Updates markdown with annotations
48
+ rev sync # Auto-detect most recent .docx
49
+ rev sync reviewed.docx --comments-only # Insert comments only; never modify prose
50
+ rev verify-anchors reviewed.docx # Report which anchors still match current prose
49
51
  ```
50
52
 
53
+ Use `--comments-only` when the markdown has been revised between sending the
54
+ docx out and receiving it back. Without the flag, track changes from a stale
55
+ draft would clobber newer edits. With it, only comments are imported, placed
56
+ at fuzzy-matched anchors against the current prose. Run `rev verify-anchors`
57
+ first to see which comments will land cleanly and which need manual placement.
58
+
51
59
  ### 4. View and address comments
52
60
 
53
61
  ```bash
@@ -111,6 +119,8 @@ rev response # Generate point-by-point response letter
111
119
  | Create LaTeX project | `rev new my-project --template latex` |
112
120
  | Import Word doc | `rev import manuscript.docx` |
113
121
  | Sync Word feedback | `rev sync reviewed.docx` |
122
+ | Sync comments only (prose unchanged) | `rev sync reviewed.docx --comments-only` |
123
+ | Verify anchors against current prose | `rev verify-anchors reviewed.docx` |
114
124
  | Sync PDF comments | `rev sync annotated.pdf` |
115
125
  | Extract PDF comments | `rev pdf-comments annotated.pdf` |
116
126
  | Extract with highlighted text | `rev pdf-comments file.pdf --with-text` |
package/dist/package.json DELETED
@@ -1,137 +0,0 @@
1
- {
2
- "name": "docrev",
3
- "version": "0.9.4",
4
- "description": "Academic paper revision workflow: Word ↔ Markdown round-trips, DOI validation, reviewer comments",
5
- "type": "module",
6
- "types": "dist/lib/types.d.ts",
7
- "exports": {
8
- ".": {
9
- "types": "./dist/lib/annotations.d.ts",
10
- "import": "./dist/lib/annotations.js"
11
- },
12
- "./annotations": {
13
- "types": "./dist/lib/annotations.d.ts",
14
- "import": "./dist/lib/annotations.js"
15
- },
16
- "./build": {
17
- "types": "./dist/lib/build.d.ts",
18
- "import": "./dist/lib/build.js"
19
- },
20
- "./citations": {
21
- "types": "./dist/lib/citations.d.ts",
22
- "import": "./dist/lib/citations.js"
23
- },
24
- "./crossref": {
25
- "types": "./dist/lib/crossref.d.ts",
26
- "import": "./dist/lib/crossref.js"
27
- },
28
- "./doi": {
29
- "types": "./dist/lib/doi.d.ts",
30
- "import": "./dist/lib/doi.js"
31
- },
32
- "./equations": {
33
- "types": "./dist/lib/equations.d.ts",
34
- "import": "./dist/lib/equations.js"
35
- },
36
- "./git": {
37
- "types": "./dist/lib/git.d.ts",
38
- "import": "./dist/lib/git.js"
39
- },
40
- "./journals": {
41
- "types": "./dist/lib/journals.d.ts",
42
- "import": "./dist/lib/journals.js"
43
- },
44
- "./merge": {
45
- "types": "./dist/lib/merge.d.ts",
46
- "import": "./dist/lib/merge.js"
47
- },
48
- "./sections": {
49
- "types": "./dist/lib/sections.d.ts",
50
- "import": "./dist/lib/sections.js"
51
- },
52
- "./word": {
53
- "types": "./dist/lib/word.d.ts",
54
- "import": "./dist/lib/word.js"
55
- },
56
- "./variables": {
57
- "types": "./dist/lib/variables.d.ts",
58
- "import": "./dist/lib/variables.js"
59
- },
60
- "./grammar": {
61
- "types": "./dist/lib/grammar.d.ts",
62
- "import": "./dist/lib/grammar.js"
63
- },
64
- "./trackchanges": {
65
- "types": "./dist/lib/trackchanges.d.ts",
66
- "import": "./dist/lib/trackchanges.js"
67
- },
68
- "./spelling": {
69
- "types": "./dist/lib/spelling.d.ts",
70
- "import": "./dist/lib/spelling.js"
71
- },
72
- "./wordcomments": {
73
- "types": "./dist/lib/wordcomments.d.ts",
74
- "import": "./dist/lib/wordcomments.js"
75
- }
76
- },
77
- "engines": {
78
- "node": ">=18.0.0"
79
- },
80
- "bin": {
81
- "rev": "bin/rev.js"
82
- },
83
- "scripts": {
84
- "build": "tsc && node scripts/postbuild.js",
85
- "build:watch": "tsc --watch",
86
- "dev": "tsx bin/rev.ts",
87
- "test": "tsx --test test/*.test.js",
88
- "test:ts": "tsx --test test/*.test.ts",
89
- "test:watch": "node --test --watch test/*.test.js",
90
- "test:coverage": "c8 --reporter=text --reporter=lcov node --test test/*.test.js",
91
- "typecheck": "tsc --noEmit",
92
- "prepublishOnly": "npm run build"
93
- },
94
- "repository": {
95
- "type": "git",
96
- "url": "git+https://github.com/gcol33/docrev.git"
97
- },
98
- "bugs": {
99
- "url": "https://github.com/gcol33/docrev/issues"
100
- },
101
- "homepage": "https://github.com/gcol33/docrev#readme",
102
- "keywords": [
103
- "markdown",
104
- "word",
105
- "docx",
106
- "track-changes",
107
- "comments",
108
- "academic",
109
- "writing",
110
- "pandoc",
111
- "criticmarkup"
112
- ],
113
- "author": "Gilles Colling",
114
- "license": "MIT",
115
- "dependencies": {
116
- "adm-zip": "^0.5.16",
117
- "chalk": "^5.3.0",
118
- "commander": "^12.0.0",
119
- "dictionary-en": "^4.0.0",
120
- "dictionary-en-gb": "^3.0.0",
121
- "diff": "^8.0.2",
122
- "mathml-to-latex": "^1.5.0",
123
- "nspell": "^2.1.5",
124
- "pdf-lib": "^1.17.1",
125
- "pdfjs-dist": "^5.4.530",
126
- "tsx": "^4.21.0",
127
- "xml2js": "^0.6.2",
128
- "yaml": "^2.8.2"
129
- },
130
- "devDependencies": {
131
- "@types/adm-zip": "^0.5.7",
132
- "@types/node": "^25.2.0",
133
- "@types/xml2js": "^0.4.14",
134
- "c8": "^10.1.2",
135
- "typescript": "^5.9.3"
136
- }
137
- }