@hanna84/mcp-writing 3.5.0 → 3.5.1
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,16 @@ 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.1](https://github.com/hannasdev/mcp-writing/compare/v3.5.0...v3.5.1)
|
|
8
|
+
|
|
9
|
+
- Fix beta accountability follow-ups and test coverage [`#183`](https://github.com/hannasdev/mcp-writing/pull/183)
|
|
10
|
+
|
|
7
11
|
#### [v3.5.0](https://github.com/hannasdev/mcp-writing/compare/v3.4.4...v3.5.0)
|
|
8
12
|
|
|
13
|
+
> 8 May 2026
|
|
14
|
+
|
|
9
15
|
- feat: add accountable beta-reader bundle fingerprinting [`#182`](https://github.com/hannasdev/mcp-writing/pull/182)
|
|
16
|
+
- Release 3.5.0 [`6134dfc`](https://github.com/hannasdev/mcp-writing/commit/6134dfc17e998c118f5fdbb68f1a26e3ba673f05)
|
|
10
17
|
|
|
11
18
|
#### [v3.4.4](https://github.com/hannasdev/mcp-writing/compare/v3.4.3...v3.4.4)
|
|
12
19
|
|
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
|
},
|
|
@@ -346,11 +346,16 @@ function buildPageFingerprintToken({ seedHash, pageNumber }) {
|
|
|
346
346
|
return `BR-${digest}-P${String(pageNumber).padStart(3, "0")}`;
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
function sanitizeFooterRecipientDisplayName(recipientDisplayName) {
|
|
350
|
+
return String(recipientDisplayName ?? "").replaceAll("|", "/");
|
|
351
|
+
}
|
|
352
|
+
|
|
349
353
|
export function renderReviewBundleMarkdown(dbHandle, plan, { generatedAt, syncDir: syncDirOpt } = {}) {
|
|
350
354
|
const profile = plan.profile;
|
|
351
|
-
const
|
|
352
|
-
const
|
|
353
|
-
const
|
|
355
|
+
const isBetaProfile = profile === "beta_reader_personalized";
|
|
356
|
+
const includeSceneIds = isBetaProfile ? false : Boolean(plan.resolved_scope?.options?.include_scene_ids);
|
|
357
|
+
const includeMetadataSidebar = isBetaProfile ? false : Boolean(plan.resolved_scope?.options?.include_metadata_sidebar);
|
|
358
|
+
const includeParagraphAnchors = isBetaProfile ? false : Boolean(plan.resolved_scope?.options?.include_paragraph_anchors);
|
|
354
359
|
// Prefer explicitly threaded syncDir; fall back to env.
|
|
355
360
|
// No further fallback: if syncDir is null, resolveSceneFilePath returns null
|
|
356
361
|
// and SCENE_PROSE_READ_FAILED is thrown, making misconfiguration explicit.
|
|
@@ -365,12 +370,13 @@ export function renderReviewBundleMarkdown(dbHandle, plan, { generatedAt, syncDi
|
|
|
365
370
|
const headerLines = [
|
|
366
371
|
`# Review Bundle: ${escapeMarkdown(plan.resolved_scope.project_id)}`,
|
|
367
372
|
"",
|
|
368
|
-
`- Profile: ${profile}
|
|
373
|
+
...(profile !== "beta_reader_personalized" ? [`- Profile: ${profile}`] : []),
|
|
369
374
|
...(profile === "beta_reader_personalized"
|
|
370
375
|
? [`- Recipient: ${escapeMarkdown(recipientDisplayName)}`]
|
|
371
376
|
: []),
|
|
372
|
-
|
|
373
|
-
|
|
377
|
+
...(profile !== "beta_reader_personalized"
|
|
378
|
+
? [`- Generated at: ${generatedAt ?? new Date().toISOString()}`, `- Scene count: ${plan.summary.scene_count}`]
|
|
379
|
+
: []),
|
|
374
380
|
];
|
|
375
381
|
sections.push(headerLines.join("\n"));
|
|
376
382
|
|
|
@@ -421,13 +427,24 @@ export function renderReviewBundlePdf(dbHandle, plan, { generatedAt, syncDir: sy
|
|
|
421
427
|
|
|
422
428
|
export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt, syncDir: syncDirOpt } = {}) {
|
|
423
429
|
const profile = plan.profile;
|
|
424
|
-
const includeSceneIds =
|
|
430
|
+
const includeSceneIds = profile === "beta_reader_personalized"
|
|
431
|
+
? false
|
|
432
|
+
: Boolean(plan.resolved_scope?.options?.include_scene_ids);
|
|
425
433
|
const syncDir = syncDirOpt ?? process.env.WRITING_SYNC_DIR ?? null;
|
|
434
|
+
const isBetaProfile = profile === "beta_reader_personalized";
|
|
435
|
+
const proseFontSize = isBetaProfile ? 8 : 10;
|
|
436
|
+
const proseLineGap = isBetaProfile ? 3.2 : 3;
|
|
437
|
+
const bodyFont = profile === "beta_reader_personalized" ? "Times-Roman" : "Helvetica";
|
|
438
|
+
const coverHeadingFont = profile === "beta_reader_personalized" ? "Times-Bold" : "Helvetica-Bold";
|
|
439
|
+
// Beta scene headings intentionally use body font (non-bold) per product direction.
|
|
440
|
+
const sceneHeadingFont = isBetaProfile ? bodyFont : coverHeadingFont;
|
|
441
|
+
const metaFont = profile === "beta_reader_personalized" ? "Times-Italic" : "Helvetica-Oblique";
|
|
426
442
|
|
|
427
443
|
const sceneIds = plan.ordering.map(row => row.scene_id);
|
|
428
444
|
const rows = loadBundleSceneRows(dbHandle, plan.resolved_scope.project_id, sceneIds);
|
|
429
445
|
const recipientName = plan.resolved_scope?.options?.recipient_name;
|
|
430
446
|
const recipientDisplayName = normalizeRecipientDisplayName(recipientName);
|
|
447
|
+
const footerRecipientDisplayName = sanitizeFooterRecipientDisplayName(recipientDisplayName);
|
|
431
448
|
const betaAccountabilityEnabled = profile === "beta_reader_personalized"
|
|
432
449
|
&& Boolean(plan.resolved_scope?.options?.beta_accountability);
|
|
433
450
|
const effectiveGeneratedAt = generatedAt ?? new Date().toISOString();
|
|
@@ -441,7 +458,8 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
441
458
|
const pdfOptions = profile === "beta_reader_personalized"
|
|
442
459
|
? {
|
|
443
460
|
size: [432, 648], // 6x9in in PDF points
|
|
444
|
-
|
|
461
|
+
// Extra bottom margin reserves clear space above the accountability footer.
|
|
462
|
+
margins: { top: 64, right: 58, bottom: 96, left: 58 },
|
|
445
463
|
autoFirstPage: false,
|
|
446
464
|
}
|
|
447
465
|
: {
|
|
@@ -455,23 +473,30 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
455
473
|
|
|
456
474
|
const drawAccountabilityFooter = () => {
|
|
457
475
|
if (!betaAccountabilityEnabled || !fingerprintSeedHash) return;
|
|
476
|
+
const previousX = doc.x;
|
|
477
|
+
const previousY = doc.y;
|
|
458
478
|
pageNumber += 1;
|
|
459
479
|
const token = buildPageFingerprintToken({
|
|
460
480
|
seedHash: fingerprintSeedHash,
|
|
461
481
|
pageNumber,
|
|
462
482
|
});
|
|
463
483
|
pageTokens.push({ page: pageNumber, token });
|
|
464
|
-
const footerY = doc.page.height -
|
|
465
|
-
const
|
|
466
|
-
const
|
|
484
|
+
const footerY = doc.page.height - 42;
|
|
485
|
+
const footerText = `For: ${footerRecipientDisplayName} | Fingerprint: ${token}`;
|
|
486
|
+
const pageNumberText = String(pageNumber);
|
|
467
487
|
doc.save();
|
|
468
|
-
doc.font("
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
});
|
|
488
|
+
doc.font("Times-Roman").fontSize(8).fillColor("#555555");
|
|
489
|
+
// Draw footer in no-wrap mode to avoid layout flow side effects.
|
|
490
|
+
doc.text(footerText, doc.page.margins.left, footerY, { lineBreak: false });
|
|
491
|
+
const pageNumberWidth = doc.widthOfString(pageNumberText);
|
|
492
|
+
const pageNumberX = (doc.page.width - pageNumberWidth) / 2;
|
|
493
|
+
doc.text(pageNumberText, pageNumberX, doc.page.height - 24, { lineBreak: false });
|
|
474
494
|
doc.restore();
|
|
495
|
+
// Restore prose style so auto-flowed text keeps consistent typography
|
|
496
|
+
// on pages added during long text rendering.
|
|
497
|
+
doc.font(bodyFont).fontSize(proseFontSize);
|
|
498
|
+
doc.x = previousX;
|
|
499
|
+
doc.y = previousY;
|
|
475
500
|
};
|
|
476
501
|
doc.on("pageAdded", drawAccountabilityFooter);
|
|
477
502
|
|
|
@@ -505,22 +530,25 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
505
530
|
|
|
506
531
|
try {
|
|
507
532
|
doc.addPage();
|
|
508
|
-
doc.fontSize(24).font(
|
|
533
|
+
doc.fontSize(24).font(coverHeadingFont).text(`Review Bundle: ${plan.resolved_scope.project_id}`, { align: "left" });
|
|
509
534
|
doc.moveDown(0.5);
|
|
510
|
-
doc.fontSize(11).font(
|
|
511
|
-
|
|
535
|
+
doc.fontSize(11).font(bodyFont);
|
|
536
|
+
if (profile !== "beta_reader_personalized") {
|
|
537
|
+
doc.text(`Profile: ${profile}`, { align: "left" });
|
|
538
|
+
}
|
|
512
539
|
if (profile === "beta_reader_personalized") {
|
|
513
540
|
doc.text(`Recipient: ${recipientDisplayName}`, { align: "left" });
|
|
541
|
+
} else {
|
|
542
|
+
doc.text(`Generated: ${effectiveGeneratedAt}`, { align: "left" });
|
|
543
|
+
doc.text(`Scenes: ${plan.summary.scene_count}`, { align: "left" });
|
|
514
544
|
}
|
|
515
|
-
doc.text(`Generated: ${effectiveGeneratedAt}`, { align: "left" });
|
|
516
|
-
doc.text(`Scenes: ${plan.summary.scene_count}`, { align: "left" });
|
|
517
545
|
doc.moveDown();
|
|
518
546
|
|
|
519
547
|
if (profile === "beta_reader_personalized") {
|
|
520
|
-
doc.fontSize(12).font("
|
|
548
|
+
doc.fontSize(12).font("Times-Bold").text("Usage Notice", { align: "left" });
|
|
521
549
|
doc.moveDown(0.3);
|
|
522
550
|
const noticeWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
|
523
|
-
doc.fontSize(10).font("
|
|
551
|
+
doc.fontSize(10).font("Times-Roman");
|
|
524
552
|
doc.text("This beta-reader draft is intended for private review and feedback. Please do not redistribute without explicit author permission.", {
|
|
525
553
|
align: "left",
|
|
526
554
|
width: noticeWidth,
|
|
@@ -529,22 +557,29 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
529
557
|
}
|
|
530
558
|
|
|
531
559
|
for (const scene of rows) {
|
|
532
|
-
|
|
560
|
+
if (isBetaProfile) {
|
|
561
|
+
// Give chapter titles generous vertical breathing room for a
|
|
562
|
+
// print-like opening feel before prose begins.
|
|
563
|
+
doc.moveDown(2.0);
|
|
564
|
+
}
|
|
565
|
+
doc.fontSize(isBetaProfile ? 13 : 14).font(sceneHeadingFont);
|
|
533
566
|
let heading = scene.title || scene.scene_id;
|
|
534
567
|
if (includeSceneIds) {
|
|
535
568
|
heading += ` [${scene.scene_id}]`;
|
|
536
569
|
}
|
|
537
|
-
doc.text(heading, { align: "left" });
|
|
538
|
-
doc.moveDown(0.2);
|
|
570
|
+
doc.text(heading, { align: isBetaProfile ? "center" : "left" });
|
|
571
|
+
doc.moveDown(isBetaProfile ? 1.6 : 0.2);
|
|
539
572
|
|
|
540
573
|
const metaParts = [];
|
|
541
|
-
if (
|
|
542
|
-
|
|
574
|
+
if (profile !== "beta_reader_personalized") {
|
|
575
|
+
if (scene.pov) metaParts.push(`POV: ${scene.pov}`);
|
|
576
|
+
if (scene.save_the_cat_beat) metaParts.push(`Beat: ${scene.save_the_cat_beat}`);
|
|
577
|
+
}
|
|
543
578
|
if (metaParts.length > 0) {
|
|
544
579
|
const metaWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
|
545
|
-
doc.fontSize(9).font(
|
|
580
|
+
doc.fontSize(9).font(metaFont);
|
|
546
581
|
doc.text(metaParts.join(" • "), { align: "left", width: metaWidth });
|
|
547
|
-
doc.font(
|
|
582
|
+
doc.font(bodyFont);
|
|
548
583
|
doc.moveDown(0.2);
|
|
549
584
|
}
|
|
550
585
|
|
|
@@ -572,11 +607,11 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
572
607
|
prose = resolved;
|
|
573
608
|
|
|
574
609
|
const textWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
|
575
|
-
doc.fontSize(
|
|
610
|
+
doc.fontSize(proseFontSize).font(bodyFont);
|
|
576
611
|
doc.text(prose, {
|
|
577
612
|
align: "left",
|
|
578
613
|
width: textWidth,
|
|
579
|
-
lineGap:
|
|
614
|
+
lineGap: proseLineGap,
|
|
580
615
|
});
|
|
581
616
|
}
|
|
582
617
|
|