@puredesktop/platform-editor 1.0.0-beta.1 → 1.0.0-beta.2
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.
- package/package.json +1 -1
- package/src/CollapsePanel.tsx +35 -35
- package/src/DocumentDiffView.tsx +130 -130
- package/src/DocumentEditor.test.ts +118 -118
- package/src/DocumentEditor.tsx +2651 -2631
- package/src/alignedLineDiff.test.ts +52 -52
- package/src/alignedLineDiff.ts +67 -67
- package/src/collectionAssetSrc.ts +2 -2
- package/src/commentUtils.test.ts +350 -350
- package/src/commentUtils.ts +546 -546
- package/src/constants/toolKeys.ts +14 -14
- package/src/contentFormat.test.ts +27 -27
- package/src/contentFormat.ts +18 -18
- package/src/editorExtensions.ts +155 -155
- package/src/extensions/appliedChangeMark.ts +115 -115
- package/src/extensions/autoReviewPrompts.test.ts +529 -529
- package/src/extensions/autoReviewPrompts.ts +1442 -1442
- package/src/extensions/collectionImage.ts +26 -26
- package/src/extensions/collectionImagePaste.ts +215 -215
- package/src/extensions/commentMark.ts +223 -223
- package/src/extensions/documentAssetIdentity.ts +54 -54
- package/src/extensions/figure.ts +92 -92
- package/src/extensions/footnote.ts +186 -186
- package/src/extensions/indexMarker.ts +88 -88
- package/src/extensions/mathEditing.ts +239 -239
- package/src/extensions/mermaidBlock.tsx +297 -297
- package/src/extensions/slashCommands.test.ts +170 -170
- package/src/extensions/slashCommands.ts +746 -746
- package/src/extensions/smartTypography.test.ts +74 -74
- package/src/extensions/smartTypography.ts +120 -120
- package/src/index.ts +138 -137
- package/src/insertCollectionImage.test.ts +60 -60
- package/src/insertCollectionImage.ts +63 -63
- package/src/insertInlineAssetKind.test.ts +67 -67
- package/src/insertInlineAssetKind.ts +49 -49
- package/src/mermaidPreview.test.ts +54 -54
- package/src/mermaidPreview.ts +115 -115
- package/src/styled.ts +697 -676
- package/src/toolbar/ToolBtn.tsx +77 -63
- package/src/toolbar/Toolbar.test.tsx +325 -325
- package/src/toolbar/Toolbar.tsx +662 -624
- package/src/toolbar/groups/HeadingGroup.tsx +175 -153
- package/src/toolbar/overlayTypes.ts +28 -28
- package/src/useEditorExtensions.ts +22 -22
- package/src/utils/markdownUtils.ts +331 -331
|
@@ -1,529 +1,529 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @vitest-environment happy-dom
|
|
3
|
-
*/
|
|
4
|
-
import { Editor } from '@tiptap/core'
|
|
5
|
-
import StarterKit from '@tiptap/starter-kit'
|
|
6
|
-
import { describe, expect, it } from 'vitest'
|
|
7
|
-
import { CommentMark } from './commentMark.js'
|
|
8
|
-
import {
|
|
9
|
-
applyAutoReviewFindings,
|
|
10
|
-
AutoReviewPrompts,
|
|
11
|
-
buildAutoReviewPrompt,
|
|
12
|
-
collectAutoReviewParagraphs,
|
|
13
|
-
collectScopedAutoReviewParagraphs,
|
|
14
|
-
enabledEditorialTypesForProcess,
|
|
15
|
-
parseAutoReviewFindingResponse,
|
|
16
|
-
runAutoReview,
|
|
17
|
-
validateAutoReviewFindings,
|
|
18
|
-
type AutoReviewFindingInput,
|
|
19
|
-
} from './autoReviewPrompts.js'
|
|
20
|
-
|
|
21
|
-
describe('AutoReviewPrompts', () => {
|
|
22
|
-
it('builds separate prompts with paragraph-only payloads', () => {
|
|
23
|
-
const paragraphs = [
|
|
24
|
-
{ id: 'p-1', text: 'Evidence is limited.', from: 1, to: 21 },
|
|
25
|
-
]
|
|
26
|
-
|
|
27
|
-
expect(buildAutoReviewPrompt('claim', paragraphs)).toContain(
|
|
28
|
-
'one thing only: CLAIMS',
|
|
29
|
-
)
|
|
30
|
-
expect(buildAutoReviewPrompt('claim', paragraphs)).toContain(
|
|
31
|
-
'STEP 1 — GATES',
|
|
32
|
-
)
|
|
33
|
-
expect(buildAutoReviewPrompt('claim', paragraphs)).toContain(
|
|
34
|
-
'GATE A — Is it a citation or reference?',
|
|
35
|
-
)
|
|
36
|
-
expect(buildAutoReviewPrompt('claim', paragraphs)).toContain(
|
|
37
|
-
'ONE SENTENCE CAN CARRY TWO CLAIMS',
|
|
38
|
-
)
|
|
39
|
-
expect(buildAutoReviewPrompt('claim', paragraphs)).toContain(
|
|
40
|
-
'source it, soften it, or cut it',
|
|
41
|
-
)
|
|
42
|
-
expect(buildAutoReviewPrompt('claim', paragraphs)).toContain(
|
|
43
|
-
'severity "minor" | "notable" | "significant"',
|
|
44
|
-
)
|
|
45
|
-
expect(buildAutoReviewPrompt('claim', paragraphs)).toContain(
|
|
46
|
-
'"text": "Evidence is limited."',
|
|
47
|
-
)
|
|
48
|
-
expect(buildAutoReviewPrompt('fact-check', paragraphs)).toContain(
|
|
49
|
-
'one thing only: FACT-CHECK NEEDS',
|
|
50
|
-
)
|
|
51
|
-
expect(buildAutoReviewPrompt('fact-check', paragraphs)).toContain(
|
|
52
|
-
'You DETECT; you do not VERIFY',
|
|
53
|
-
)
|
|
54
|
-
expect(buildAutoReviewPrompt('fact-check', paragraphs)).toContain(
|
|
55
|
-
'ATOMIC SPANS',
|
|
56
|
-
)
|
|
57
|
-
expect(buildAutoReviewPrompt('fact-check', paragraphs)).toContain(
|
|
58
|
-
'severity "minor" | "notable" | "significant"',
|
|
59
|
-
)
|
|
60
|
-
expect(buildAutoReviewPrompt('source', paragraphs)).toContain(
|
|
61
|
-
'one thing only: SOURCE checks',
|
|
62
|
-
)
|
|
63
|
-
expect(buildAutoReviewPrompt('source', paragraphs)).toContain(
|
|
64
|
-
'annotation_type "source"',
|
|
65
|
-
)
|
|
66
|
-
expect(buildAutoReviewPrompt('thread', paragraphs)).toContain(
|
|
67
|
-
'You are detecting THREADS',
|
|
68
|
-
)
|
|
69
|
-
expect(buildAutoReviewPrompt('thread', paragraphs)).toContain(
|
|
70
|
-
'annotation_type "thread"',
|
|
71
|
-
)
|
|
72
|
-
expect(buildAutoReviewPrompt('logic', paragraphs)).toContain(
|
|
73
|
-
'You are detecting LOGIC gaps',
|
|
74
|
-
)
|
|
75
|
-
expect(buildAutoReviewPrompt('logic', paragraphs)).toContain(
|
|
76
|
-
'annotation_type "logic"',
|
|
77
|
-
)
|
|
78
|
-
expect(buildAutoReviewPrompt('copyedit', paragraphs)).toContain(
|
|
79
|
-
'You are the COPYEDIT pass',
|
|
80
|
-
)
|
|
81
|
-
expect(buildAutoReviewPrompt('copyedit', paragraphs)).toContain(
|
|
82
|
-
'annotation_type "copyedit"',
|
|
83
|
-
)
|
|
84
|
-
expect(buildAutoReviewPrompt('copyedit', paragraphs)).toContain(
|
|
85
|
-
'{{HOUSE_STYLE}}',
|
|
86
|
-
)
|
|
87
|
-
expect(buildAutoReviewPrompt('voice', paragraphs)).toContain(
|
|
88
|
-
'You are the VOICE check',
|
|
89
|
-
)
|
|
90
|
-
expect(buildAutoReviewPrompt('voice', paragraphs)).toContain(
|
|
91
|
-
'annotation_type "voice"',
|
|
92
|
-
)
|
|
93
|
-
expect(buildAutoReviewPrompt('voice', paragraphs)).toContain(
|
|
94
|
-
'{{VOICE_PROFILE}}',
|
|
95
|
-
)
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('enables the new editorial checks while keeping conflict disabled', () => {
|
|
99
|
-
expect(enabledEditorialTypesForProcess('source')).toEqual(['source'])
|
|
100
|
-
expect(enabledEditorialTypesForProcess('source-and-thread')).toEqual([
|
|
101
|
-
'source',
|
|
102
|
-
'thread',
|
|
103
|
-
])
|
|
104
|
-
expect(enabledEditorialTypesForProcess('thread')).toEqual(['thread'])
|
|
105
|
-
expect(enabledEditorialTypesForProcess('logic')).toEqual(['logic'])
|
|
106
|
-
expect(enabledEditorialTypesForProcess('logic-and-conflict')).toEqual([
|
|
107
|
-
'logic',
|
|
108
|
-
])
|
|
109
|
-
expect(enabledEditorialTypesForProcess('conflict')).toEqual([])
|
|
110
|
-
expect(enabledEditorialTypesForProcess('full-editorial')).toEqual([
|
|
111
|
-
'claim',
|
|
112
|
-
'fact-check',
|
|
113
|
-
'source',
|
|
114
|
-
'thread',
|
|
115
|
-
])
|
|
116
|
-
expect(enabledEditorialTypesForProcess('copyedit')).toEqual(['copyedit'])
|
|
117
|
-
expect(enabledEditorialTypesForProcess('voice')).toEqual(['voice'])
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it('collects only paragraph text from a TipTap document', () => {
|
|
121
|
-
const editor = new Editor({
|
|
122
|
-
extensions: [StarterKit, CommentMark, AutoReviewPrompts],
|
|
123
|
-
content:
|
|
124
|
-
'<h1>Heading claim</h1><p>Small layout failures are easy to spot.</p><p></p><p>Footnotes can collide with running text.</p>',
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
expect(collectAutoReviewParagraphs(editor)).toEqual([
|
|
128
|
-
expect.objectContaining({
|
|
129
|
-
id: 'p-1',
|
|
130
|
-
text: 'Small layout failures are easy to spot.',
|
|
131
|
-
}),
|
|
132
|
-
expect.objectContaining({
|
|
133
|
-
id: 'p-2',
|
|
134
|
-
text: 'Footnotes can collide with running text.',
|
|
135
|
-
}),
|
|
136
|
-
])
|
|
137
|
-
editor.destroy()
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
it('skips protected generated content when collecting paragraphs', () => {
|
|
141
|
-
const editor = new Editor({
|
|
142
|
-
extensions: [StarterKit, CommentMark, AutoReviewPrompts],
|
|
143
|
-
content:
|
|
144
|
-
'<p>Body claim should be reviewed.</p><p>Reference text should not be reviewed.</p>',
|
|
145
|
-
})
|
|
146
|
-
const generated = editor.view.dom.querySelectorAll('p')[1]
|
|
147
|
-
generated.setAttribute('data-pm-generated-references', 'true')
|
|
148
|
-
generated.setAttribute('contenteditable', 'false')
|
|
149
|
-
|
|
150
|
-
expect(collectAutoReviewParagraphs(editor)).toEqual([
|
|
151
|
-
expect.objectContaining({
|
|
152
|
-
text: 'Body claim should be reviewed.',
|
|
153
|
-
}),
|
|
154
|
-
])
|
|
155
|
-
editor.destroy()
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it('validates anchors, filters low confidence, and prefers fact checks for duplicate spans', () => {
|
|
159
|
-
const paragraphs = [
|
|
160
|
-
{
|
|
161
|
-
id: 'p-1',
|
|
162
|
-
text: 'Small layout failures are easy to spot.',
|
|
163
|
-
from: 1,
|
|
164
|
-
to: 40,
|
|
165
|
-
},
|
|
166
|
-
]
|
|
167
|
-
const findings: AutoReviewFindingInput[] = [
|
|
168
|
-
{
|
|
169
|
-
paragraph_id: 'p-1',
|
|
170
|
-
exact_text: 'Small layout failures',
|
|
171
|
-
annotation_type: 'claim',
|
|
172
|
-
shape: 'generalization',
|
|
173
|
-
suggested_note: 'Add support.',
|
|
174
|
-
severity: 'significant',
|
|
175
|
-
confidence: 0.9,
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
paragraph_id: 'p-1',
|
|
179
|
-
exact_text: 'Small layout failures',
|
|
180
|
-
annotation_type: 'fact_check',
|
|
181
|
-
shape: 'attribution',
|
|
182
|
-
suggested_note: 'Verify this statement.',
|
|
183
|
-
severity: 'notable',
|
|
184
|
-
confidence: 0.95,
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
paragraph_id: 'p-1',
|
|
188
|
-
exact_text: 'not in paragraph',
|
|
189
|
-
annotation_type: 'claim',
|
|
190
|
-
confidence: 0.95,
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
paragraph_id: 'p-1',
|
|
194
|
-
exact_text: 'easy to spot',
|
|
195
|
-
annotation_type: 'claim',
|
|
196
|
-
confidence: 0.1,
|
|
197
|
-
},
|
|
198
|
-
]
|
|
199
|
-
|
|
200
|
-
expect(validateAutoReviewFindings(paragraphs, findings)).toEqual([
|
|
201
|
-
expect.objectContaining({
|
|
202
|
-
annotationType: 'fact-check',
|
|
203
|
-
exactText: 'Small layout failures',
|
|
204
|
-
severity: 'advisory',
|
|
205
|
-
from: 1,
|
|
206
|
-
to: 22,
|
|
207
|
-
}),
|
|
208
|
-
])
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
it('normalizes realistic model finding shapes before anchoring them', () => {
|
|
212
|
-
const paragraphs = [
|
|
213
|
-
{
|
|
214
|
-
id: 'p-1',
|
|
215
|
-
text: 'The “quiet export” is a stronger path - by far.',
|
|
216
|
-
from: 1,
|
|
217
|
-
to: 48,
|
|
218
|
-
},
|
|
219
|
-
]
|
|
220
|
-
const findings = parseAutoReviewFindingResponse(
|
|
221
|
-
JSON.stringify([
|
|
222
|
-
{
|
|
223
|
-
paragraphId: 'p-1',
|
|
224
|
-
span: 'the "quiet export" is a stronger path',
|
|
225
|
-
type: 'CLAIM',
|
|
226
|
-
note: '"stronger path" — by whose measure?',
|
|
227
|
-
severity: 'notable',
|
|
228
|
-
confidence: 0.92,
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
paragraph_id: 'p-1',
|
|
232
|
-
exactText: 'BY FAR',
|
|
233
|
-
annotationType: 'FACT_CHECK',
|
|
234
|
-
suggestedNote: '"by far" — what comparison establishes this?',
|
|
235
|
-
confidence: 0.91,
|
|
236
|
-
},
|
|
237
|
-
]),
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
expect(findings).toEqual([
|
|
241
|
-
expect.objectContaining({
|
|
242
|
-
paragraph_id: 'p-1',
|
|
243
|
-
exact_text: 'the "quiet export" is a stronger path',
|
|
244
|
-
annotation_type: 'claim',
|
|
245
|
-
}),
|
|
246
|
-
expect.objectContaining({
|
|
247
|
-
paragraph_id: 'p-1',
|
|
248
|
-
exact_text: 'BY FAR',
|
|
249
|
-
annotation_type: 'fact_check',
|
|
250
|
-
}),
|
|
251
|
-
])
|
|
252
|
-
|
|
253
|
-
expect(validateAutoReviewFindings(paragraphs, findings)).toEqual([
|
|
254
|
-
expect.objectContaining({
|
|
255
|
-
annotationType: 'claim',
|
|
256
|
-
exactText: 'The “quiet export” is a stronger path',
|
|
257
|
-
from: 1,
|
|
258
|
-
to: 38,
|
|
259
|
-
}),
|
|
260
|
-
expect.objectContaining({
|
|
261
|
-
annotationType: 'fact-check',
|
|
262
|
-
exactText: 'by far',
|
|
263
|
-
from: 41,
|
|
264
|
-
to: 47,
|
|
265
|
-
}),
|
|
266
|
-
])
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
it('applies validated findings as claim and fact-check comment marks', () => {
|
|
270
|
-
const editor = new Editor({
|
|
271
|
-
extensions: [StarterKit, CommentMark, AutoReviewPrompts],
|
|
272
|
-
content: '<p>Small layout failures are easy to spot.</p>',
|
|
273
|
-
})
|
|
274
|
-
const paragraphs = collectAutoReviewParagraphs(editor)
|
|
275
|
-
const result = applyAutoReviewFindings(
|
|
276
|
-
editor,
|
|
277
|
-
paragraphs,
|
|
278
|
-
[
|
|
279
|
-
{
|
|
280
|
-
paragraph_id: 'p-1',
|
|
281
|
-
exact_text: 'Small layout failures',
|
|
282
|
-
annotation_type: 'claim',
|
|
283
|
-
subject: 'Layout failures',
|
|
284
|
-
justification: 'This generalization needs evidence.',
|
|
285
|
-
suggested_note: 'Add citation or narrow the claim.',
|
|
286
|
-
severity: 'warning',
|
|
287
|
-
confidence: 0.91,
|
|
288
|
-
},
|
|
289
|
-
],
|
|
290
|
-
{
|
|
291
|
-
author: 'Agent',
|
|
292
|
-
createdAt: '2026-06-18T12:00:00.000Z',
|
|
293
|
-
idPrefix: 'test-review',
|
|
294
|
-
},
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
expect(result.applied).toHaveLength(1)
|
|
298
|
-
expect(result.skipped).toBe(0)
|
|
299
|
-
expect(editor.getHTML()).toContain('data-comment-type="claim"')
|
|
300
|
-
expect(editor.getHTML()).toContain(
|
|
301
|
-
'data-comment-review-status="needs-support"',
|
|
302
|
-
)
|
|
303
|
-
expect(editor.getHTML()).toContain('data-comment-severity="advisory"')
|
|
304
|
-
expect(editor.getHTML()).toContain('Add citation or narrow the claim.')
|
|
305
|
-
editor.destroy()
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
it('applies source, thread, and logic findings with their default open states', () => {
|
|
309
|
-
const editor = new Editor({
|
|
310
|
-
extensions: [StarterKit, CommentMark, AutoReviewPrompts],
|
|
311
|
-
content:
|
|
312
|
-
'<p>Smith 2019 found a 40 percent drop, and later we return to this below, so adoption rises because costs fall.</p>',
|
|
313
|
-
})
|
|
314
|
-
const paragraphs = collectAutoReviewParagraphs(editor)
|
|
315
|
-
const result = applyAutoReviewFindings(
|
|
316
|
-
editor,
|
|
317
|
-
paragraphs,
|
|
318
|
-
[
|
|
319
|
-
{
|
|
320
|
-
paragraph_id: 'p-1',
|
|
321
|
-
exact_text: 'Smith 2019 found a 40 percent drop',
|
|
322
|
-
annotation_type: 'source',
|
|
323
|
-
suggested_note:
|
|
324
|
-
'"40 percent drop" — does Smith 2019 actually make that finding?',
|
|
325
|
-
confidence: 0.95,
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
paragraph_id: 'p-1',
|
|
329
|
-
exact_text: 'we return to this below',
|
|
330
|
-
annotation_type: 'thread',
|
|
331
|
-
suggested_note:
|
|
332
|
-
'"return to this below" — does the promised payoff land later?',
|
|
333
|
-
confidence: 0.9,
|
|
334
|
-
},
|
|
335
|
-
{
|
|
336
|
-
paragraph_id: 'p-1',
|
|
337
|
-
exact_text: 'adoption rises because costs fall',
|
|
338
|
-
annotation_type: 'logic',
|
|
339
|
-
suggested_note:
|
|
340
|
-
'Costs fall, then adoption rises — is the linking step stated?',
|
|
341
|
-
confidence: 0.92,
|
|
342
|
-
},
|
|
343
|
-
],
|
|
344
|
-
{
|
|
345
|
-
author: 'Agent',
|
|
346
|
-
createdAt: '2026-06-18T12:00:00.000Z',
|
|
347
|
-
idPrefix: 'test-review',
|
|
348
|
-
},
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
const html = editor.getHTML()
|
|
352
|
-
expect(result.applied).toHaveLength(3)
|
|
353
|
-
expect(html).toContain('data-comment-type="source"')
|
|
354
|
-
expect(html).toContain('data-comment-type="thread"')
|
|
355
|
-
expect(html).toContain('data-comment-type="logic"')
|
|
356
|
-
expect(html).toContain('data-comment-review-status="unverified"')
|
|
357
|
-
expect(html).toContain('data-comment-review-status="open"')
|
|
358
|
-
expect(html).toContain('data-comment-review-status="needs-support"')
|
|
359
|
-
editor.destroy()
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
it('applies copyedit and voice findings with their default open states', () => {
|
|
363
|
-
const editor = new Editor({
|
|
364
|
-
extensions: [StarterKit, CommentMark, AutoReviewPrompts],
|
|
365
|
-
content:
|
|
366
|
-
'<p>This sentence have a mechanical error, and this passage suddenly sounds promotional.</p>',
|
|
367
|
-
})
|
|
368
|
-
const paragraphs = collectAutoReviewParagraphs(editor)
|
|
369
|
-
const result = applyAutoReviewFindings(
|
|
370
|
-
editor,
|
|
371
|
-
paragraphs,
|
|
372
|
-
[
|
|
373
|
-
{
|
|
374
|
-
paragraph_id: 'p-1',
|
|
375
|
-
exact_text: 'sentence have',
|
|
376
|
-
annotation_type: 'copyedit',
|
|
377
|
-
suggested_note: '"sentence have" → "sentence has".',
|
|
378
|
-
confidence: 0.95,
|
|
379
|
-
},
|
|
380
|
-
{
|
|
381
|
-
paragraph_id: 'p-1',
|
|
382
|
-
exact_text: 'suddenly sounds promotional',
|
|
383
|
-
annotation_type: 'voice',
|
|
384
|
-
suggested_note:
|
|
385
|
-
'This shifts more promotional than the supplied profile — intended?',
|
|
386
|
-
confidence: 0.91,
|
|
387
|
-
},
|
|
388
|
-
],
|
|
389
|
-
{
|
|
390
|
-
author: 'Agent',
|
|
391
|
-
createdAt: '2026-06-18T12:00:00.000Z',
|
|
392
|
-
idPrefix: 'test-review',
|
|
393
|
-
},
|
|
394
|
-
)
|
|
395
|
-
|
|
396
|
-
const html = editor.getHTML()
|
|
397
|
-
expect(result.applied).toHaveLength(2)
|
|
398
|
-
expect(html).toContain('data-comment-type="copyedit"')
|
|
399
|
-
expect(html).toContain('data-comment-review-status="suggested"')
|
|
400
|
-
expect(html).toContain('data-comment-type="voice"')
|
|
401
|
-
expect(html).toContain('data-comment-review-status="needs-review"')
|
|
402
|
-
editor.destroy()
|
|
403
|
-
})
|
|
404
|
-
|
|
405
|
-
it('reports an unavailable provider instead of running local detection', async () => {
|
|
406
|
-
const editor = new Editor({
|
|
407
|
-
extensions: [StarterKit, CommentMark, AutoReviewPrompts],
|
|
408
|
-
content:
|
|
409
|
-
'<p>EPUB 3 shipped in 2011 according to the publishing standard record.</p>',
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
const result = await runAutoReview(editor, {
|
|
413
|
-
createdAt: '2026-06-18T12:00:00.000Z',
|
|
414
|
-
idPrefix: 'test-review',
|
|
415
|
-
})
|
|
416
|
-
|
|
417
|
-
expect(result).toEqual(
|
|
418
|
-
expect.objectContaining({
|
|
419
|
-
paragraphCount: 1,
|
|
420
|
-
findingCount: 0,
|
|
421
|
-
applied: [],
|
|
422
|
-
skipped: 0,
|
|
423
|
-
unavailableReason: 'Editorial review provider is not configured.',
|
|
424
|
-
}),
|
|
425
|
-
)
|
|
426
|
-
expect(editor.getHTML()).not.toContain('data-comment-id=')
|
|
427
|
-
editor.destroy()
|
|
428
|
-
})
|
|
429
|
-
|
|
430
|
-
it('runs a configured findings provider and skips duplicate applications', async () => {
|
|
431
|
-
const editor = new Editor({
|
|
432
|
-
extensions: [StarterKit, CommentMark, AutoReviewPrompts],
|
|
433
|
-
content: '<p>Small layout failures are easy to spot.</p>',
|
|
434
|
-
})
|
|
435
|
-
|
|
436
|
-
const first = await runAutoReview(editor, {
|
|
437
|
-
createdAt: '2026-06-18T12:00:00.000Z',
|
|
438
|
-
idPrefix: 'test-review',
|
|
439
|
-
findingsProvider: async () => [
|
|
440
|
-
{
|
|
441
|
-
paragraph_id: 'p-1',
|
|
442
|
-
exact_text: 'Small layout failures',
|
|
443
|
-
annotation_type: 'claim',
|
|
444
|
-
suggested_note: '"Small layout failures" — says who?',
|
|
445
|
-
confidence: 0.9,
|
|
446
|
-
},
|
|
447
|
-
],
|
|
448
|
-
})
|
|
449
|
-
const second = await runAutoReview(editor, {
|
|
450
|
-
createdAt: '2026-06-18T12:01:00.000Z',
|
|
451
|
-
idPrefix: 'test-review',
|
|
452
|
-
findingsProvider: async () => [
|
|
453
|
-
{
|
|
454
|
-
paragraph_id: 'p-1',
|
|
455
|
-
exact_text: 'Small layout failures',
|
|
456
|
-
annotation_type: 'claim',
|
|
457
|
-
suggested_note: '"Small layout failures" — says who?',
|
|
458
|
-
confidence: 0.9,
|
|
459
|
-
},
|
|
460
|
-
],
|
|
461
|
-
})
|
|
462
|
-
|
|
463
|
-
expect(first.applied).toHaveLength(1)
|
|
464
|
-
expect(second.applied).toHaveLength(0)
|
|
465
|
-
expect(editor.getHTML().match(/data-comment-id=/g)).toHaveLength(1)
|
|
466
|
-
editor.destroy()
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
it('passes local scope paragraphs to the findings provider', async () => {
|
|
470
|
-
const editor = new Editor({
|
|
471
|
-
extensions: [StarterKit, CommentMark, AutoReviewPrompts],
|
|
472
|
-
content:
|
|
473
|
-
'<p>Small layout failures are easy to spot because footnotes can collide with running text.</p><p>Nature Communications reviewed this policy in 2026.</p>',
|
|
474
|
-
})
|
|
475
|
-
const allParagraphs = collectAutoReviewParagraphs(editor)
|
|
476
|
-
editor.commands.setTextSelection({
|
|
477
|
-
from: allParagraphs[0].from,
|
|
478
|
-
to: allParagraphs[0].to,
|
|
479
|
-
})
|
|
480
|
-
const localParagraphs = collectScopedAutoReviewParagraphs(editor, 'local')
|
|
481
|
-
const documentParagraphs = collectScopedAutoReviewParagraphs(
|
|
482
|
-
editor,
|
|
483
|
-
'document',
|
|
484
|
-
)
|
|
485
|
-
|
|
486
|
-
expect(localParagraphs).toHaveLength(1)
|
|
487
|
-
expect(documentParagraphs).toHaveLength(2)
|
|
488
|
-
const result = await runAutoReview(editor, {
|
|
489
|
-
scope: 'local',
|
|
490
|
-
findingsProvider: async ({ paragraphs }) => {
|
|
491
|
-
expect(paragraphs).toHaveLength(1)
|
|
492
|
-
return []
|
|
493
|
-
},
|
|
494
|
-
})
|
|
495
|
-
expect(result.paragraphCount).toBe(1)
|
|
496
|
-
editor.destroy()
|
|
497
|
-
})
|
|
498
|
-
|
|
499
|
-
it('parses the required JSON response shape and common aliases', () => {
|
|
500
|
-
expect(
|
|
501
|
-
parseAutoReviewFindingResponse(
|
|
502
|
-
'{"findings":[{"paragraph_id":"p-1","exact_text":"x","annotation_type":"claim"}]}',
|
|
503
|
-
),
|
|
504
|
-
).toEqual([
|
|
505
|
-
{
|
|
506
|
-
paragraph_id: 'p-1',
|
|
507
|
-
exact_text: 'x',
|
|
508
|
-
annotation_type: 'claim',
|
|
509
|
-
},
|
|
510
|
-
])
|
|
511
|
-
expect(
|
|
512
|
-
parseAutoReviewFindingResponse(
|
|
513
|
-
'{"annotations":[{"paragraphId":"p-1","span":"y","type":"FACT_CHECK"}]}',
|
|
514
|
-
),
|
|
515
|
-
).toEqual([
|
|
516
|
-
{
|
|
517
|
-
paragraph_id: 'p-1',
|
|
518
|
-
exact_text: 'y',
|
|
519
|
-
annotation_type: 'fact_check',
|
|
520
|
-
shape: undefined,
|
|
521
|
-
subject: undefined,
|
|
522
|
-
justification: undefined,
|
|
523
|
-
suggested_note: undefined,
|
|
524
|
-
severity: undefined,
|
|
525
|
-
confidence: undefined,
|
|
526
|
-
},
|
|
527
|
-
])
|
|
528
|
-
})
|
|
529
|
-
})
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
import { Editor } from '@tiptap/core'
|
|
5
|
+
import StarterKit from '@tiptap/starter-kit'
|
|
6
|
+
import { describe, expect, it } from 'vitest'
|
|
7
|
+
import { CommentMark } from './commentMark.js'
|
|
8
|
+
import {
|
|
9
|
+
applyAutoReviewFindings,
|
|
10
|
+
AutoReviewPrompts,
|
|
11
|
+
buildAutoReviewPrompt,
|
|
12
|
+
collectAutoReviewParagraphs,
|
|
13
|
+
collectScopedAutoReviewParagraphs,
|
|
14
|
+
enabledEditorialTypesForProcess,
|
|
15
|
+
parseAutoReviewFindingResponse,
|
|
16
|
+
runAutoReview,
|
|
17
|
+
validateAutoReviewFindings,
|
|
18
|
+
type AutoReviewFindingInput,
|
|
19
|
+
} from './autoReviewPrompts.js'
|
|
20
|
+
|
|
21
|
+
describe('AutoReviewPrompts', () => {
|
|
22
|
+
it('builds separate prompts with paragraph-only payloads', () => {
|
|
23
|
+
const paragraphs = [
|
|
24
|
+
{ id: 'p-1', text: 'Evidence is limited.', from: 1, to: 21 },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
expect(buildAutoReviewPrompt('claim', paragraphs)).toContain(
|
|
28
|
+
'one thing only: CLAIMS',
|
|
29
|
+
)
|
|
30
|
+
expect(buildAutoReviewPrompt('claim', paragraphs)).toContain(
|
|
31
|
+
'STEP 1 — GATES',
|
|
32
|
+
)
|
|
33
|
+
expect(buildAutoReviewPrompt('claim', paragraphs)).toContain(
|
|
34
|
+
'GATE A — Is it a citation or reference?',
|
|
35
|
+
)
|
|
36
|
+
expect(buildAutoReviewPrompt('claim', paragraphs)).toContain(
|
|
37
|
+
'ONE SENTENCE CAN CARRY TWO CLAIMS',
|
|
38
|
+
)
|
|
39
|
+
expect(buildAutoReviewPrompt('claim', paragraphs)).toContain(
|
|
40
|
+
'source it, soften it, or cut it',
|
|
41
|
+
)
|
|
42
|
+
expect(buildAutoReviewPrompt('claim', paragraphs)).toContain(
|
|
43
|
+
'severity "minor" | "notable" | "significant"',
|
|
44
|
+
)
|
|
45
|
+
expect(buildAutoReviewPrompt('claim', paragraphs)).toContain(
|
|
46
|
+
'"text": "Evidence is limited."',
|
|
47
|
+
)
|
|
48
|
+
expect(buildAutoReviewPrompt('fact-check', paragraphs)).toContain(
|
|
49
|
+
'one thing only: FACT-CHECK NEEDS',
|
|
50
|
+
)
|
|
51
|
+
expect(buildAutoReviewPrompt('fact-check', paragraphs)).toContain(
|
|
52
|
+
'You DETECT; you do not VERIFY',
|
|
53
|
+
)
|
|
54
|
+
expect(buildAutoReviewPrompt('fact-check', paragraphs)).toContain(
|
|
55
|
+
'ATOMIC SPANS',
|
|
56
|
+
)
|
|
57
|
+
expect(buildAutoReviewPrompt('fact-check', paragraphs)).toContain(
|
|
58
|
+
'severity "minor" | "notable" | "significant"',
|
|
59
|
+
)
|
|
60
|
+
expect(buildAutoReviewPrompt('source', paragraphs)).toContain(
|
|
61
|
+
'one thing only: SOURCE checks',
|
|
62
|
+
)
|
|
63
|
+
expect(buildAutoReviewPrompt('source', paragraphs)).toContain(
|
|
64
|
+
'annotation_type "source"',
|
|
65
|
+
)
|
|
66
|
+
expect(buildAutoReviewPrompt('thread', paragraphs)).toContain(
|
|
67
|
+
'You are detecting THREADS',
|
|
68
|
+
)
|
|
69
|
+
expect(buildAutoReviewPrompt('thread', paragraphs)).toContain(
|
|
70
|
+
'annotation_type "thread"',
|
|
71
|
+
)
|
|
72
|
+
expect(buildAutoReviewPrompt('logic', paragraphs)).toContain(
|
|
73
|
+
'You are detecting LOGIC gaps',
|
|
74
|
+
)
|
|
75
|
+
expect(buildAutoReviewPrompt('logic', paragraphs)).toContain(
|
|
76
|
+
'annotation_type "logic"',
|
|
77
|
+
)
|
|
78
|
+
expect(buildAutoReviewPrompt('copyedit', paragraphs)).toContain(
|
|
79
|
+
'You are the COPYEDIT pass',
|
|
80
|
+
)
|
|
81
|
+
expect(buildAutoReviewPrompt('copyedit', paragraphs)).toContain(
|
|
82
|
+
'annotation_type "copyedit"',
|
|
83
|
+
)
|
|
84
|
+
expect(buildAutoReviewPrompt('copyedit', paragraphs)).toContain(
|
|
85
|
+
'{{HOUSE_STYLE}}',
|
|
86
|
+
)
|
|
87
|
+
expect(buildAutoReviewPrompt('voice', paragraphs)).toContain(
|
|
88
|
+
'You are the VOICE check',
|
|
89
|
+
)
|
|
90
|
+
expect(buildAutoReviewPrompt('voice', paragraphs)).toContain(
|
|
91
|
+
'annotation_type "voice"',
|
|
92
|
+
)
|
|
93
|
+
expect(buildAutoReviewPrompt('voice', paragraphs)).toContain(
|
|
94
|
+
'{{VOICE_PROFILE}}',
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('enables the new editorial checks while keeping conflict disabled', () => {
|
|
99
|
+
expect(enabledEditorialTypesForProcess('source')).toEqual(['source'])
|
|
100
|
+
expect(enabledEditorialTypesForProcess('source-and-thread')).toEqual([
|
|
101
|
+
'source',
|
|
102
|
+
'thread',
|
|
103
|
+
])
|
|
104
|
+
expect(enabledEditorialTypesForProcess('thread')).toEqual(['thread'])
|
|
105
|
+
expect(enabledEditorialTypesForProcess('logic')).toEqual(['logic'])
|
|
106
|
+
expect(enabledEditorialTypesForProcess('logic-and-conflict')).toEqual([
|
|
107
|
+
'logic',
|
|
108
|
+
])
|
|
109
|
+
expect(enabledEditorialTypesForProcess('conflict')).toEqual([])
|
|
110
|
+
expect(enabledEditorialTypesForProcess('full-editorial')).toEqual([
|
|
111
|
+
'claim',
|
|
112
|
+
'fact-check',
|
|
113
|
+
'source',
|
|
114
|
+
'thread',
|
|
115
|
+
])
|
|
116
|
+
expect(enabledEditorialTypesForProcess('copyedit')).toEqual(['copyedit'])
|
|
117
|
+
expect(enabledEditorialTypesForProcess('voice')).toEqual(['voice'])
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('collects only paragraph text from a TipTap document', () => {
|
|
121
|
+
const editor = new Editor({
|
|
122
|
+
extensions: [StarterKit, CommentMark, AutoReviewPrompts],
|
|
123
|
+
content:
|
|
124
|
+
'<h1>Heading claim</h1><p>Small layout failures are easy to spot.</p><p></p><p>Footnotes can collide with running text.</p>',
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
expect(collectAutoReviewParagraphs(editor)).toEqual([
|
|
128
|
+
expect.objectContaining({
|
|
129
|
+
id: 'p-1',
|
|
130
|
+
text: 'Small layout failures are easy to spot.',
|
|
131
|
+
}),
|
|
132
|
+
expect.objectContaining({
|
|
133
|
+
id: 'p-2',
|
|
134
|
+
text: 'Footnotes can collide with running text.',
|
|
135
|
+
}),
|
|
136
|
+
])
|
|
137
|
+
editor.destroy()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('skips protected generated content when collecting paragraphs', () => {
|
|
141
|
+
const editor = new Editor({
|
|
142
|
+
extensions: [StarterKit, CommentMark, AutoReviewPrompts],
|
|
143
|
+
content:
|
|
144
|
+
'<p>Body claim should be reviewed.</p><p>Reference text should not be reviewed.</p>',
|
|
145
|
+
})
|
|
146
|
+
const generated = editor.view.dom.querySelectorAll('p')[1]
|
|
147
|
+
generated.setAttribute('data-pm-generated-references', 'true')
|
|
148
|
+
generated.setAttribute('contenteditable', 'false')
|
|
149
|
+
|
|
150
|
+
expect(collectAutoReviewParagraphs(editor)).toEqual([
|
|
151
|
+
expect.objectContaining({
|
|
152
|
+
text: 'Body claim should be reviewed.',
|
|
153
|
+
}),
|
|
154
|
+
])
|
|
155
|
+
editor.destroy()
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('validates anchors, filters low confidence, and prefers fact checks for duplicate spans', () => {
|
|
159
|
+
const paragraphs = [
|
|
160
|
+
{
|
|
161
|
+
id: 'p-1',
|
|
162
|
+
text: 'Small layout failures are easy to spot.',
|
|
163
|
+
from: 1,
|
|
164
|
+
to: 40,
|
|
165
|
+
},
|
|
166
|
+
]
|
|
167
|
+
const findings: AutoReviewFindingInput[] = [
|
|
168
|
+
{
|
|
169
|
+
paragraph_id: 'p-1',
|
|
170
|
+
exact_text: 'Small layout failures',
|
|
171
|
+
annotation_type: 'claim',
|
|
172
|
+
shape: 'generalization',
|
|
173
|
+
suggested_note: 'Add support.',
|
|
174
|
+
severity: 'significant',
|
|
175
|
+
confidence: 0.9,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
paragraph_id: 'p-1',
|
|
179
|
+
exact_text: 'Small layout failures',
|
|
180
|
+
annotation_type: 'fact_check',
|
|
181
|
+
shape: 'attribution',
|
|
182
|
+
suggested_note: 'Verify this statement.',
|
|
183
|
+
severity: 'notable',
|
|
184
|
+
confidence: 0.95,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
paragraph_id: 'p-1',
|
|
188
|
+
exact_text: 'not in paragraph',
|
|
189
|
+
annotation_type: 'claim',
|
|
190
|
+
confidence: 0.95,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
paragraph_id: 'p-1',
|
|
194
|
+
exact_text: 'easy to spot',
|
|
195
|
+
annotation_type: 'claim',
|
|
196
|
+
confidence: 0.1,
|
|
197
|
+
},
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
expect(validateAutoReviewFindings(paragraphs, findings)).toEqual([
|
|
201
|
+
expect.objectContaining({
|
|
202
|
+
annotationType: 'fact-check',
|
|
203
|
+
exactText: 'Small layout failures',
|
|
204
|
+
severity: 'advisory',
|
|
205
|
+
from: 1,
|
|
206
|
+
to: 22,
|
|
207
|
+
}),
|
|
208
|
+
])
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('normalizes realistic model finding shapes before anchoring them', () => {
|
|
212
|
+
const paragraphs = [
|
|
213
|
+
{
|
|
214
|
+
id: 'p-1',
|
|
215
|
+
text: 'The “quiet export” is a stronger path - by far.',
|
|
216
|
+
from: 1,
|
|
217
|
+
to: 48,
|
|
218
|
+
},
|
|
219
|
+
]
|
|
220
|
+
const findings = parseAutoReviewFindingResponse(
|
|
221
|
+
JSON.stringify([
|
|
222
|
+
{
|
|
223
|
+
paragraphId: 'p-1',
|
|
224
|
+
span: 'the "quiet export" is a stronger path',
|
|
225
|
+
type: 'CLAIM',
|
|
226
|
+
note: '"stronger path" — by whose measure?',
|
|
227
|
+
severity: 'notable',
|
|
228
|
+
confidence: 0.92,
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
paragraph_id: 'p-1',
|
|
232
|
+
exactText: 'BY FAR',
|
|
233
|
+
annotationType: 'FACT_CHECK',
|
|
234
|
+
suggestedNote: '"by far" — what comparison establishes this?',
|
|
235
|
+
confidence: 0.91,
|
|
236
|
+
},
|
|
237
|
+
]),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
expect(findings).toEqual([
|
|
241
|
+
expect.objectContaining({
|
|
242
|
+
paragraph_id: 'p-1',
|
|
243
|
+
exact_text: 'the "quiet export" is a stronger path',
|
|
244
|
+
annotation_type: 'claim',
|
|
245
|
+
}),
|
|
246
|
+
expect.objectContaining({
|
|
247
|
+
paragraph_id: 'p-1',
|
|
248
|
+
exact_text: 'BY FAR',
|
|
249
|
+
annotation_type: 'fact_check',
|
|
250
|
+
}),
|
|
251
|
+
])
|
|
252
|
+
|
|
253
|
+
expect(validateAutoReviewFindings(paragraphs, findings)).toEqual([
|
|
254
|
+
expect.objectContaining({
|
|
255
|
+
annotationType: 'claim',
|
|
256
|
+
exactText: 'The “quiet export” is a stronger path',
|
|
257
|
+
from: 1,
|
|
258
|
+
to: 38,
|
|
259
|
+
}),
|
|
260
|
+
expect.objectContaining({
|
|
261
|
+
annotationType: 'fact-check',
|
|
262
|
+
exactText: 'by far',
|
|
263
|
+
from: 41,
|
|
264
|
+
to: 47,
|
|
265
|
+
}),
|
|
266
|
+
])
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('applies validated findings as claim and fact-check comment marks', () => {
|
|
270
|
+
const editor = new Editor({
|
|
271
|
+
extensions: [StarterKit, CommentMark, AutoReviewPrompts],
|
|
272
|
+
content: '<p>Small layout failures are easy to spot.</p>',
|
|
273
|
+
})
|
|
274
|
+
const paragraphs = collectAutoReviewParagraphs(editor)
|
|
275
|
+
const result = applyAutoReviewFindings(
|
|
276
|
+
editor,
|
|
277
|
+
paragraphs,
|
|
278
|
+
[
|
|
279
|
+
{
|
|
280
|
+
paragraph_id: 'p-1',
|
|
281
|
+
exact_text: 'Small layout failures',
|
|
282
|
+
annotation_type: 'claim',
|
|
283
|
+
subject: 'Layout failures',
|
|
284
|
+
justification: 'This generalization needs evidence.',
|
|
285
|
+
suggested_note: 'Add citation or narrow the claim.',
|
|
286
|
+
severity: 'warning',
|
|
287
|
+
confidence: 0.91,
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
{
|
|
291
|
+
author: 'Agent',
|
|
292
|
+
createdAt: '2026-06-18T12:00:00.000Z',
|
|
293
|
+
idPrefix: 'test-review',
|
|
294
|
+
},
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
expect(result.applied).toHaveLength(1)
|
|
298
|
+
expect(result.skipped).toBe(0)
|
|
299
|
+
expect(editor.getHTML()).toContain('data-comment-type="claim"')
|
|
300
|
+
expect(editor.getHTML()).toContain(
|
|
301
|
+
'data-comment-review-status="needs-support"',
|
|
302
|
+
)
|
|
303
|
+
expect(editor.getHTML()).toContain('data-comment-severity="advisory"')
|
|
304
|
+
expect(editor.getHTML()).toContain('Add citation or narrow the claim.')
|
|
305
|
+
editor.destroy()
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('applies source, thread, and logic findings with their default open states', () => {
|
|
309
|
+
const editor = new Editor({
|
|
310
|
+
extensions: [StarterKit, CommentMark, AutoReviewPrompts],
|
|
311
|
+
content:
|
|
312
|
+
'<p>Smith 2019 found a 40 percent drop, and later we return to this below, so adoption rises because costs fall.</p>',
|
|
313
|
+
})
|
|
314
|
+
const paragraphs = collectAutoReviewParagraphs(editor)
|
|
315
|
+
const result = applyAutoReviewFindings(
|
|
316
|
+
editor,
|
|
317
|
+
paragraphs,
|
|
318
|
+
[
|
|
319
|
+
{
|
|
320
|
+
paragraph_id: 'p-1',
|
|
321
|
+
exact_text: 'Smith 2019 found a 40 percent drop',
|
|
322
|
+
annotation_type: 'source',
|
|
323
|
+
suggested_note:
|
|
324
|
+
'"40 percent drop" — does Smith 2019 actually make that finding?',
|
|
325
|
+
confidence: 0.95,
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
paragraph_id: 'p-1',
|
|
329
|
+
exact_text: 'we return to this below',
|
|
330
|
+
annotation_type: 'thread',
|
|
331
|
+
suggested_note:
|
|
332
|
+
'"return to this below" — does the promised payoff land later?',
|
|
333
|
+
confidence: 0.9,
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
paragraph_id: 'p-1',
|
|
337
|
+
exact_text: 'adoption rises because costs fall',
|
|
338
|
+
annotation_type: 'logic',
|
|
339
|
+
suggested_note:
|
|
340
|
+
'Costs fall, then adoption rises — is the linking step stated?',
|
|
341
|
+
confidence: 0.92,
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
{
|
|
345
|
+
author: 'Agent',
|
|
346
|
+
createdAt: '2026-06-18T12:00:00.000Z',
|
|
347
|
+
idPrefix: 'test-review',
|
|
348
|
+
},
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
const html = editor.getHTML()
|
|
352
|
+
expect(result.applied).toHaveLength(3)
|
|
353
|
+
expect(html).toContain('data-comment-type="source"')
|
|
354
|
+
expect(html).toContain('data-comment-type="thread"')
|
|
355
|
+
expect(html).toContain('data-comment-type="logic"')
|
|
356
|
+
expect(html).toContain('data-comment-review-status="unverified"')
|
|
357
|
+
expect(html).toContain('data-comment-review-status="open"')
|
|
358
|
+
expect(html).toContain('data-comment-review-status="needs-support"')
|
|
359
|
+
editor.destroy()
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('applies copyedit and voice findings with their default open states', () => {
|
|
363
|
+
const editor = new Editor({
|
|
364
|
+
extensions: [StarterKit, CommentMark, AutoReviewPrompts],
|
|
365
|
+
content:
|
|
366
|
+
'<p>This sentence have a mechanical error, and this passage suddenly sounds promotional.</p>',
|
|
367
|
+
})
|
|
368
|
+
const paragraphs = collectAutoReviewParagraphs(editor)
|
|
369
|
+
const result = applyAutoReviewFindings(
|
|
370
|
+
editor,
|
|
371
|
+
paragraphs,
|
|
372
|
+
[
|
|
373
|
+
{
|
|
374
|
+
paragraph_id: 'p-1',
|
|
375
|
+
exact_text: 'sentence have',
|
|
376
|
+
annotation_type: 'copyedit',
|
|
377
|
+
suggested_note: '"sentence have" → "sentence has".',
|
|
378
|
+
confidence: 0.95,
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
paragraph_id: 'p-1',
|
|
382
|
+
exact_text: 'suddenly sounds promotional',
|
|
383
|
+
annotation_type: 'voice',
|
|
384
|
+
suggested_note:
|
|
385
|
+
'This shifts more promotional than the supplied profile — intended?',
|
|
386
|
+
confidence: 0.91,
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
{
|
|
390
|
+
author: 'Agent',
|
|
391
|
+
createdAt: '2026-06-18T12:00:00.000Z',
|
|
392
|
+
idPrefix: 'test-review',
|
|
393
|
+
},
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
const html = editor.getHTML()
|
|
397
|
+
expect(result.applied).toHaveLength(2)
|
|
398
|
+
expect(html).toContain('data-comment-type="copyedit"')
|
|
399
|
+
expect(html).toContain('data-comment-review-status="suggested"')
|
|
400
|
+
expect(html).toContain('data-comment-type="voice"')
|
|
401
|
+
expect(html).toContain('data-comment-review-status="needs-review"')
|
|
402
|
+
editor.destroy()
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it('reports an unavailable provider instead of running local detection', async () => {
|
|
406
|
+
const editor = new Editor({
|
|
407
|
+
extensions: [StarterKit, CommentMark, AutoReviewPrompts],
|
|
408
|
+
content:
|
|
409
|
+
'<p>EPUB 3 shipped in 2011 according to the publishing standard record.</p>',
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
const result = await runAutoReview(editor, {
|
|
413
|
+
createdAt: '2026-06-18T12:00:00.000Z',
|
|
414
|
+
idPrefix: 'test-review',
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
expect(result).toEqual(
|
|
418
|
+
expect.objectContaining({
|
|
419
|
+
paragraphCount: 1,
|
|
420
|
+
findingCount: 0,
|
|
421
|
+
applied: [],
|
|
422
|
+
skipped: 0,
|
|
423
|
+
unavailableReason: 'Editorial review provider is not configured.',
|
|
424
|
+
}),
|
|
425
|
+
)
|
|
426
|
+
expect(editor.getHTML()).not.toContain('data-comment-id=')
|
|
427
|
+
editor.destroy()
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it('runs a configured findings provider and skips duplicate applications', async () => {
|
|
431
|
+
const editor = new Editor({
|
|
432
|
+
extensions: [StarterKit, CommentMark, AutoReviewPrompts],
|
|
433
|
+
content: '<p>Small layout failures are easy to spot.</p>',
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
const first = await runAutoReview(editor, {
|
|
437
|
+
createdAt: '2026-06-18T12:00:00.000Z',
|
|
438
|
+
idPrefix: 'test-review',
|
|
439
|
+
findingsProvider: async () => [
|
|
440
|
+
{
|
|
441
|
+
paragraph_id: 'p-1',
|
|
442
|
+
exact_text: 'Small layout failures',
|
|
443
|
+
annotation_type: 'claim',
|
|
444
|
+
suggested_note: '"Small layout failures" — says who?',
|
|
445
|
+
confidence: 0.9,
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
})
|
|
449
|
+
const second = await runAutoReview(editor, {
|
|
450
|
+
createdAt: '2026-06-18T12:01:00.000Z',
|
|
451
|
+
idPrefix: 'test-review',
|
|
452
|
+
findingsProvider: async () => [
|
|
453
|
+
{
|
|
454
|
+
paragraph_id: 'p-1',
|
|
455
|
+
exact_text: 'Small layout failures',
|
|
456
|
+
annotation_type: 'claim',
|
|
457
|
+
suggested_note: '"Small layout failures" — says who?',
|
|
458
|
+
confidence: 0.9,
|
|
459
|
+
},
|
|
460
|
+
],
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
expect(first.applied).toHaveLength(1)
|
|
464
|
+
expect(second.applied).toHaveLength(0)
|
|
465
|
+
expect(editor.getHTML().match(/data-comment-id=/g)).toHaveLength(1)
|
|
466
|
+
editor.destroy()
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it('passes local scope paragraphs to the findings provider', async () => {
|
|
470
|
+
const editor = new Editor({
|
|
471
|
+
extensions: [StarterKit, CommentMark, AutoReviewPrompts],
|
|
472
|
+
content:
|
|
473
|
+
'<p>Small layout failures are easy to spot because footnotes can collide with running text.</p><p>Nature Communications reviewed this policy in 2026.</p>',
|
|
474
|
+
})
|
|
475
|
+
const allParagraphs = collectAutoReviewParagraphs(editor)
|
|
476
|
+
editor.commands.setTextSelection({
|
|
477
|
+
from: allParagraphs[0].from,
|
|
478
|
+
to: allParagraphs[0].to,
|
|
479
|
+
})
|
|
480
|
+
const localParagraphs = collectScopedAutoReviewParagraphs(editor, 'local')
|
|
481
|
+
const documentParagraphs = collectScopedAutoReviewParagraphs(
|
|
482
|
+
editor,
|
|
483
|
+
'document',
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
expect(localParagraphs).toHaveLength(1)
|
|
487
|
+
expect(documentParagraphs).toHaveLength(2)
|
|
488
|
+
const result = await runAutoReview(editor, {
|
|
489
|
+
scope: 'local',
|
|
490
|
+
findingsProvider: async ({ paragraphs }) => {
|
|
491
|
+
expect(paragraphs).toHaveLength(1)
|
|
492
|
+
return []
|
|
493
|
+
},
|
|
494
|
+
})
|
|
495
|
+
expect(result.paragraphCount).toBe(1)
|
|
496
|
+
editor.destroy()
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
it('parses the required JSON response shape and common aliases', () => {
|
|
500
|
+
expect(
|
|
501
|
+
parseAutoReviewFindingResponse(
|
|
502
|
+
'{"findings":[{"paragraph_id":"p-1","exact_text":"x","annotation_type":"claim"}]}',
|
|
503
|
+
),
|
|
504
|
+
).toEqual([
|
|
505
|
+
{
|
|
506
|
+
paragraph_id: 'p-1',
|
|
507
|
+
exact_text: 'x',
|
|
508
|
+
annotation_type: 'claim',
|
|
509
|
+
},
|
|
510
|
+
])
|
|
511
|
+
expect(
|
|
512
|
+
parseAutoReviewFindingResponse(
|
|
513
|
+
'{"annotations":[{"paragraphId":"p-1","span":"y","type":"FACT_CHECK"}]}',
|
|
514
|
+
),
|
|
515
|
+
).toEqual([
|
|
516
|
+
{
|
|
517
|
+
paragraph_id: 'p-1',
|
|
518
|
+
exact_text: 'y',
|
|
519
|
+
annotation_type: 'fact_check',
|
|
520
|
+
shape: undefined,
|
|
521
|
+
subject: undefined,
|
|
522
|
+
justification: undefined,
|
|
523
|
+
suggested_note: undefined,
|
|
524
|
+
severity: undefined,
|
|
525
|
+
confidence: undefined,
|
|
526
|
+
},
|
|
527
|
+
])
|
|
528
|
+
})
|
|
529
|
+
})
|