docrev 0.8.1 → 0.8.5

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 (306) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/PLAN-tables-and-postprocess.md +850 -0
  3. package/README.md +33 -0
  4. package/bin/rev.js +12 -131
  5. package/bin/rev.ts +145 -0
  6. package/dist/bin/rev.d.ts +9 -0
  7. package/dist/bin/rev.d.ts.map +1 -0
  8. package/dist/bin/rev.js +118 -0
  9. package/dist/bin/rev.js.map +1 -0
  10. package/dist/lib/annotations.d.ts +91 -0
  11. package/dist/lib/annotations.d.ts.map +1 -0
  12. package/dist/lib/annotations.js +554 -0
  13. package/dist/lib/annotations.js.map +1 -0
  14. package/dist/lib/build.d.ts +171 -0
  15. package/dist/lib/build.d.ts.map +1 -0
  16. package/dist/lib/build.js +755 -0
  17. package/dist/lib/build.js.map +1 -0
  18. package/dist/lib/citations.d.ts +34 -0
  19. package/dist/lib/citations.d.ts.map +1 -0
  20. package/dist/lib/citations.js +140 -0
  21. package/dist/lib/citations.js.map +1 -0
  22. package/dist/lib/commands/build.d.ts +13 -0
  23. package/dist/lib/commands/build.d.ts.map +1 -0
  24. package/dist/lib/commands/build.js +678 -0
  25. package/dist/lib/commands/build.js.map +1 -0
  26. package/dist/lib/commands/citations.d.ts +11 -0
  27. package/dist/lib/commands/citations.d.ts.map +1 -0
  28. package/dist/lib/commands/citations.js +428 -0
  29. package/dist/lib/commands/citations.js.map +1 -0
  30. package/dist/lib/commands/comments.d.ts +11 -0
  31. package/dist/lib/commands/comments.d.ts.map +1 -0
  32. package/dist/lib/commands/comments.js +883 -0
  33. package/dist/lib/commands/comments.js.map +1 -0
  34. package/dist/lib/commands/context.d.ts +35 -0
  35. package/dist/lib/commands/context.d.ts.map +1 -0
  36. package/dist/lib/commands/context.js +59 -0
  37. package/dist/lib/commands/context.js.map +1 -0
  38. package/dist/lib/commands/core.d.ts +11 -0
  39. package/dist/lib/commands/core.d.ts.map +1 -0
  40. package/dist/lib/commands/core.js +246 -0
  41. package/dist/lib/commands/core.js.map +1 -0
  42. package/dist/lib/commands/doi.d.ts +11 -0
  43. package/dist/lib/commands/doi.d.ts.map +1 -0
  44. package/dist/lib/commands/doi.js +373 -0
  45. package/dist/lib/commands/doi.js.map +1 -0
  46. package/dist/lib/commands/history.d.ts +11 -0
  47. package/dist/lib/commands/history.d.ts.map +1 -0
  48. package/dist/lib/commands/history.js +245 -0
  49. package/dist/lib/commands/history.js.map +1 -0
  50. package/dist/lib/commands/index.d.ts +28 -0
  51. package/dist/lib/commands/index.d.ts.map +1 -0
  52. package/dist/lib/commands/index.js +35 -0
  53. package/dist/lib/commands/index.js.map +1 -0
  54. package/dist/lib/commands/init.d.ts +11 -0
  55. package/dist/lib/commands/init.d.ts.map +1 -0
  56. package/dist/lib/commands/init.js +209 -0
  57. package/dist/lib/commands/init.js.map +1 -0
  58. package/dist/lib/commands/response.d.ts +11 -0
  59. package/dist/lib/commands/response.d.ts.map +1 -0
  60. package/dist/lib/commands/response.js +317 -0
  61. package/dist/lib/commands/response.js.map +1 -0
  62. package/dist/lib/commands/sections.d.ts +11 -0
  63. package/dist/lib/commands/sections.d.ts.map +1 -0
  64. package/dist/lib/commands/sections.js +1071 -0
  65. package/dist/lib/commands/sections.js.map +1 -0
  66. package/dist/lib/commands/utilities.d.ts +19 -0
  67. package/dist/lib/commands/utilities.d.ts.map +1 -0
  68. package/dist/lib/commands/utilities.js +2009 -0
  69. package/dist/lib/commands/utilities.js.map +1 -0
  70. package/dist/lib/comment-realign.d.ts +50 -0
  71. package/dist/lib/comment-realign.d.ts.map +1 -0
  72. package/dist/lib/comment-realign.js +372 -0
  73. package/dist/lib/comment-realign.js.map +1 -0
  74. package/dist/lib/config.d.ts +41 -0
  75. package/dist/lib/config.d.ts.map +1 -0
  76. package/dist/lib/config.js +76 -0
  77. package/dist/lib/config.js.map +1 -0
  78. package/dist/lib/crossref.d.ts +108 -0
  79. package/dist/lib/crossref.d.ts.map +1 -0
  80. package/dist/lib/crossref.js +597 -0
  81. package/dist/lib/crossref.js.map +1 -0
  82. package/dist/lib/dependencies.d.ts +30 -0
  83. package/dist/lib/dependencies.d.ts.map +1 -0
  84. package/dist/lib/dependencies.js +95 -0
  85. package/dist/lib/dependencies.js.map +1 -0
  86. package/dist/lib/doi-cache.d.ts +29 -0
  87. package/dist/lib/doi-cache.d.ts.map +1 -0
  88. package/dist/lib/doi-cache.js +104 -0
  89. package/dist/lib/doi-cache.js.map +1 -0
  90. package/dist/lib/doi.d.ts +65 -0
  91. package/dist/lib/doi.d.ts.map +1 -0
  92. package/dist/lib/doi.js +710 -0
  93. package/dist/lib/doi.js.map +1 -0
  94. package/dist/lib/equations.d.ts +61 -0
  95. package/dist/lib/equations.d.ts.map +1 -0
  96. package/dist/lib/equations.js +445 -0
  97. package/dist/lib/equations.js.map +1 -0
  98. package/dist/lib/errors.d.ts +60 -0
  99. package/dist/lib/errors.d.ts.map +1 -0
  100. package/dist/lib/errors.js +303 -0
  101. package/dist/lib/errors.js.map +1 -0
  102. package/dist/lib/format.d.ts +104 -0
  103. package/dist/lib/format.d.ts.map +1 -0
  104. package/dist/lib/format.js +416 -0
  105. package/dist/lib/format.js.map +1 -0
  106. package/dist/lib/git.d.ts +88 -0
  107. package/dist/lib/git.d.ts.map +1 -0
  108. package/dist/lib/git.js +304 -0
  109. package/dist/lib/git.js.map +1 -0
  110. package/dist/lib/grammar.d.ts +62 -0
  111. package/dist/lib/grammar.d.ts.map +1 -0
  112. package/dist/lib/grammar.js +244 -0
  113. package/dist/lib/grammar.js.map +1 -0
  114. package/dist/lib/image-registry.d.ts +68 -0
  115. package/dist/lib/image-registry.d.ts.map +1 -0
  116. package/dist/lib/image-registry.js +112 -0
  117. package/dist/lib/image-registry.js.map +1 -0
  118. package/dist/lib/import.d.ts +184 -0
  119. package/dist/lib/import.d.ts.map +1 -0
  120. package/dist/lib/import.js +1581 -0
  121. package/dist/lib/import.js.map +1 -0
  122. package/dist/lib/journals.d.ts +55 -0
  123. package/dist/lib/journals.d.ts.map +1 -0
  124. package/dist/lib/journals.js +417 -0
  125. package/dist/lib/journals.js.map +1 -0
  126. package/dist/lib/merge.d.ts +138 -0
  127. package/dist/lib/merge.d.ts.map +1 -0
  128. package/dist/lib/merge.js +603 -0
  129. package/dist/lib/merge.js.map +1 -0
  130. package/dist/lib/orcid.d.ts +36 -0
  131. package/dist/lib/orcid.d.ts.map +1 -0
  132. package/dist/lib/orcid.js +117 -0
  133. package/dist/lib/orcid.js.map +1 -0
  134. package/dist/lib/pdf-comments.d.ts +95 -0
  135. package/dist/lib/pdf-comments.d.ts.map +1 -0
  136. package/dist/lib/pdf-comments.js +192 -0
  137. package/dist/lib/pdf-comments.js.map +1 -0
  138. package/dist/lib/pdf-import.d.ts +118 -0
  139. package/dist/lib/pdf-import.d.ts.map +1 -0
  140. package/dist/lib/pdf-import.js +397 -0
  141. package/dist/lib/pdf-import.js.map +1 -0
  142. package/dist/lib/plugins.d.ts +76 -0
  143. package/dist/lib/plugins.d.ts.map +1 -0
  144. package/dist/lib/plugins.js +235 -0
  145. package/dist/lib/plugins.js.map +1 -0
  146. package/dist/lib/postprocess.d.ts +42 -0
  147. package/dist/lib/postprocess.d.ts.map +1 -0
  148. package/dist/lib/postprocess.js +138 -0
  149. package/dist/lib/postprocess.js.map +1 -0
  150. package/dist/lib/pptx-template.d.ts +59 -0
  151. package/dist/lib/pptx-template.d.ts.map +1 -0
  152. package/dist/lib/pptx-template.js +613 -0
  153. package/dist/lib/pptx-template.js.map +1 -0
  154. package/dist/lib/pptx-themes.d.ts +80 -0
  155. package/dist/lib/pptx-themes.d.ts.map +1 -0
  156. package/dist/lib/pptx-themes.js +818 -0
  157. package/dist/lib/pptx-themes.js.map +1 -0
  158. package/dist/lib/protect-restore.d.ts +137 -0
  159. package/dist/lib/protect-restore.d.ts.map +1 -0
  160. package/dist/lib/protect-restore.js +394 -0
  161. package/dist/lib/protect-restore.js.map +1 -0
  162. package/dist/lib/rate-limiter.d.ts +27 -0
  163. package/dist/lib/rate-limiter.d.ts.map +1 -0
  164. package/dist/lib/rate-limiter.js +79 -0
  165. package/dist/lib/rate-limiter.js.map +1 -0
  166. package/dist/lib/response.d.ts +41 -0
  167. package/dist/lib/response.d.ts.map +1 -0
  168. package/dist/lib/response.js +150 -0
  169. package/dist/lib/response.js.map +1 -0
  170. package/dist/lib/review.d.ts +35 -0
  171. package/dist/lib/review.d.ts.map +1 -0
  172. package/dist/lib/review.js +263 -0
  173. package/dist/lib/review.js.map +1 -0
  174. package/dist/lib/schema.d.ts +66 -0
  175. package/dist/lib/schema.d.ts.map +1 -0
  176. package/dist/lib/schema.js +339 -0
  177. package/dist/lib/schema.js.map +1 -0
  178. package/dist/lib/scientific-words.d.ts +6 -0
  179. package/dist/lib/scientific-words.d.ts.map +1 -0
  180. package/dist/lib/scientific-words.js +66 -0
  181. package/dist/lib/scientific-words.js.map +1 -0
  182. package/dist/lib/sections.d.ts +40 -0
  183. package/dist/lib/sections.d.ts.map +1 -0
  184. package/dist/lib/sections.js +288 -0
  185. package/dist/lib/sections.js.map +1 -0
  186. package/dist/lib/slides.d.ts +86 -0
  187. package/dist/lib/slides.d.ts.map +1 -0
  188. package/dist/lib/slides.js +676 -0
  189. package/dist/lib/slides.js.map +1 -0
  190. package/dist/lib/spelling.d.ts +76 -0
  191. package/dist/lib/spelling.d.ts.map +1 -0
  192. package/dist/lib/spelling.js +272 -0
  193. package/dist/lib/spelling.js.map +1 -0
  194. package/dist/lib/templates.d.ts +30 -0
  195. package/dist/lib/templates.d.ts.map +1 -0
  196. package/dist/lib/templates.js +504 -0
  197. package/dist/lib/templates.js.map +1 -0
  198. package/dist/lib/themes.d.ts +85 -0
  199. package/dist/lib/themes.d.ts.map +1 -0
  200. package/dist/lib/themes.js +652 -0
  201. package/dist/lib/themes.js.map +1 -0
  202. package/dist/lib/trackchanges.d.ts +51 -0
  203. package/dist/lib/trackchanges.d.ts.map +1 -0
  204. package/dist/lib/trackchanges.js +202 -0
  205. package/dist/lib/trackchanges.js.map +1 -0
  206. package/dist/lib/tui.d.ts +76 -0
  207. package/dist/lib/tui.d.ts.map +1 -0
  208. package/dist/lib/tui.js +377 -0
  209. package/dist/lib/tui.js.map +1 -0
  210. package/dist/lib/types.d.ts +447 -0
  211. package/dist/lib/types.d.ts.map +1 -0
  212. package/dist/lib/types.js +6 -0
  213. package/dist/lib/types.js.map +1 -0
  214. package/dist/lib/undo.d.ts +57 -0
  215. package/dist/lib/undo.d.ts.map +1 -0
  216. package/dist/lib/undo.js +185 -0
  217. package/dist/lib/undo.js.map +1 -0
  218. package/dist/lib/utils.d.ts +16 -0
  219. package/dist/lib/utils.d.ts.map +1 -0
  220. package/dist/lib/utils.js +40 -0
  221. package/dist/lib/utils.js.map +1 -0
  222. package/dist/lib/variables.d.ts +42 -0
  223. package/dist/lib/variables.d.ts.map +1 -0
  224. package/dist/lib/variables.js +141 -0
  225. package/dist/lib/variables.js.map +1 -0
  226. package/dist/lib/word.d.ts +80 -0
  227. package/dist/lib/word.d.ts.map +1 -0
  228. package/dist/lib/word.js +360 -0
  229. package/dist/lib/word.js.map +1 -0
  230. package/dist/lib/wordcomments.d.ts +51 -0
  231. package/dist/lib/wordcomments.d.ts.map +1 -0
  232. package/dist/lib/wordcomments.js +587 -0
  233. package/dist/lib/wordcomments.js.map +1 -0
  234. package/eslint.config.js +27 -0
  235. package/lib/annotations.ts +622 -0
  236. package/lib/apply-buildup-colors.py +88 -0
  237. package/lib/build.ts +1013 -0
  238. package/lib/{citations.js → citations.ts} +38 -27
  239. package/lib/commands/{build.js → build.ts} +80 -27
  240. package/lib/commands/{citations.js → citations.ts} +36 -18
  241. package/lib/commands/{comments.js → comments.ts} +187 -54
  242. package/lib/commands/{context.js → context.ts} +18 -8
  243. package/lib/commands/{core.js → core.ts} +34 -20
  244. package/lib/commands/{doi.js → doi.ts} +32 -16
  245. package/lib/commands/{history.js → history.ts} +25 -12
  246. package/lib/commands/{index.js → index.ts} +9 -5
  247. package/lib/commands/{init.js → init.ts} +20 -8
  248. package/lib/commands/{response.js → response.ts} +47 -20
  249. package/lib/commands/{sections.js → sections.ts} +273 -68
  250. package/lib/commands/{utilities.js → utilities.ts} +338 -158
  251. package/lib/{comment-realign.js → comment-realign.ts} +117 -45
  252. package/lib/config.ts +84 -0
  253. package/lib/{crossref.js → crossref.ts} +213 -138
  254. package/lib/dependencies.ts +106 -0
  255. package/lib/doi-cache.ts +115 -0
  256. package/lib/{doi.js → doi.ts} +115 -281
  257. package/lib/{equations.js → equations.ts} +60 -64
  258. package/lib/{errors.js → errors.ts} +56 -48
  259. package/lib/{format.js → format.ts} +137 -63
  260. package/lib/{git.js → git.ts} +66 -63
  261. package/lib/{grammar.js → grammar.ts} +45 -32
  262. package/lib/image-registry.ts +180 -0
  263. package/lib/import.ts +2060 -0
  264. package/lib/journals.ts +505 -0
  265. package/lib/{merge.js → merge.ts} +185 -135
  266. package/lib/{orcid.js → orcid.ts} +17 -22
  267. package/lib/{pdf-comments.js → pdf-comments.ts} +76 -18
  268. package/lib/{pdf-import.js → pdf-import.ts} +148 -70
  269. package/lib/{plugins.js → plugins.ts} +82 -39
  270. package/lib/postprocess.ts +188 -0
  271. package/lib/pptx-color-filter.lua +37 -0
  272. package/lib/pptx-template.ts +625 -0
  273. package/lib/pptx-themes/academic.pptx +0 -0
  274. package/lib/pptx-themes/corporate.pptx +0 -0
  275. package/lib/pptx-themes/dark.pptx +0 -0
  276. package/lib/pptx-themes/default.pptx +0 -0
  277. package/lib/pptx-themes/minimal.pptx +0 -0
  278. package/lib/pptx-themes/plant.pptx +0 -0
  279. package/lib/pptx-themes.ts +896 -0
  280. package/lib/protect-restore.ts +516 -0
  281. package/lib/rate-limiter.ts +94 -0
  282. package/lib/{response.js → response.ts} +36 -21
  283. package/lib/{review.js → review.ts} +53 -43
  284. package/lib/{schema.js → schema.ts} +70 -25
  285. package/lib/{sections.js → sections.ts} +71 -76
  286. package/lib/slides.ts +793 -0
  287. package/lib/{spelling.js → spelling.ts} +43 -59
  288. package/lib/{templates.js → templates.ts} +20 -17
  289. package/lib/themes.ts +742 -0
  290. package/lib/{trackchanges.js → trackchanges.ts} +52 -23
  291. package/lib/types.ts +509 -0
  292. package/lib/{undo.js → undo.ts} +75 -52
  293. package/lib/utils.ts +41 -0
  294. package/lib/{variables.js → variables.ts} +60 -54
  295. package/lib/word.ts +428 -0
  296. package/lib/{wordcomments.js → wordcomments.ts} +94 -40
  297. package/package.json +15 -5
  298. package/skill/REFERENCE.md +67 -0
  299. package/tsconfig.json +26 -0
  300. package/lib/annotations.js +0 -414
  301. package/lib/build.js +0 -639
  302. package/lib/config.js +0 -79
  303. package/lib/import.js +0 -1145
  304. package/lib/journals.js +0 -629
  305. package/lib/word.js +0 -225
  306. /package/lib/{scientific-words.js → scientific-words.ts} +0 -0
