@hanna84/mcp-writing 3.5.0 → 3.5.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/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.2](https://github.com/hannasdev/mcp-writing/compare/v3.5.1...v3.5.2)
|
|
8
|
+
|
|
9
|
+
- Improve beta prose flow and centered scene dateline [`#184`](https://github.com/hannasdev/mcp-writing/pull/184)
|
|
10
|
+
|
|
11
|
+
#### [v3.5.1](https://github.com/hannasdev/mcp-writing/compare/v3.5.0...v3.5.1)
|
|
12
|
+
|
|
13
|
+
> 8 May 2026
|
|
14
|
+
|
|
15
|
+
- Fix beta accountability follow-ups and test coverage [`#183`](https://github.com/hannasdev/mcp-writing/pull/183)
|
|
16
|
+
- Release 3.5.1 [`28d77f7`](https://github.com/hannasdev/mcp-writing/commit/28d77f755708dc8e987caa69dfcb6f3c5408b916)
|
|
17
|
+
|
|
7
18
|
#### [v3.5.0](https://github.com/hannasdev/mcp-writing/compare/v3.4.4...v3.5.0)
|
|
8
19
|
|
|
20
|
+
> 8 May 2026
|
|
21
|
+
|
|
9
22
|
- feat: add accountable beta-reader bundle fingerprinting [`#182`](https://github.com/hannasdev/mcp-writing/pull/182)
|
|
23
|
+
- Release 3.5.0 [`6134dfc`](https://github.com/hannasdev/mcp-writing/commit/6134dfc17e998c118f5fdbb68f1a26e3ba673f05)
|
|
10
24
|
|
|
11
25
|
#### [v3.4.4](https://github.com/hannasdev/mcp-writing/compare/v3.4.3...v3.4.4)
|
|
12
26
|
|
package/package.json
CHANGED
|
@@ -335,16 +335,23 @@ export function buildReviewBundlePlan(dbHandle, {
|
|
|
335
335
|
: undefined;
|
|
336
336
|
|
|
337
337
|
const safeBundleName = slugifyBundleName(bundle_name || `${project_id}-${profile}`);
|
|
338
|
+
const normalizedSceneIds = Array.isArray(scene_ids)
|
|
339
|
+
? Array.from(new Set(scene_ids.map(sceneId => String(sceneId)))).sort()
|
|
340
|
+
: undefined;
|
|
338
341
|
const appliedFilters = {
|
|
339
342
|
...(part !== undefined ? { part } : {}),
|
|
340
343
|
...(chapter !== undefined ? { chapter } : {}),
|
|
341
344
|
...(Array.isArray(normalizedChapters) ? { chapters: normalizedChapters } : {}),
|
|
342
345
|
...(tag ? { tag } : {}),
|
|
343
|
-
...(Array.isArray(
|
|
346
|
+
...(Array.isArray(normalizedSceneIds) ? { scene_ids: normalizedSceneIds } : {}),
|
|
344
347
|
};
|
|
345
348
|
const resolvedBetaAccountability = profile === "beta_reader_personalized"
|
|
346
349
|
? Boolean(beta_accountability ?? true)
|
|
347
350
|
: false;
|
|
351
|
+
const isBetaProfile = profile === "beta_reader_personalized";
|
|
352
|
+
const resolvedIncludeSceneIds = isBetaProfile ? false : Boolean(include_scene_ids);
|
|
353
|
+
const resolvedIncludeMetadataSidebar = isBetaProfile ? false : Boolean(include_metadata_sidebar);
|
|
354
|
+
const resolvedIncludeParagraphAnchors = isBetaProfile ? false : Boolean(include_paragraph_anchors);
|
|
348
355
|
|
|
349
356
|
return {
|
|
350
357
|
ok: true,
|
|
@@ -353,9 +360,9 @@ export function buildReviewBundlePlan(dbHandle, {
|
|
|
353
360
|
project_id,
|
|
354
361
|
filters: appliedFilters,
|
|
355
362
|
options: {
|
|
356
|
-
include_scene_ids:
|
|
357
|
-
include_metadata_sidebar:
|
|
358
|
-
include_paragraph_anchors:
|
|
363
|
+
include_scene_ids: resolvedIncludeSceneIds,
|
|
364
|
+
include_metadata_sidebar: resolvedIncludeMetadataSidebar,
|
|
365
|
+
include_paragraph_anchors: resolvedIncludeParagraphAnchors,
|
|
359
366
|
beta_accountability: resolvedBetaAccountability,
|
|
360
367
|
...(resolvedRecipientName ? { recipient_name: resolvedRecipientName } : {}),
|
|
361
368
|
},
|
|
@@ -233,6 +233,122 @@ function readProse(filePath, { syncDir } = {}) {
|
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
function normalizeHardWrappedProse(rawProse) {
|
|
237
|
+
const prose = String(rawProse ?? "").replace(/\r\n?/g, "\n").trim();
|
|
238
|
+
if (!prose) return "";
|
|
239
|
+
const paragraphs = prose
|
|
240
|
+
.split(/\n\s*\n/g)
|
|
241
|
+
.map(paragraph => paragraph.replace(/\s*\n\s*/g, " ").trim())
|
|
242
|
+
.filter(Boolean);
|
|
243
|
+
return paragraphs.join("\n\n");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function extractSceneDateline(prose) {
|
|
247
|
+
const normalized = String(prose ?? "").replace(/\r\n?/g, "\n").trim();
|
|
248
|
+
if (!normalized) {
|
|
249
|
+
return { dateline: null, body: "" };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const lines = normalized
|
|
253
|
+
.split("\n")
|
|
254
|
+
.map(line => line.trim())
|
|
255
|
+
.filter(Boolean);
|
|
256
|
+
if (lines.length === 0) {
|
|
257
|
+
return { dateline: null, body: "" };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const firstParagraph = lines[0];
|
|
261
|
+
const dashMatch = firstParagraph.match(/^(.+?)\s*[–-]\s*(.+)$/);
|
|
262
|
+
const left = dashMatch?.[1]?.trim() ?? "";
|
|
263
|
+
const right = dashMatch?.[2]?.trim() ?? "";
|
|
264
|
+
const totalWords = firstParagraph.split(/\s+/).filter(Boolean).length;
|
|
265
|
+
const looksLikeDateline = (
|
|
266
|
+
firstParagraph.length >= 6
|
|
267
|
+
&& firstParagraph.length <= 90
|
|
268
|
+
&& Boolean(dashMatch)
|
|
269
|
+
&& left.length >= 2
|
|
270
|
+
&& right.length >= 2
|
|
271
|
+
&& totalWords <= 14
|
|
272
|
+
&& !/[!?]/.test(firstParagraph)
|
|
273
|
+
&& !/[“”"']/.test(firstParagraph)
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
if (!looksLikeDateline) {
|
|
277
|
+
return { dateline: null, body: normalized };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
dateline: firstParagraph,
|
|
282
|
+
body: lines.slice(1).join("\n"),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function normalizeBetaProseFlow(prose) {
|
|
287
|
+
const normalized = String(prose ?? "").replace(/\r\n?/g, "\n").trim();
|
|
288
|
+
if (!normalized) return "";
|
|
289
|
+
const paragraphs = normalized
|
|
290
|
+
.split(/\n\s*\n/g)
|
|
291
|
+
.map(paragraph => paragraph
|
|
292
|
+
.split("\n")
|
|
293
|
+
.map(line => line.trim())
|
|
294
|
+
.filter(Boolean)
|
|
295
|
+
.join("\n"))
|
|
296
|
+
.filter(Boolean);
|
|
297
|
+
// For beta exports, convert paragraph blocks into regular line breaks so the
|
|
298
|
+
// reading flow stays continuous without large section gaps.
|
|
299
|
+
return paragraphs.join("\n");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function normalizeBetaTypography(prose) {
|
|
303
|
+
return String(prose ?? "")
|
|
304
|
+
.replace(/(^|\s)--(\s|$)/g, "$1—$2");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function renderProseWithInlineEmphasis(doc, prose, {
|
|
308
|
+
bodyFont,
|
|
309
|
+
italicFont,
|
|
310
|
+
fontSize,
|
|
311
|
+
width,
|
|
312
|
+
align = "left",
|
|
313
|
+
lineGap = 0,
|
|
314
|
+
paragraphGap = 0,
|
|
315
|
+
blankLineMoveDown = 0.15,
|
|
316
|
+
}) {
|
|
317
|
+
const lines = String(prose ?? "").split("\n");
|
|
318
|
+
for (const line of lines) {
|
|
319
|
+
if (line.length === 0) {
|
|
320
|
+
doc.moveDown(blankLineMoveDown);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (line.trim() === "***") {
|
|
325
|
+
doc.moveDown(0.5);
|
|
326
|
+
doc.fontSize(fontSize).font(bodyFont);
|
|
327
|
+
doc.text("***", { align: "center", width, lineGap, paragraphGap: 0 });
|
|
328
|
+
doc.moveDown(0.5);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const segments = line.split(/(\*[^*\n]+\*)/g).filter(Boolean);
|
|
333
|
+
|
|
334
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
335
|
+
const segment = segments[index];
|
|
336
|
+
const isItalic = /^\*[^*\n]+\*$/.test(segment);
|
|
337
|
+
const text = isItalic ? segment.slice(1, -1) : segment;
|
|
338
|
+
if (!text) continue;
|
|
339
|
+
doc.fontSize(fontSize).font(isItalic ? italicFont : bodyFont);
|
|
340
|
+
doc.text(text, {
|
|
341
|
+
align,
|
|
342
|
+
width,
|
|
343
|
+
lineGap,
|
|
344
|
+
paragraphGap,
|
|
345
|
+
continued: index < segments.length - 1,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
doc.font(bodyFont).fontSize(fontSize);
|
|
350
|
+
}
|
|
351
|
+
|
|
236
352
|
function renderSceneBlock(scene, options) {
|
|
237
353
|
const {
|
|
238
354
|
profile,
|
|
@@ -278,11 +394,11 @@ function renderSceneBlock(scene, options) {
|
|
|
278
394
|
|
|
279
395
|
const prose = scene.prose ?? "";
|
|
280
396
|
if (!includeParagraphAnchors || prose.length === 0) {
|
|
281
|
-
parts.push(prose);
|
|
397
|
+
parts.push(normalizeHardWrappedProse(prose));
|
|
282
398
|
return parts.join("\n\n");
|
|
283
399
|
}
|
|
284
400
|
|
|
285
|
-
const paragraphs = prose
|
|
401
|
+
const paragraphs = normalizeHardWrappedProse(prose)
|
|
286
402
|
.split(/\n\s*\n/g)
|
|
287
403
|
.map(p => p.trim())
|
|
288
404
|
.filter(Boolean);
|
|
@@ -346,11 +462,16 @@ function buildPageFingerprintToken({ seedHash, pageNumber }) {
|
|
|
346
462
|
return `BR-${digest}-P${String(pageNumber).padStart(3, "0")}`;
|
|
347
463
|
}
|
|
348
464
|
|
|
465
|
+
function sanitizeFooterRecipientDisplayName(recipientDisplayName) {
|
|
466
|
+
return String(recipientDisplayName ?? "").replaceAll("|", "/");
|
|
467
|
+
}
|
|
468
|
+
|
|
349
469
|
export function renderReviewBundleMarkdown(dbHandle, plan, { generatedAt, syncDir: syncDirOpt } = {}) {
|
|
350
470
|
const profile = plan.profile;
|
|
351
|
-
const
|
|
352
|
-
const
|
|
353
|
-
const
|
|
471
|
+
const isBetaProfile = profile === "beta_reader_personalized";
|
|
472
|
+
const includeSceneIds = isBetaProfile ? false : Boolean(plan.resolved_scope?.options?.include_scene_ids);
|
|
473
|
+
const includeMetadataSidebar = isBetaProfile ? false : Boolean(plan.resolved_scope?.options?.include_metadata_sidebar);
|
|
474
|
+
const includeParagraphAnchors = isBetaProfile ? false : Boolean(plan.resolved_scope?.options?.include_paragraph_anchors);
|
|
354
475
|
// Prefer explicitly threaded syncDir; fall back to env.
|
|
355
476
|
// No further fallback: if syncDir is null, resolveSceneFilePath returns null
|
|
356
477
|
// and SCENE_PROSE_READ_FAILED is thrown, making misconfiguration explicit.
|
|
@@ -365,12 +486,13 @@ export function renderReviewBundleMarkdown(dbHandle, plan, { generatedAt, syncDi
|
|
|
365
486
|
const headerLines = [
|
|
366
487
|
`# Review Bundle: ${escapeMarkdown(plan.resolved_scope.project_id)}`,
|
|
367
488
|
"",
|
|
368
|
-
`- Profile: ${profile}
|
|
489
|
+
...(profile !== "beta_reader_personalized" ? [`- Profile: ${profile}`] : []),
|
|
369
490
|
...(profile === "beta_reader_personalized"
|
|
370
491
|
? [`- Recipient: ${escapeMarkdown(recipientDisplayName)}`]
|
|
371
492
|
: []),
|
|
372
|
-
|
|
373
|
-
|
|
493
|
+
...(profile !== "beta_reader_personalized"
|
|
494
|
+
? [`- Generated at: ${generatedAt ?? new Date().toISOString()}`, `- Scene count: ${plan.summary.scene_count}`]
|
|
495
|
+
: []),
|
|
374
496
|
];
|
|
375
497
|
sections.push(headerLines.join("\n"));
|
|
376
498
|
|
|
@@ -421,13 +543,25 @@ export function renderReviewBundlePdf(dbHandle, plan, { generatedAt, syncDir: sy
|
|
|
421
543
|
|
|
422
544
|
export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt, syncDir: syncDirOpt } = {}) {
|
|
423
545
|
const profile = plan.profile;
|
|
424
|
-
const includeSceneIds =
|
|
546
|
+
const includeSceneIds = profile === "beta_reader_personalized"
|
|
547
|
+
? false
|
|
548
|
+
: Boolean(plan.resolved_scope?.options?.include_scene_ids);
|
|
425
549
|
const syncDir = syncDirOpt ?? process.env.WRITING_SYNC_DIR ?? null;
|
|
550
|
+
const isBetaProfile = profile === "beta_reader_personalized";
|
|
551
|
+
const proseFontSize = isBetaProfile ? 8 : 10;
|
|
552
|
+
const proseLineGap = isBetaProfile ? 1.6 : 3;
|
|
553
|
+
const bodyFont = profile === "beta_reader_personalized" ? "Times-Roman" : "Helvetica";
|
|
554
|
+
const coverHeadingFont = profile === "beta_reader_personalized" ? "Times-Bold" : "Helvetica-Bold";
|
|
555
|
+
// Beta scene headings intentionally use body font (non-bold) per product direction.
|
|
556
|
+
const sceneHeadingFont = isBetaProfile ? bodyFont : coverHeadingFont;
|
|
557
|
+
const italicFont = profile === "beta_reader_personalized" ? "Times-Italic" : "Helvetica-Oblique";
|
|
558
|
+
const metaFont = italicFont;
|
|
426
559
|
|
|
427
560
|
const sceneIds = plan.ordering.map(row => row.scene_id);
|
|
428
561
|
const rows = loadBundleSceneRows(dbHandle, plan.resolved_scope.project_id, sceneIds);
|
|
429
562
|
const recipientName = plan.resolved_scope?.options?.recipient_name;
|
|
430
563
|
const recipientDisplayName = normalizeRecipientDisplayName(recipientName);
|
|
564
|
+
const footerRecipientDisplayName = sanitizeFooterRecipientDisplayName(recipientDisplayName);
|
|
431
565
|
const betaAccountabilityEnabled = profile === "beta_reader_personalized"
|
|
432
566
|
&& Boolean(plan.resolved_scope?.options?.beta_accountability);
|
|
433
567
|
const effectiveGeneratedAt = generatedAt ?? new Date().toISOString();
|
|
@@ -441,7 +575,8 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
441
575
|
const pdfOptions = profile === "beta_reader_personalized"
|
|
442
576
|
? {
|
|
443
577
|
size: [432, 648], // 6x9in in PDF points
|
|
444
|
-
|
|
578
|
+
// Extra bottom margin reserves clear space above the accountability footer.
|
|
579
|
+
margins: { top: 64, right: 58, bottom: 96, left: 58 },
|
|
445
580
|
autoFirstPage: false,
|
|
446
581
|
}
|
|
447
582
|
: {
|
|
@@ -455,23 +590,30 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
455
590
|
|
|
456
591
|
const drawAccountabilityFooter = () => {
|
|
457
592
|
if (!betaAccountabilityEnabled || !fingerprintSeedHash) return;
|
|
593
|
+
const previousX = doc.x;
|
|
594
|
+
const previousY = doc.y;
|
|
458
595
|
pageNumber += 1;
|
|
459
596
|
const token = buildPageFingerprintToken({
|
|
460
597
|
seedHash: fingerprintSeedHash,
|
|
461
598
|
pageNumber,
|
|
462
599
|
});
|
|
463
600
|
pageTokens.push({ page: pageNumber, token });
|
|
464
|
-
const footerY = doc.page.height -
|
|
465
|
-
const
|
|
466
|
-
const
|
|
601
|
+
const footerY = doc.page.height - 42;
|
|
602
|
+
const footerText = `For: ${footerRecipientDisplayName} | Fingerprint: ${token}`;
|
|
603
|
+
const pageNumberText = String(pageNumber);
|
|
467
604
|
doc.save();
|
|
468
|
-
doc.font("
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
});
|
|
605
|
+
doc.font("Times-Roman").fontSize(8).fillColor("#555555");
|
|
606
|
+
// Draw footer in no-wrap mode to avoid layout flow side effects.
|
|
607
|
+
doc.text(footerText, doc.page.margins.left, footerY, { lineBreak: false });
|
|
608
|
+
const pageNumberWidth = doc.widthOfString(pageNumberText);
|
|
609
|
+
const pageNumberX = (doc.page.width - pageNumberWidth) / 2;
|
|
610
|
+
doc.text(pageNumberText, pageNumberX, doc.page.height - 24, { lineBreak: false });
|
|
474
611
|
doc.restore();
|
|
612
|
+
// Restore prose style so auto-flowed text keeps consistent typography
|
|
613
|
+
// on pages added during long text rendering.
|
|
614
|
+
doc.font(bodyFont).fontSize(proseFontSize).fillColor("#000000");
|
|
615
|
+
doc.x = previousX;
|
|
616
|
+
doc.y = previousY;
|
|
475
617
|
};
|
|
476
618
|
doc.on("pageAdded", drawAccountabilityFooter);
|
|
477
619
|
|
|
@@ -505,22 +647,26 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
505
647
|
|
|
506
648
|
try {
|
|
507
649
|
doc.addPage();
|
|
508
|
-
|
|
509
|
-
doc.
|
|
510
|
-
doc.
|
|
511
|
-
doc.
|
|
650
|
+
const coverLabel = `Review Bundle: ${plan.resolved_scope.project_id}`;
|
|
651
|
+
doc.fontSize(isBetaProfile ? 11 : 24).font(coverHeadingFont).text(coverLabel, { align: "left" });
|
|
652
|
+
doc.moveDown(isBetaProfile ? 0.2 : 0.5);
|
|
653
|
+
doc.fontSize(11).font(bodyFont);
|
|
654
|
+
if (profile !== "beta_reader_personalized") {
|
|
655
|
+
doc.text(`Profile: ${profile}`, { align: "left" });
|
|
656
|
+
}
|
|
512
657
|
if (profile === "beta_reader_personalized") {
|
|
513
658
|
doc.text(`Recipient: ${recipientDisplayName}`, { align: "left" });
|
|
659
|
+
} else {
|
|
660
|
+
doc.text(`Generated: ${effectiveGeneratedAt}`, { align: "left" });
|
|
661
|
+
doc.text(`Scenes: ${plan.summary.scene_count}`, { align: "left" });
|
|
514
662
|
}
|
|
515
|
-
doc.text(`Generated: ${effectiveGeneratedAt}`, { align: "left" });
|
|
516
|
-
doc.text(`Scenes: ${plan.summary.scene_count}`, { align: "left" });
|
|
517
663
|
doc.moveDown();
|
|
518
664
|
|
|
519
665
|
if (profile === "beta_reader_personalized") {
|
|
520
|
-
doc.fontSize(12).font("
|
|
666
|
+
doc.fontSize(12).font("Times-Bold").text("Usage Notice", { align: "left" });
|
|
521
667
|
doc.moveDown(0.3);
|
|
522
668
|
const noticeWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
|
523
|
-
doc.fontSize(10).font("
|
|
669
|
+
doc.fontSize(10).font("Times-Roman");
|
|
524
670
|
doc.text("This beta-reader draft is intended for private review and feedback. Please do not redistribute without explicit author permission.", {
|
|
525
671
|
align: "left",
|
|
526
672
|
width: noticeWidth,
|
|
@@ -529,22 +675,29 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
529
675
|
}
|
|
530
676
|
|
|
531
677
|
for (const scene of rows) {
|
|
532
|
-
|
|
678
|
+
if (isBetaProfile) {
|
|
679
|
+
// Give chapter titles generous vertical breathing room for a
|
|
680
|
+
// print-like opening feel before prose begins.
|
|
681
|
+
doc.moveDown(2.0);
|
|
682
|
+
}
|
|
683
|
+
doc.fontSize(isBetaProfile ? 13 : 14).font(sceneHeadingFont);
|
|
533
684
|
let heading = scene.title || scene.scene_id;
|
|
534
685
|
if (includeSceneIds) {
|
|
535
686
|
heading += ` [${scene.scene_id}]`;
|
|
536
687
|
}
|
|
537
|
-
doc.text(heading, { align: "left" });
|
|
538
|
-
doc.moveDown(0.2);
|
|
688
|
+
doc.text(heading, { align: isBetaProfile ? "center" : "left" });
|
|
689
|
+
doc.moveDown(isBetaProfile ? 1.6 : 0.2);
|
|
539
690
|
|
|
540
691
|
const metaParts = [];
|
|
541
|
-
if (
|
|
542
|
-
|
|
692
|
+
if (profile !== "beta_reader_personalized") {
|
|
693
|
+
if (scene.pov) metaParts.push(`POV: ${scene.pov}`);
|
|
694
|
+
if (scene.save_the_cat_beat) metaParts.push(`Beat: ${scene.save_the_cat_beat}`);
|
|
695
|
+
}
|
|
543
696
|
if (metaParts.length > 0) {
|
|
544
697
|
const metaWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
|
545
|
-
doc.fontSize(9).font(
|
|
698
|
+
doc.fontSize(9).font(metaFont);
|
|
546
699
|
doc.text(metaParts.join(" • "), { align: "left", width: metaWidth });
|
|
547
|
-
doc.font(
|
|
700
|
+
doc.font(bodyFont);
|
|
548
701
|
doc.moveDown(0.2);
|
|
549
702
|
}
|
|
550
703
|
|
|
@@ -569,14 +722,35 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
569
722
|
}
|
|
570
723
|
);
|
|
571
724
|
}
|
|
572
|
-
|
|
725
|
+
let sceneDateline = null;
|
|
726
|
+
if (isBetaProfile) {
|
|
727
|
+
prose = normalizeBetaProseFlow(resolved);
|
|
728
|
+
const extracted = extractSceneDateline(prose);
|
|
729
|
+
sceneDateline = extracted.dateline ? normalizeBetaTypography(extracted.dateline) : null;
|
|
730
|
+
prose = normalizeBetaTypography(extracted.body);
|
|
731
|
+
} else {
|
|
732
|
+
prose = normalizeHardWrappedProse(resolved);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (sceneDateline) {
|
|
736
|
+
doc.fontSize(10).font(metaFont);
|
|
737
|
+
doc.text(sceneDateline, {
|
|
738
|
+
align: "center",
|
|
739
|
+
width: doc.page.width - doc.page.margins.left - doc.page.margins.right,
|
|
740
|
+
});
|
|
741
|
+
doc.moveDown(1.0);
|
|
742
|
+
}
|
|
573
743
|
|
|
574
744
|
const textWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
|
575
|
-
doc
|
|
576
|
-
|
|
745
|
+
renderProseWithInlineEmphasis(doc, prose, {
|
|
746
|
+
bodyFont,
|
|
747
|
+
italicFont,
|
|
748
|
+
fontSize: proseFontSize,
|
|
577
749
|
align: "left",
|
|
578
750
|
width: textWidth,
|
|
579
|
-
lineGap:
|
|
751
|
+
lineGap: proseLineGap,
|
|
752
|
+
paragraphGap: 0,
|
|
753
|
+
blankLineMoveDown: isBetaProfile ? 0.15 : 0.65,
|
|
580
754
|
});
|
|
581
755
|
}
|
|
582
756
|
|
|
@@ -607,4 +781,5 @@ export {
|
|
|
607
781
|
buildPageFingerprintToken,
|
|
608
782
|
buildFingerprintSeed,
|
|
609
783
|
buildFingerprintSeedHash,
|
|
784
|
+
extractSceneDateline,
|
|
610
785
|
};
|