@hanna84/mcp-writing 3.5.1 → 3.5.3
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/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,23 @@ All notable changes to this project will be documented in this file. Dates are d
|
|
|
4
4
|
|
|
5
5
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
6
6
|
|
|
7
|
+
#### [v3.5.3](https://github.com/hannasdev/mcp-writing/compare/v3.5.2...v3.5.3)
|
|
8
|
+
|
|
9
|
+
- fix(review-bundles): keep epigraph prose without epigraph titles in beta exports [`#185`](https://github.com/hannasdev/mcp-writing/pull/185)
|
|
10
|
+
|
|
11
|
+
#### [v3.5.2](https://github.com/hannasdev/mcp-writing/compare/v3.5.1...v3.5.2)
|
|
12
|
+
|
|
13
|
+
> 8 May 2026
|
|
14
|
+
|
|
15
|
+
- Improve beta prose flow and centered scene dateline [`#184`](https://github.com/hannasdev/mcp-writing/pull/184)
|
|
16
|
+
- Release 3.5.2 [`5fb224a`](https://github.com/hannasdev/mcp-writing/commit/5fb224af5a66111c9b8d3d32e17acbb908fa5f97)
|
|
17
|
+
|
|
7
18
|
#### [v3.5.1](https://github.com/hannasdev/mcp-writing/compare/v3.5.0...v3.5.1)
|
|
8
19
|
|
|
20
|
+
> 8 May 2026
|
|
21
|
+
|
|
9
22
|
- Fix beta accountability follow-ups and test coverage [`#183`](https://github.com/hannasdev/mcp-writing/pull/183)
|
|
23
|
+
- Release 3.5.1 [`28d77f7`](https://github.com/hannasdev/mcp-writing/commit/28d77f755708dc8e987caa69dfcb6f3c5408b916)
|
|
10
24
|
|
|
11
25
|
#### [v3.5.0](https://github.com/hannasdev/mcp-writing/compare/v3.4.4...v3.5.0)
|
|
12
26
|
|
package/package.json
CHANGED
|
@@ -57,7 +57,7 @@ function renderBetaFeedbackFormMarkdown({ projectId, recipientName, generatedAt
|
|
|
57
57
|
].join("\n") + "\n";
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
function
|
|
60
|
+
function loadBundleSceneRowsWithTags(dbHandle, projectId, sceneIds) {
|
|
61
61
|
if (!Array.isArray(sceneIds) || sceneIds.length === 0) return [];
|
|
62
62
|
const rows = [];
|
|
63
63
|
// 900 is safely below SQLite's per-query bound of 999 host parameters
|
|
@@ -74,6 +74,7 @@ function loadBundleSceneRows(dbHandle, projectId, sceneIds) {
|
|
|
74
74
|
title,
|
|
75
75
|
part,
|
|
76
76
|
chapter,
|
|
77
|
+
chapter_title,
|
|
77
78
|
timeline_position,
|
|
78
79
|
logline,
|
|
79
80
|
pov,
|
|
@@ -85,12 +86,36 @@ function loadBundleSceneRows(dbHandle, projectId, sceneIds) {
|
|
|
85
86
|
rows.push(...chunkRows);
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
const rowMap = new Map(rows.map(row => [row.scene_id, row]));
|
|
89
89
|
const orderedRows = [];
|
|
90
90
|
const missingSceneIds = [];
|
|
91
91
|
|
|
92
|
+
// Load tags in batches to avoid N+1 queries for large bundles.
|
|
93
|
+
const tagsBySceneId = new Map(rows.map(row => [row.scene_id, []]));
|
|
94
|
+
const tagChunkSize = 900;
|
|
95
|
+
for (let offset = 0; offset < rows.length; offset += tagChunkSize) {
|
|
96
|
+
const chunk = rows.slice(offset, offset + tagChunkSize);
|
|
97
|
+
const chunkSceneIds = chunk.map(row => row.scene_id);
|
|
98
|
+
if (chunkSceneIds.length === 0) continue;
|
|
99
|
+
const placeholders = chunkSceneIds.map(() => "?").join(",");
|
|
100
|
+
const tagRows = dbHandle.prepare(`
|
|
101
|
+
SELECT scene_id, tag
|
|
102
|
+
FROM scene_tags
|
|
103
|
+
WHERE project_id = ? AND scene_id IN (${placeholders})
|
|
104
|
+
`).all(projectId, ...chunkSceneIds);
|
|
105
|
+
for (const tagRow of tagRows) {
|
|
106
|
+
const tags = tagsBySceneId.get(tagRow.scene_id);
|
|
107
|
+
if (tags) tags.push(tagRow.tag);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const rowsWithTags = rows.map(row => ({
|
|
112
|
+
...row,
|
|
113
|
+
tags: tagsBySceneId.get(row.scene_id) ?? [],
|
|
114
|
+
}));
|
|
115
|
+
const rowMapWithTags = new Map(rowsWithTags.map(row => [row.scene_id, row]));
|
|
116
|
+
|
|
92
117
|
for (const sceneId of sceneIds) {
|
|
93
|
-
const row =
|
|
118
|
+
const row = rowMapWithTags.get(sceneId);
|
|
94
119
|
if (row) {
|
|
95
120
|
orderedRows.push(row);
|
|
96
121
|
} else {
|
|
@@ -233,20 +258,149 @@ function readProse(filePath, { syncDir } = {}) {
|
|
|
233
258
|
}
|
|
234
259
|
}
|
|
235
260
|
|
|
261
|
+
function normalizeHardWrappedProse(rawProse) {
|
|
262
|
+
const prose = String(rawProse ?? "").replace(/\r\n?/g, "\n").trim();
|
|
263
|
+
if (!prose) return "";
|
|
264
|
+
const paragraphs = prose
|
|
265
|
+
.split(/\n\s*\n/g)
|
|
266
|
+
.map(paragraph => paragraph.replace(/\s*\n\s*/g, " ").trim())
|
|
267
|
+
.filter(Boolean);
|
|
268
|
+
return paragraphs.join("\n\n");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function extractSceneDateline(prose) {
|
|
272
|
+
const normalized = String(prose ?? "").replace(/\r\n?/g, "\n").trim();
|
|
273
|
+
if (!normalized) {
|
|
274
|
+
return { dateline: null, body: "" };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const lines = normalized
|
|
278
|
+
.split("\n")
|
|
279
|
+
.map(line => line.trim())
|
|
280
|
+
.filter(Boolean);
|
|
281
|
+
if (lines.length === 0) {
|
|
282
|
+
return { dateline: null, body: "" };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const firstParagraph = lines[0];
|
|
286
|
+
const dashMatch = firstParagraph.match(/^(.+?)\s*[–-]\s*(.+)$/);
|
|
287
|
+
const left = dashMatch?.[1]?.trim() ?? "";
|
|
288
|
+
const right = dashMatch?.[2]?.trim() ?? "";
|
|
289
|
+
const totalWords = firstParagraph.split(/\s+/).filter(Boolean).length;
|
|
290
|
+
const looksLikeDateline = (
|
|
291
|
+
firstParagraph.length >= 6
|
|
292
|
+
&& firstParagraph.length <= 90
|
|
293
|
+
&& Boolean(dashMatch)
|
|
294
|
+
&& left.length >= 2
|
|
295
|
+
&& right.length >= 2
|
|
296
|
+
&& totalWords <= 14
|
|
297
|
+
&& !/[!?]/.test(firstParagraph)
|
|
298
|
+
&& !/[“”"']/.test(firstParagraph)
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
if (!looksLikeDateline) {
|
|
302
|
+
return { dateline: null, body: normalized };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
dateline: firstParagraph,
|
|
307
|
+
body: lines.slice(1).join("\n"),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function normalizeBetaProseFlow(prose) {
|
|
312
|
+
const normalized = String(prose ?? "").replace(/\r\n?/g, "\n").trim();
|
|
313
|
+
if (!normalized) return "";
|
|
314
|
+
const paragraphs = normalized
|
|
315
|
+
.split(/\n\s*\n/g)
|
|
316
|
+
.map(paragraph => paragraph
|
|
317
|
+
.split("\n")
|
|
318
|
+
.map(line => line.trim())
|
|
319
|
+
.filter(Boolean)
|
|
320
|
+
.join("\n"))
|
|
321
|
+
.filter(Boolean);
|
|
322
|
+
// For beta exports, convert paragraph blocks into regular line breaks so the
|
|
323
|
+
// reading flow stays continuous without large section gaps.
|
|
324
|
+
return paragraphs.join("\n");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function normalizeBetaTypography(prose) {
|
|
328
|
+
return String(prose ?? "")
|
|
329
|
+
.replace(/(^|\s)--(\s|$)/g, "$1—$2");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function renderProseWithInlineEmphasis(doc, prose, {
|
|
333
|
+
bodyFont,
|
|
334
|
+
italicFont,
|
|
335
|
+
fontSize,
|
|
336
|
+
width,
|
|
337
|
+
align = "left",
|
|
338
|
+
lineGap = 0,
|
|
339
|
+
paragraphGap = 0,
|
|
340
|
+
blankLineMoveDown = 0.15,
|
|
341
|
+
}) {
|
|
342
|
+
const lines = String(prose ?? "").split("\n");
|
|
343
|
+
for (const line of lines) {
|
|
344
|
+
if (line.length === 0) {
|
|
345
|
+
doc.moveDown(blankLineMoveDown);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (line.trim() === "***") {
|
|
350
|
+
doc.moveDown(0.5);
|
|
351
|
+
doc.fontSize(fontSize).font(bodyFont);
|
|
352
|
+
doc.text("***", { align: "center", width, lineGap, paragraphGap: 0 });
|
|
353
|
+
doc.moveDown(0.5);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const segments = line.split(/(\*[^*\n]+\*)/g).filter(Boolean);
|
|
358
|
+
|
|
359
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
360
|
+
const segment = segments[index];
|
|
361
|
+
const isItalic = /^\*[^*\n]+\*$/.test(segment);
|
|
362
|
+
const text = isItalic ? segment.slice(1, -1) : segment;
|
|
363
|
+
if (!text) continue;
|
|
364
|
+
doc.fontSize(fontSize).font(isItalic ? italicFont : bodyFont);
|
|
365
|
+
doc.text(text, {
|
|
366
|
+
align,
|
|
367
|
+
width,
|
|
368
|
+
lineGap,
|
|
369
|
+
paragraphGap,
|
|
370
|
+
continued: index < segments.length - 1,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
doc.font(bodyFont).fontSize(fontSize);
|
|
375
|
+
}
|
|
376
|
+
|
|
236
377
|
function renderSceneBlock(scene, options) {
|
|
237
378
|
const {
|
|
238
379
|
profile,
|
|
239
380
|
includeSceneIds,
|
|
240
381
|
includeMetadataSidebar,
|
|
241
382
|
includeParagraphAnchors,
|
|
383
|
+
showChapterHeading,
|
|
242
384
|
} = options;
|
|
243
385
|
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
386
|
+
const isBetaProfile = profile === "beta_reader_personalized";
|
|
387
|
+
const isEpigraph = isBetaProfile && scene.tags?.includes("epigraph");
|
|
388
|
+
|
|
389
|
+
const parts = [];
|
|
248
390
|
|
|
249
|
-
|
|
391
|
+
// Render chapter heading only when the caller detects a chapter transition.
|
|
392
|
+
if (isBetaProfile && scene.chapter_title && showChapterHeading) {
|
|
393
|
+
parts.push(`## ${escapeMarkdown(scene.chapter_title)}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Only render heading if not an epigraph
|
|
397
|
+
if (!isEpigraph) {
|
|
398
|
+
const title = scene.title || scene.scene_id;
|
|
399
|
+
const sceneHeading = includeSceneIds
|
|
400
|
+
? `## ${escapeMarkdown(title)} (${escapeMarkdown(scene.scene_id)})`
|
|
401
|
+
: `## ${escapeMarkdown(title)}`;
|
|
402
|
+
parts.push(sceneHeading);
|
|
403
|
+
}
|
|
250
404
|
|
|
251
405
|
if (profile === "outline_discussion") {
|
|
252
406
|
const summaryParts = [];
|
|
@@ -278,11 +432,11 @@ function renderSceneBlock(scene, options) {
|
|
|
278
432
|
|
|
279
433
|
const prose = scene.prose ?? "";
|
|
280
434
|
if (!includeParagraphAnchors || prose.length === 0) {
|
|
281
|
-
parts.push(prose);
|
|
435
|
+
parts.push(normalizeHardWrappedProse(prose));
|
|
282
436
|
return parts.join("\n\n");
|
|
283
437
|
}
|
|
284
438
|
|
|
285
|
-
const paragraphs = prose
|
|
439
|
+
const paragraphs = normalizeHardWrappedProse(prose)
|
|
286
440
|
.split(/\n\s*\n/g)
|
|
287
441
|
.map(p => p.trim())
|
|
288
442
|
.filter(Boolean);
|
|
@@ -362,7 +516,7 @@ export function renderReviewBundleMarkdown(dbHandle, plan, { generatedAt, syncDi
|
|
|
362
516
|
const syncDir = syncDirOpt ?? process.env.WRITING_SYNC_DIR ?? null;
|
|
363
517
|
|
|
364
518
|
const sceneIds = plan.ordering.map(row => row.scene_id);
|
|
365
|
-
const rows =
|
|
519
|
+
const rows = loadBundleSceneRowsWithTags(dbHandle, plan.resolved_scope.project_id, sceneIds);
|
|
366
520
|
const sections = [];
|
|
367
521
|
const recipientName = plan.resolved_scope?.options?.recipient_name;
|
|
368
522
|
const recipientDisplayName = normalizeRecipientDisplayName(recipientName);
|
|
@@ -391,7 +545,8 @@ export function renderReviewBundleMarkdown(dbHandle, plan, { generatedAt, syncDi
|
|
|
391
545
|
);
|
|
392
546
|
}
|
|
393
547
|
|
|
394
|
-
for (
|
|
548
|
+
for (let sceneIndex = 0; sceneIndex < rows.length; sceneIndex += 1) {
|
|
549
|
+
const scene = rows[sceneIndex];
|
|
395
550
|
let prose = "";
|
|
396
551
|
if (profile === "editor_detailed" || profile === "beta_reader_personalized") {
|
|
397
552
|
const resolved = readProse(scene.file_path, { syncDir });
|
|
@@ -409,11 +564,16 @@ export function renderReviewBundleMarkdown(dbHandle, plan, { generatedAt, syncDi
|
|
|
409
564
|
prose = resolved;
|
|
410
565
|
}
|
|
411
566
|
const withProse = { ...scene, prose };
|
|
567
|
+
const prevScene = sceneIndex > 0 ? rows[sceneIndex - 1] : null;
|
|
568
|
+
const showChapterHeading = isBetaProfile
|
|
569
|
+
&& Boolean(scene.chapter_title)
|
|
570
|
+
&& (!prevScene || prevScene.chapter !== scene.chapter);
|
|
412
571
|
sections.push(renderSceneBlock(withProse, {
|
|
413
572
|
profile,
|
|
414
573
|
includeSceneIds,
|
|
415
574
|
includeMetadataSidebar,
|
|
416
575
|
includeParagraphAnchors,
|
|
576
|
+
showChapterHeading,
|
|
417
577
|
}));
|
|
418
578
|
}
|
|
419
579
|
|
|
@@ -433,15 +593,16 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
433
593
|
const syncDir = syncDirOpt ?? process.env.WRITING_SYNC_DIR ?? null;
|
|
434
594
|
const isBetaProfile = profile === "beta_reader_personalized";
|
|
435
595
|
const proseFontSize = isBetaProfile ? 8 : 10;
|
|
436
|
-
const proseLineGap = isBetaProfile ?
|
|
596
|
+
const proseLineGap = isBetaProfile ? 1.6 : 3;
|
|
437
597
|
const bodyFont = profile === "beta_reader_personalized" ? "Times-Roman" : "Helvetica";
|
|
438
598
|
const coverHeadingFont = profile === "beta_reader_personalized" ? "Times-Bold" : "Helvetica-Bold";
|
|
439
599
|
// Beta scene headings intentionally use body font (non-bold) per product direction.
|
|
440
600
|
const sceneHeadingFont = isBetaProfile ? bodyFont : coverHeadingFont;
|
|
441
|
-
const
|
|
601
|
+
const italicFont = profile === "beta_reader_personalized" ? "Times-Italic" : "Helvetica-Oblique";
|
|
602
|
+
const metaFont = italicFont;
|
|
442
603
|
|
|
443
604
|
const sceneIds = plan.ordering.map(row => row.scene_id);
|
|
444
|
-
const rows =
|
|
605
|
+
const rows = loadBundleSceneRowsWithTags(dbHandle, plan.resolved_scope.project_id, sceneIds);
|
|
445
606
|
const recipientName = plan.resolved_scope?.options?.recipient_name;
|
|
446
607
|
const recipientDisplayName = normalizeRecipientDisplayName(recipientName);
|
|
447
608
|
const footerRecipientDisplayName = sanitizeFooterRecipientDisplayName(recipientDisplayName);
|
|
@@ -494,7 +655,7 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
494
655
|
doc.restore();
|
|
495
656
|
// Restore prose style so auto-flowed text keeps consistent typography
|
|
496
657
|
// on pages added during long text rendering.
|
|
497
|
-
doc.font(bodyFont).fontSize(proseFontSize);
|
|
658
|
+
doc.font(bodyFont).fontSize(proseFontSize).fillColor("#000000");
|
|
498
659
|
doc.x = previousX;
|
|
499
660
|
doc.y = previousY;
|
|
500
661
|
};
|
|
@@ -530,8 +691,9 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
530
691
|
|
|
531
692
|
try {
|
|
532
693
|
doc.addPage();
|
|
533
|
-
|
|
534
|
-
doc.
|
|
694
|
+
const coverLabel = `Review Bundle: ${plan.resolved_scope.project_id}`;
|
|
695
|
+
doc.fontSize(isBetaProfile ? 11 : 24).font(coverHeadingFont).text(coverLabel, { align: "left" });
|
|
696
|
+
doc.moveDown(isBetaProfile ? 0.2 : 0.5);
|
|
535
697
|
doc.fontSize(11).font(bodyFont);
|
|
536
698
|
if (profile !== "beta_reader_personalized") {
|
|
537
699
|
doc.text(`Profile: ${profile}`, { align: "left" });
|
|
@@ -556,19 +718,31 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
556
718
|
doc.moveDown();
|
|
557
719
|
}
|
|
558
720
|
|
|
559
|
-
for (
|
|
721
|
+
for (let sceneIndex = 0; sceneIndex < rows.length; sceneIndex += 1) {
|
|
722
|
+
const scene = rows[sceneIndex];
|
|
723
|
+
const prevScene = sceneIndex > 0 ? rows[sceneIndex - 1] : null;
|
|
560
724
|
if (isBetaProfile) {
|
|
561
725
|
// Give chapter titles generous vertical breathing room for a
|
|
562
726
|
// print-like opening feel before prose begins.
|
|
563
727
|
doc.moveDown(2.0);
|
|
564
728
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
729
|
+
if (isBetaProfile && scene.chapter_title && (!prevScene || prevScene.chapter !== scene.chapter)) {
|
|
730
|
+
doc.fontSize(16).font(coverHeadingFont);
|
|
731
|
+
doc.text(scene.chapter_title, { align: "center" });
|
|
732
|
+
doc.moveDown(1.0);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Skip title rendering for epigraphs in beta profile
|
|
736
|
+
const isEpigraph = isBetaProfile && scene.tags?.includes("epigraph");
|
|
737
|
+
if (!isEpigraph) {
|
|
738
|
+
doc.fontSize(isBetaProfile ? 13 : 14).font(sceneHeadingFont);
|
|
739
|
+
let heading = scene.title || scene.scene_id;
|
|
740
|
+
if (includeSceneIds) {
|
|
741
|
+
heading += ` [${scene.scene_id}]`;
|
|
742
|
+
}
|
|
743
|
+
doc.text(heading, { align: isBetaProfile ? "center" : "left" });
|
|
744
|
+
doc.moveDown(isBetaProfile ? 1.6 : 0.2);
|
|
569
745
|
}
|
|
570
|
-
doc.text(heading, { align: isBetaProfile ? "center" : "left" });
|
|
571
|
-
doc.moveDown(isBetaProfile ? 1.6 : 0.2);
|
|
572
746
|
|
|
573
747
|
const metaParts = [];
|
|
574
748
|
if (profile !== "beta_reader_personalized") {
|
|
@@ -604,14 +778,35 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
604
778
|
}
|
|
605
779
|
);
|
|
606
780
|
}
|
|
607
|
-
|
|
781
|
+
let sceneDateline = null;
|
|
782
|
+
if (isBetaProfile) {
|
|
783
|
+
prose = normalizeBetaProseFlow(resolved);
|
|
784
|
+
const extracted = extractSceneDateline(prose);
|
|
785
|
+
sceneDateline = extracted.dateline ? normalizeBetaTypography(extracted.dateline) : null;
|
|
786
|
+
prose = normalizeBetaTypography(extracted.body);
|
|
787
|
+
} else {
|
|
788
|
+
prose = normalizeHardWrappedProse(resolved);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (sceneDateline) {
|
|
792
|
+
doc.fontSize(10).font(metaFont);
|
|
793
|
+
doc.text(sceneDateline, {
|
|
794
|
+
align: "center",
|
|
795
|
+
width: doc.page.width - doc.page.margins.left - doc.page.margins.right,
|
|
796
|
+
});
|
|
797
|
+
doc.moveDown(1.0);
|
|
798
|
+
}
|
|
608
799
|
|
|
609
800
|
const textWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
|
610
|
-
doc
|
|
611
|
-
|
|
801
|
+
renderProseWithInlineEmphasis(doc, prose, {
|
|
802
|
+
bodyFont,
|
|
803
|
+
italicFont,
|
|
804
|
+
fontSize: proseFontSize,
|
|
612
805
|
align: "left",
|
|
613
806
|
width: textWidth,
|
|
614
807
|
lineGap: proseLineGap,
|
|
808
|
+
paragraphGap: 0,
|
|
809
|
+
blankLineMoveDown: isBetaProfile ? 0.15 : 0.65,
|
|
615
810
|
});
|
|
616
811
|
}
|
|
617
812
|
|
|
@@ -619,7 +814,7 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
619
814
|
// Add page break between scenes only for prose-including profiles where
|
|
620
815
|
// clear scene separation matters. For outline_discussion, let content flow.
|
|
621
816
|
const includesProse = profile === "editor_detailed" || profile === "beta_reader_personalized";
|
|
622
|
-
if (includesProse &&
|
|
817
|
+
if (includesProse && sceneIndex < rows.length - 1) {
|
|
623
818
|
doc.addPage();
|
|
624
819
|
}
|
|
625
820
|
}
|
|
@@ -642,4 +837,5 @@ export {
|
|
|
642
837
|
buildPageFingerprintToken,
|
|
643
838
|
buildFingerprintSeed,
|
|
644
839
|
buildFingerprintSeedHash,
|
|
840
|
+
extractSceneDateline,
|
|
645
841
|
};
|
package/src/sync/importer.js
CHANGED
|
@@ -358,12 +358,7 @@ export function importScrivenerSync({
|
|
|
358
358
|
continue;
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
-
|
|
362
|
-
logger(` SKIP (epigraph) ${filename}`);
|
|
363
|
-
skipped++;
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
|
|
361
|
+
const isEpigraphScene = isEpigraph(rawTitle);
|
|
367
362
|
const title = cleanTitle(rawTitle);
|
|
368
363
|
const existingScene = existingScenes.get(String(binderId)) ?? null;
|
|
369
364
|
const sceneId = existingScene?.meta?.scene_id ?? makeSceneId(binderId, title);
|
|
@@ -375,6 +370,10 @@ export function importScrivenerSync({
|
|
|
375
370
|
const destFile = path.join(targetDir, `${seq.toString().padStart(3, "0")} ${rawTitle} [${binderId}].${ext}`);
|
|
376
371
|
const sidecar = destFile.replace(/\.(txt|md)$/, ".meta.yaml");
|
|
377
372
|
|
|
373
|
+
const existingTags = Array.isArray(existingScene?.meta?.tags)
|
|
374
|
+
? existingScene.meta.tags
|
|
375
|
+
: [];
|
|
376
|
+
|
|
378
377
|
const meta = {
|
|
379
378
|
...(existingScene?.meta ?? {}),
|
|
380
379
|
scene_id: sceneId,
|
|
@@ -383,6 +382,7 @@ export function importScrivenerSync({
|
|
|
383
382
|
title,
|
|
384
383
|
timeline_position: seq,
|
|
385
384
|
...(beatCarry ? { save_the_cat_beat: beatCarry } : {}),
|
|
385
|
+
...(isEpigraphScene ? { tags: [...existingTags, "epigraph"].filter((v, i, a) => a.indexOf(v) === i) } : {}),
|
|
386
386
|
};
|
|
387
387
|
|
|
388
388
|
if (!beatCarry && existingScene?.meta && Object.hasOwn(existingScene.meta, "save_the_cat_beat")) {
|
|
@@ -424,7 +424,7 @@ export function importScrivenerSync({
|
|
|
424
424
|
logger("");
|
|
425
425
|
logger(`${"-".repeat(50)}`);
|
|
426
426
|
logger(`Created: ${created} sidecars${dryRun ? " (dry run)" : ""}`);
|
|
427
|
-
logger(`Skipped: ${skipped} (empty /
|
|
427
|
+
logger(`Skipped: ${skipped} (empty / pattern)`);
|
|
428
428
|
if (existing) logger(`Existing: ${existing} already had sidecars`);
|
|
429
429
|
logger(`Beat markers seen: ${beatMarkersSeen}`);
|
|
430
430
|
|