@@ -10,42 +10,79 @@ import * as path from 'path';
10
10
  import * as crypto from 'crypto';
11
11
  import { diffWords, diffSentences } from 'diff';
12
12
  import { extractFromWord, extractWordComments } from './import.js';
13
+ import type { ReviewerChange, Conflict, MergeResult } from './types.js';
13
14
 
14
- // Single base document for three-way merge
15
+ // =============================================================================
16
+ // Constants
17
+ // =============================================================================
18
+
19
+ /** Directory for revision tracking data */
15
20
  const REV_DIR = '.rev';
21
+
22
+ /** Path to base document for three-way merge */
16
23
  const BASE_FILE = '.rev/base.docx';
24
+
25
+ /** Path to conflict resolution state */
17
26
  const CONFLICTS_FILE = '.rev/conflicts.json';
18
27
 
19
- /**
20
- * Represents a change from a reviewer
21
- * @typedef {Object} ReviewerChange
22
- * @property {string} reviewer - Reviewer name/identifier
23
- * @property {string} type - 'insert' | 'delete' | 'replace'
24
- * @property {number} start - Start position in original text
25
- * @property {number} end - End position in original text
26
- * @property {string} oldText - Original text (for delete/replace)
27
- * @property {string} newText - New text (for insert/replace)
28
- * @property {string} [date] - Date of change (from Word track changes)
29
- */
28
+ /** Minimum word length for similarity calculations */
29
+ const MIN_WORD_LENGTH = 2;
30
30
 
31
- /**
32
- * Represents a conflict between reviewers
33
- * @typedef {Object} Conflict
34
- * @property {string} id - Unique conflict ID
35
- * @property {number} start - Start position in original
36
- * @property {number} end - End position in original
37
- * @property {string} original - Original text
38
- * @property {ReviewerChange[]} changes - Conflicting changes from different reviewers
39
- * @property {string} [section] - Section file this conflict belongs to
40
- * @property {number} [line] - Line number in section
41
- * @property {string|null} resolved - Resolution choice or null if unresolved
42
- */
31
+ /** Similarity threshold below which changes are considered conflicts */
32
+ const CONFLICT_SIMILARITY_THRESHOLD = 0.8;
33
+
34
+ /** Characters of context for change attribution */
35
+ const CHANGE_CONTEXT_SIZE = 50;
36
+
37
+ // =============================================================================
38
+ // Interfaces
39
+ // =============================================================================
40
+
41
+ interface ReviewerDoc {
42
+ path: string;
43
+ name: string;
44
+ }
45
+
46
+ interface ReviewerComment {
47
+ text: string;
48
+ reviewer: string;
49
+ }
50
+
51
+ interface MergeOptions {
52
+ diffLevel?: 'sentence' | 'word';
53
+ autoResolve?: boolean;
54
+ }
55
+
56
+ interface CheckMatchResult {
57
+ matches: boolean;
58
+ similarity: number;
59
+ }
60
+
61
+ interface ConflictDetectionResult {
62
+ conflicts: Conflict[];
63
+ nonConflicting: ReviewerChange[];
64
+ }
65
+
66
+ interface ConflictsData {
67
+ base: string;
68
+ merged: string;
69
+ conflicts: Conflict[];
70
+ }
71
+
72
+ // =============================================================================
73
+ // Public API
74
+ // =============================================================================
43
75
 
44
76
  /**
45
- * Initialize .rev directory
46
- * @param {string} projectDir
77
+ * Initialize .rev directory for revision tracking
78
+ * @param projectDir - Project directory path
79
+ * @throws {TypeError} If projectDir is not a string
47
80
  */
48
- export function initRevDir(projectDir) {
81
+ export function initRevDir(projectDir: string): void {
82
+ if (typeof projectDir !== 'string') {
83
+ throw new TypeError(`projectDir must be a string, got ${typeof projectDir}`);
84
+ }
85
+
49
86
  const revDir = path.join(projectDir, REV_DIR);
50
87
  if (!fs.existsSync(revDir)) {
51
88
  fs.mkdirSync(revDir, { recursive: true });
@@ -55,10 +92,22 @@ export function initRevDir(projectDir) {
55
92
  /**
56
93
  * Store the base document for three-way merge
57
94
  * Overwrites any previous base document
58
- * @param {string} projectDir
59
- * @param {string} docxPath - Path to the built docx
95
+ * @param projectDir - Project directory path
96
+ * @param docxPath - Path to the built docx to store as base
97
+ * @throws {TypeError} If arguments are not strings
98
+ * @throws {Error} If docxPath does not exist
60
99
  */
61
- export function storeBaseDocument(projectDir, docxPath) {
100
+ export function storeBaseDocument(projectDir: string, docxPath: string): void {
101
+ if (typeof projectDir !== 'string') {
102
+ throw new TypeError(`projectDir must be a string, got ${typeof projectDir}`);
103
+ }
104
+ if (typeof docxPath !== 'string') {
105
+ throw new TypeError(`docxPath must be a string, got ${typeof docxPath}`);
106
+ }
107
+ if (!fs.existsSync(docxPath)) {
108
+ throw new Error(`Source document not found: ${docxPath}`);
109
+ }
110
+
62
111
  initRevDir(projectDir);
63
112
  const basePath = path.join(projectDir, BASE_FILE);
64
113
  fs.copyFileSync(docxPath, basePath);
@@ -66,10 +115,15 @@ export function storeBaseDocument(projectDir, docxPath) {
66
115
 
67
116
  /**
68
117
  * Get the base document path if it exists
69
- * @param {string} projectDir
70
- * @returns {string|null}
118
+ * @param projectDir - Project directory path
119
+ * @returns Path to base document or null if not found
120
+ * @throws {TypeError} If projectDir is not a string
71
121
  */
72
- export function getBaseDocument(projectDir) {
122
+ export function getBaseDocument(projectDir: string): string | null {
123
+ if (typeof projectDir !== 'string') {
124
+ throw new TypeError(`projectDir must be a string, got ${typeof projectDir}`);
125
+ }
126
+
73
127
  const basePath = path.join(projectDir, BASE_FILE);
74
128
  if (fs.existsSync(basePath)) {
75
129
  return basePath;
@@ -79,22 +133,31 @@ export function getBaseDocument(projectDir) {
79
133
 
80
134
  /**
81
135
  * Check if base document exists
82
- * @param {string} projectDir
83
- * @returns {boolean}
136
+ * @param projectDir - Project directory path
137
+ * @returns True if base document exists
138
+ * @throws {TypeError} If projectDir is not a string
84
139
  */
85
- export function hasBaseDocument(projectDir) {
140
+ export function hasBaseDocument(projectDir: string): boolean {
141
+ if (typeof projectDir !== 'string') {
142
+ throw new TypeError(`projectDir must be a string, got ${typeof projectDir}`);
143
+ }
144
+
86
145
  return fs.existsSync(path.join(projectDir, BASE_FILE));
87
146
  }
88
147
 
89
148
  /**
90
- * Compute text similarity between two strings
91
- * @param {string} text1
92
- * @param {string} text2
93
- * @returns {number} Similarity score 0-1
149
+ * Compute text similarity between two strings using Jaccard-like coefficient
150
+ * @param text1 - First text to compare
151
+ * @param text2 - Second text to compare
152
+ * @returns Similarity score 0-1 (0 = no similarity, 1 = identical)
94
153
  */
95
- export function computeSimilarity(text1, text2) {
96
- const words1 = new Set(text1.toLowerCase().split(/\s+/).filter(w => w.length > 2));
97
- const words2 = text2.toLowerCase().split(/\s+/).filter(w => w.length > 2);
154
+ export function computeSimilarity(text1: string, text2: string): number {
155
+ if (typeof text1 !== 'string' || typeof text2 !== 'string') {
156
+ return 0;
157
+ }
158
+
159
+ const words1 = new Set(text1.toLowerCase().split(/\s+/).filter(w => w.length > MIN_WORD_LENGTH));
160
+ const words2 = text2.toLowerCase().split(/\s+/).filter(w => w.length > MIN_WORD_LENGTH);
98
161
  if (words1.size === 0 || words2.length === 0) return 0;
99
162
  const common = words2.filter(w => words1.has(w)).length;
100
163
  return common / Math.max(words1.size, words2.length);
@@ -102,11 +165,8 @@ export function computeSimilarity(text1, text2) {
102
165
 
103
166
  /**
104
167
  * Check if base document matches reviewer document (similarity check)
105
- * @param {string} basePath
106
- * @param {string} reviewerPath
107
- * @returns {Promise<{matches: boolean, similarity: number}>}
108
168
  */
109
- export async function checkBaseMatch(basePath, reviewerPath) {
169
+ export async function checkBaseMatch(basePath: string, reviewerPath: string): Promise<CheckMatchResult> {
110
170
  try {
111
171
  const { text: baseText } = await extractFromWord(basePath);
112
172
  const { text: reviewerText } = await extractFromWord(reviewerPath);
@@ -120,13 +180,12 @@ export async function checkBaseMatch(basePath, reviewerPath) {
120
180
  /**
121
181
  * Extract changes from a Word document compared to original
122
182
  * Uses sentence-level diffing for better conflict detection
123
- * @param {string} originalText - Original text (from base document)
124
- * @param {string} wordText - Text extracted from reviewer's Word doc
125
- * @param {string} reviewer - Reviewer identifier
126
- * @returns {ReviewerChange[]}
183
+ * @param originalText - Original text (from base document)
184
+ * @param wordText - Text extracted from reviewer's Word doc
185
+ * @param reviewer - Reviewer identifier
127
186
  */
128
- export function extractChanges(originalText, wordText, reviewer) {
129
- const changes = [];
187
+ export function extractChanges(originalText: string, wordText: string, reviewer: string): ReviewerChange[] {
188
+ const changes: ReviewerChange[] = [];
130
189
 
131
190
  // Use sentence-level diff for better granularity
132
191
  const diffs = diffSentences(originalText, wordText);
@@ -136,6 +195,7 @@ export function extractChanges(originalText, wordText, reviewer) {
136
195
 
137
196
  while (i < diffs.length) {
138
197
  const part = diffs[i];
198
+ if (!part) break;
139
199
 
140
200
  if (!part.added && !part.removed) {
141
201
  // Unchanged
@@ -143,13 +203,15 @@ export function extractChanges(originalText, wordText, reviewer) {
143
203
  i++;
144
204
  } else if (part.removed && diffs[i + 1]?.added) {
145
205
  // Replacement: removed followed by added
206
+ const nextPart = diffs[i + 1];
207
+ if (!nextPart) break;
146
208
  changes.push({
147
209
  reviewer,
148
210
  type: 'replace',
149
211
  start: originalPos,
150
212
  end: originalPos + part.value.length,
151
213
  oldText: part.value,
152
- newText: diffs[i + 1].value,
214
+ newText: nextPart.value,
153
215
  });
154
216
  originalPos += part.value.length;
155
217
  i += 2;
@@ -184,13 +246,9 @@ export function extractChanges(originalText, wordText, reviewer) {
184
246
 
185
247
  /**
186
248
  * Extract changes using word-level diff (more fine-grained)
187
- * @param {string} originalText
188
- * @param {string} wordText
189
- * @param {string} reviewer
190
- * @returns {ReviewerChange[]}
191
249
  */
192
- export function extractChangesWordLevel(originalText, wordText, reviewer) {
193
- const changes = [];
250
+ export function extractChangesWordLevel(originalText: string, wordText: string, reviewer: string): ReviewerChange[] {
251
+ const changes: ReviewerChange[] = [];
194
252
  const diffs = diffWords(originalText, wordText);
195
253
 
196
254
  let originalPos = 0;
@@ -198,18 +256,21 @@ export function extractChangesWordLevel(originalText, wordText, reviewer) {
198
256
 
199
257
  while (i < diffs.length) {
200
258
  const part = diffs[i];
259
+ if (!part) break;
201
260
 
202
261
  if (!part.added && !part.removed) {
203
262
  originalPos += part.value.length;
204
263
  i++;
205
264
  } else if (part.removed && diffs[i + 1]?.added) {
265
+ const nextPart = diffs[i + 1];
266
+ if (!nextPart) break;
206
267
  changes.push({
207
268
  reviewer,
208
269
  type: 'replace',
209
270
  start: originalPos,
210
271
  end: originalPos + part.value.length,
211
272
  oldText: part.value,
212
- newText: diffs[i + 1].value,
273
+ newText: nextPart.value,
213
274
  });
214
275
  originalPos += part.value.length;
215
276
  i += 2;
@@ -242,11 +303,8 @@ export function extractChangesWordLevel(originalText, wordText, reviewer) {
242
303
 
243
304
  /**
244
305
  * Check if two changes overlap
245
- * @param {ReviewerChange} a
246
- * @param {ReviewerChange} b
247
- * @returns {boolean}
248
306
  */
249
- function changesOverlap(a, b) {
307
+ function changesOverlap(a: ReviewerChange, b: ReviewerChange): boolean {
250
308
  // Insertions at same point conflict
251
309
  if (a.type === 'insert' && b.type === 'insert' && a.start === b.start) {
252
310
  return a.newText !== b.newText; // Same insertion is not a conflict
@@ -274,22 +332,22 @@ function changesOverlap(a, b) {
274
332
 
275
333
  /**
276
334
  * Detect conflicts between changes from multiple reviewers
277
- * @param {ReviewerChange[][]} allChanges - Array of change arrays, one per reviewer
278
- * @returns {{conflicts: Conflict[], nonConflicting: ReviewerChange[]}}
335
+ * @param allChanges - Array of change arrays, one per reviewer
279
336
  */
280
- export function detectConflicts(allChanges) {
337
+ export function detectConflicts(allChanges: ReviewerChange[][]): ConflictDetectionResult {
281
338
  // Flatten and sort all changes by position
282
339
  const flat = allChanges.flat().sort((a, b) => a.start - b.start || a.end - b.end);
283
340
 
284
- const conflicts = [];
285
- const nonConflicting = [];
286
- const usedIndices = new Set();
341
+ const conflicts: Conflict[] = [];
342
+ const nonConflicting: ReviewerChange[] = [];
343
+ const usedIndices = new Set<number>();
287
344
  let conflictId = 0;
288
345
 
289
346
  for (let i = 0; i < flat.length; i++) {
290
347
  if (usedIndices.has(i)) continue;
291
348
 
292
349
  const change = flat[i];
350
+ if (!change) continue;
293
351
  const conflictingChanges = [change];
294
352
 
295
353
  // Find all changes that conflict with this one
@@ -297,6 +355,7 @@ export function detectConflicts(allChanges) {
297
355
  if (usedIndices.has(j)) continue;
298
356
 
299
357
  const other = flat[j];
358
+ if (!other) continue;
300
359
 
301
360
  // Stop if we're past the range
302
361
  if (other.start > change.end && change.type !== 'insert') break;
@@ -309,15 +368,16 @@ export function detectConflicts(allChanges) {
309
368
 
310
369
  if (conflictingChanges.length > 1) {
311
370
  // Multiple reviewers changed the same region
312
- const start = Math.min(...conflictingChanges.map(c => c.start));
313
- const end = Math.max(...conflictingChanges.map(c => c.end));
371
+ const start = Math.min(...conflictingChanges.map(c => c?.start ?? 0).filter(s => s !== undefined));
372
+ const end = Math.max(...conflictingChanges.map(c => c?.end ?? 0).filter(e => e !== undefined));
373
+ const firstChange = conflictingChanges[0];
314
374
 
315
375
  conflicts.push({
316
376
  id: `c${++conflictId}`,
317
377
  start,
318
378
  end,
319
- original: conflictingChanges[0].oldText || '',
320
- changes: conflictingChanges,
379
+ original: firstChange?.oldText || '',
380
+ changes: conflictingChanges.filter((c): c is ReviewerChange => c !== undefined),
321
381
  resolved: null,
322
382
  });
323
383
  usedIndices.add(i);
@@ -329,8 +389,8 @@ export function detectConflicts(allChanges) {
329
389
  }
330
390
 
331
391
  // Deduplicate identical non-conflicting changes
332
- const seen = new Map();
333
- const dedupedNonConflicting = [];
392
+ const seen = new Map<string, boolean>();
393
+ const dedupedNonConflicting: ReviewerChange[] = [];
334
394
 
335
395
  for (const change of nonConflicting) {
336
396
  const key = `${change.start}:${change.end}:${change.type}:${change.newText}`;
@@ -345,11 +405,10 @@ export function detectConflicts(allChanges) {
345
405
 
346
406
  /**
347
407
  * Apply non-conflicting changes to text
348
- * @param {string} originalText
349
- * @param {ReviewerChange[]} changes - Must be sorted by position
350
- * @returns {string}
408
+ * @param originalText
409
+ * @param changes - Must be sorted by position
351
410
  */
352
- export function applyChanges(originalText, changes) {
411
+ export function applyChanges(originalText: string, changes: ReviewerChange[]): string {
353
412
  // Sort by position descending to apply from end to start
354
413
  const sorted = [...changes].sort((a, b) => b.start - a.start);
355
414
 
@@ -370,11 +429,8 @@ export function applyChanges(originalText, changes) {
370
429
 
371
430
  /**
372
431
  * Apply changes as CriticMarkup annotations
373
- * @param {string} originalText
374
- * @param {ReviewerChange[]} changes
375
- * @returns {string}
376
432
  */
377
- export function applyChangesAsAnnotations(originalText, changes) {
433
+ export function applyChangesAsAnnotations(originalText: string, changes: ReviewerChange[]): string {
378
434
  const sorted = [...changes].sort((a, b) => b.start - a.start);
379
435
 
380
436
  let result = originalText;
@@ -397,18 +453,15 @@ export function applyChangesAsAnnotations(originalText, changes) {
397
453
 
398
454
  /**
399
455
  * Apply changes as git-style conflict markers
400
- * @param {string} originalText
401
- * @param {Conflict[]} conflicts
402
- * @returns {string}
403
456
  */
404
- export function applyConflictMarkers(originalText, conflicts) {
457
+ export function applyConflictMarkers(originalText: string, conflicts: Conflict[]): string {
405
458
  // Sort by position descending
406
459
  const sorted = [...conflicts].sort((a, b) => b.start - a.start);
407
460
 
408
461
  let result = originalText;
409
462
 
410
463
  for (const conflict of sorted) {
411
- const markers = [];
464
+ const markers: string[] = [];
412
465
  markers.push(`<<<<<<< CONFLICT ${conflict.id}`);
413
466
 
414
467
  for (const change of conflict.changes) {
@@ -433,12 +486,9 @@ export function applyConflictMarkers(originalText, conflicts) {
433
486
 
434
487
  /**
435
488
  * Format a conflict for display
436
- * @param {Conflict} conflict
437
- * @param {string} originalText
438
- * @returns {string}
439
489
  */
440
- export function formatConflict(conflict, originalText) {
441
- const lines = [];
490
+ export function formatConflict(conflict: Conflict, originalText: string): string {
491
+ const lines: string[] = [];
442
492
  const context = 50;
443
493
 
444
494
  // Show context
@@ -473,13 +523,10 @@ export function formatConflict(conflict, originalText) {
473
523
 
474
524
  /**
475
525
  * Save conflicts to file for later resolution
476
- * @param {string} projectDir
477
- * @param {Conflict[]} conflicts
478
- * @param {string} baseDoc - Base document path
479
526
  */
480
- export function saveConflicts(projectDir, conflicts, baseDoc) {
527
+ export function saveConflicts(projectDir: string, conflicts: Conflict[], baseDoc: string): void {
481
528
  const conflictsPath = path.join(projectDir, CONFLICTS_FILE);
482
- const data = {
529
+ const data: ConflictsData = {
483
530
  base: baseDoc,
484
531
  merged: new Date().toISOString(),
485
532
  conflicts,
@@ -496,22 +543,19 @@ export function saveConflicts(projectDir, conflicts, baseDoc) {
496
543
 
497
544
  /**
498
545
  * Load conflicts from file
499
- * @param {string} projectDir
500
- * @returns {{base: string, merged: string, conflicts: Conflict[]}|null}
501
546
  */
502
- export function loadConflicts(projectDir) {
547
+ export function loadConflicts(projectDir: string): ConflictsData | null {
503
548
  const conflictsPath = path.join(projectDir, CONFLICTS_FILE);
504
549
  if (!fs.existsSync(conflictsPath)) {
505
550
  return null;
506
551
  }
507
- return JSON.parse(fs.readFileSync(conflictsPath, 'utf-8'));
552
+ return JSON.parse(fs.readFileSync(conflictsPath, 'utf-8')) as ConflictsData;
508
553
  }
509
554
 
510
555
  /**
511
556
  * Clear conflicts file after resolution
512
- * @param {string} projectDir
513
557
  */
514
- export function clearConflicts(projectDir) {
558
+ export function clearConflicts(projectDir: string): void {
515
559
  const conflictsPath = path.join(projectDir, CONFLICTS_FILE);
516
560
  if (fs.existsSync(conflictsPath)) {
517
561
  fs.unlinkSync(conflictsPath);
@@ -520,12 +564,12 @@ export function clearConflicts(projectDir) {
520
564
 
521
565
  /**
522
566
  * Merge multiple Word documents using three-way merge
523
- * @param {string} basePath - Path to base document (original sent to reviewers)
524
- * @param {Array<{path: string, name: string}>} reviewerDocs - Reviewer Word docs
525
- * @param {Object} options
526
- * @returns {Promise<{merged: string, conflicts: Conflict[], stats: Object, baseText: string}>}
527
567
  */
528
- export async function mergeThreeWay(basePath, reviewerDocs, options = {}) {
568
+ export async function mergeThreeWay(
569
+ basePath: string,
570
+ reviewerDocs: ReviewerDoc[],
571
+ options: MergeOptions = {}
572
+ ): Promise<MergeResult & { baseText: string }> {
529
573
  const { diffLevel = 'sentence' } = options;
530
574
 
531
575
  if (!fs.existsSync(basePath)) {
@@ -536,8 +580,8 @@ export async function mergeThreeWay(basePath, reviewerDocs, options = {}) {
536
580
  const { text: baseText } = await extractFromWord(basePath);
537
581
 
538
582
  // Extract changes from each reviewer relative to base
539
- const allChanges = [];
540
- const allComments = [];
583
+ const allChanges: ReviewerChange[][] = [];
584
+ const allComments: ReviewerComment[] = [];
541
585
 
542
586
  for (const doc of reviewerDocs) {
543
587
  if (!fs.existsSync(doc.path)) {
@@ -557,8 +601,11 @@ export async function mergeThreeWay(basePath, reviewerDocs, options = {}) {
557
601
  try {
558
602
  const comments = await extractWordComments(doc.path);
559
603
  allComments.push(...comments.map(c => ({ ...c, reviewer: doc.name })));
560
- } catch {
561
- // Comments extraction failed, continue without
604
+ } catch (e) {
605
+ if (process.env.DEBUG) {
606
+ const error = e as Error;
607
+ console.warn(`merge: Failed to extract comments:`, error.message);
608
+ }
562
609
  }
563
610
  }
564
611
 
@@ -581,18 +628,18 @@ export async function mergeThreeWay(basePath, reviewerDocs, options = {}) {
581
628
  comments: allComments.length,
582
629
  };
583
630
 
584
- return { merged, conflicts, stats, baseText };
631
+ return { merged, conflicts, stats, baseText, originalText: baseText };
585
632
  }
586
633
 
587
634
  /**
588
635
  * Merge multiple Word documents against an original markdown file
589
636
  * Legacy function - use mergeThreeWay for proper three-way merge
590
- * @param {string} originalPath - Path to original markdown
591
- * @param {Array<{path: string, name: string}>} reviewerDocs - Reviewer Word docs
592
- * @param {Object} options
593
- * @returns {Promise<{merged: string, conflicts: Conflict[], stats: Object}>}
594
637
  */
595
- export async function mergeReviewerDocs(originalPath, reviewerDocs, options = {}) {
638
+ export async function mergeReviewerDocs(
639
+ originalPath: string,
640
+ reviewerDocs: ReviewerDoc[],
641
+ options: MergeOptions = {}
642
+ ): Promise<MergeResult> {
596
643
  const { autoResolve = false } = options;
597
644
 
598
645
  if (!fs.existsSync(originalPath)) {
@@ -602,8 +649,8 @@ export async function mergeReviewerDocs(originalPath, reviewerDocs, options = {}
602
649
  const originalText = fs.readFileSync(originalPath, 'utf-8');
603
650
 
604
651
  // Extract changes from each reviewer
605
- const allChanges = [];
606
- const allComments = [];
652
+ const allChanges: ReviewerChange[][] = [];
653
+ const allComments: ReviewerComment[] = [];
607
654
 
608
655
  for (const doc of reviewerDocs) {
609
656
  if (!fs.existsSync(doc.path)) {
@@ -618,8 +665,11 @@ export async function mergeReviewerDocs(originalPath, reviewerDocs, options = {}
618
665
  try {
619
666
  const comments = await extractWordComments(doc.path);
620
667
  allComments.push(...comments.map(c => ({ ...c, reviewer: doc.name })));
621
- } catch {
622
- // Comments extraction failed, continue without
668
+ } catch (e) {
669
+ if (process.env.DEBUG) {
670
+ const error = e as Error;
671
+ console.warn(`merge: Failed to extract comments:`, error.message);
672
+ }
623
673
  }
624
674
  }
625
675
 
@@ -648,26 +698,26 @@ export async function mergeReviewerDocs(originalPath, reviewerDocs, options = {}
648
698
 
649
699
  /**
650
700
  * Resolve a conflict by choosing one option
651
- * @param {Conflict} conflict
652
- * @param {number} choice - Index of chosen change (0-based)
653
- * @returns {ReviewerChange}
701
+ * @param conflict
702
+ * @param choice - Index of chosen change (0-based)
654
703
  */
655
- export function resolveConflict(conflict, choice) {
704
+ export function resolveConflict(conflict: Conflict, choice: number): ReviewerChange {
656
705
  if (choice < 0 || choice >= conflict.changes.length) {
657
706
  throw new Error(`Invalid choice: ${choice}. Must be 0-${conflict.changes.length - 1}`);
658
707
  }
659
- conflict.resolved = conflict.changes[choice].reviewer;
660
- return conflict.changes[choice];
708
+ const selectedChange = conflict.changes[choice];
709
+ if (!selectedChange) {
710
+ throw new Error(`Invalid choice: ${choice}. Change not found`);
711
+ }
712
+ conflict.resolved = selectedChange.reviewer;
713
+ return selectedChange;
661
714
  }
662
715
 
663
716
  /**
664
717
  * Get list of unresolved conflicts
665
- * @param {string} projectDir
666
- * @returns {Conflict[]}
667
718
  */
668
- export function getUnresolvedConflicts(projectDir) {
719
+ export function getUnresolvedConflicts(projectDir: string): Conflict[] {
669
720
  const data = loadConflicts(projectDir);
670
721
  if (!data) return [];
671
722
  return data.conflicts.filter(c => c.resolved === null);
672
723
  }
673
-