@hanna84/mcp-writing 3.7.1 → 3.8.0
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 +7 -0
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/core/db.js +191 -0
- package/src/core/helpers.js +6 -1
- package/src/review-bundles/review-bundles-planner.js +11 -3
- package/src/review-bundles/review-bundles-renderer.js +86 -14
- package/src/sync/sync.js +557 -11
- package/src/tools/review-bundles.js +10 -4
- package/src/tools/search.js +225 -19
- package/src/tools/styleguide.js +7 -1
- package/src/tools/sync.js +3 -0
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.8.0](https://github.com/hannasdev/mcp-writing/compare/v3.7.1...v3.8.0)
|
|
8
|
+
|
|
9
|
+
- feat: add canonical chapter and epigraph indexing [`#193`](https://github.com/hannasdev/mcp-writing/pull/193)
|
|
10
|
+
|
|
7
11
|
#### [v3.7.1](https://github.com/hannasdev/mcp-writing/compare/v3.7.0...v3.7.1)
|
|
8
12
|
|
|
13
|
+
> 14 May 2026
|
|
14
|
+
|
|
9
15
|
- Docs/chapter epigraph migration gates [`#192`](https://github.com/hannasdev/mcp-writing/pull/192)
|
|
16
|
+
- Release 3.7.1 [`3516818`](https://github.com/hannasdev/mcp-writing/commit/3516818e333b4f6667116eb86657d07b31a9b761)
|
|
10
17
|
|
|
11
18
|
#### [v3.7.0](https://github.com/hannasdev/mcp-writing/compare/v3.6.2...v3.7.0)
|
|
12
19
|
|
package/README.md
CHANGED
|
@@ -28,9 +28,9 @@ Instead of feeding an entire manuscript to an AI and hoping it fits in the conte
|
|
|
28
28
|
|
|
29
29
|
**Current status:**
|
|
30
30
|
- **Core platform complete:** Metadata-first analysis, sidecar-backed metadata maintenance, AI-assisted prose editing with confirmation + git history, review bundles, and Scrivener Direct extraction are all implemented.
|
|
31
|
-
- **
|
|
32
|
-
- **
|
|
33
|
-
- **
|
|
31
|
+
- **Active development:** OpenClaw integration, the client-agnostic setup contract, and chapter-structure follow-up work after the first-class chapter/epigraph rollout.
|
|
32
|
+
- **Deferred backlog:** embeddings search.
|
|
33
|
+
- **Ideas and open questions:** tracked separately so future exploration does not distort the active roadmap.
|
|
34
34
|
|
|
35
35
|
## Who it is for
|
|
36
36
|
|
package/package.json
CHANGED
package/src/core/db.js
CHANGED
|
@@ -2,6 +2,19 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
2
2
|
|
|
3
3
|
const dbStartupWarnings = [];
|
|
4
4
|
|
|
5
|
+
function slugifyLegacyChapterValue(value) {
|
|
6
|
+
return String(value ?? "")
|
|
7
|
+
.toLowerCase()
|
|
8
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
9
|
+
.replace(/^-+|-+$/g, "");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatLegacyChapterId(sortIndex, title) {
|
|
13
|
+
const chapterNumber = String(sortIndex).padStart(2, "0");
|
|
14
|
+
const chapterSlug = slugifyLegacyChapterValue(title) || `chapter-${sortIndex}`;
|
|
15
|
+
return `ch-${chapterNumber}-${chapterSlug}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
5
18
|
function cloneWarningDetails(details) {
|
|
6
19
|
if (!details) return details;
|
|
7
20
|
|
|
@@ -46,6 +59,8 @@ export const SCHEMA = `
|
|
|
46
59
|
CREATE TABLE IF NOT EXISTS scenes (
|
|
47
60
|
scene_id TEXT NOT NULL,
|
|
48
61
|
project_id TEXT NOT NULL REFERENCES projects(project_id),
|
|
62
|
+
chapter_id TEXT,
|
|
63
|
+
scene_role TEXT,
|
|
49
64
|
title TEXT,
|
|
50
65
|
part INTEGER,
|
|
51
66
|
chapter INTEGER,
|
|
@@ -67,6 +82,47 @@ export const SCHEMA = `
|
|
|
67
82
|
PRIMARY KEY (scene_id, project_id)
|
|
68
83
|
);
|
|
69
84
|
|
|
85
|
+
CREATE TABLE IF NOT EXISTS chapters (
|
|
86
|
+
chapter_id TEXT NOT NULL,
|
|
87
|
+
project_id TEXT NOT NULL REFERENCES projects(project_id),
|
|
88
|
+
title TEXT NOT NULL,
|
|
89
|
+
sort_index INTEGER NOT NULL,
|
|
90
|
+
logline TEXT,
|
|
91
|
+
source_path TEXT,
|
|
92
|
+
source_checksum TEXT,
|
|
93
|
+
metadata_stale INTEGER NOT NULL DEFAULT 0,
|
|
94
|
+
updated_at TEXT NOT NULL,
|
|
95
|
+
PRIMARY KEY (chapter_id, project_id),
|
|
96
|
+
UNIQUE (project_id, sort_index)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
CREATE TABLE IF NOT EXISTS epigraphs (
|
|
100
|
+
epigraph_id TEXT NOT NULL,
|
|
101
|
+
project_id TEXT NOT NULL REFERENCES projects(project_id),
|
|
102
|
+
chapter_id TEXT NOT NULL,
|
|
103
|
+
body TEXT NOT NULL,
|
|
104
|
+
file_path TEXT NOT NULL,
|
|
105
|
+
prose_checksum TEXT,
|
|
106
|
+
metadata_stale INTEGER NOT NULL DEFAULT 0,
|
|
107
|
+
updated_at TEXT NOT NULL,
|
|
108
|
+
PRIMARY KEY (epigraph_id, project_id),
|
|
109
|
+
UNIQUE (project_id, chapter_id)
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
CREATE TABLE IF NOT EXISTS epigraph_characters (
|
|
113
|
+
epigraph_id TEXT NOT NULL,
|
|
114
|
+
project_id TEXT NOT NULL,
|
|
115
|
+
character_id TEXT NOT NULL,
|
|
116
|
+
PRIMARY KEY (epigraph_id, project_id, character_id)
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
CREATE TABLE IF NOT EXISTS epigraph_tags (
|
|
120
|
+
epigraph_id TEXT NOT NULL,
|
|
121
|
+
project_id TEXT NOT NULL,
|
|
122
|
+
tag TEXT NOT NULL,
|
|
123
|
+
PRIMARY KEY (epigraph_id, project_id, tag)
|
|
124
|
+
);
|
|
125
|
+
|
|
70
126
|
CREATE TABLE IF NOT EXISTS scene_characters (
|
|
71
127
|
scene_id TEXT NOT NULL,
|
|
72
128
|
project_id TEXT NOT NULL,
|
|
@@ -484,6 +540,141 @@ const MIGRATIONS = [
|
|
|
484
540
|
});
|
|
485
541
|
}
|
|
486
542
|
},
|
|
543
|
+
// 8: add canonical chapter/epigraph entities and scene linkage fields
|
|
544
|
+
(db) => {
|
|
545
|
+
const sceneColumns = db.prepare(`PRAGMA table_info(scenes)`).all();
|
|
546
|
+
if (!sceneColumns.some(c => c.name === "chapter_id")) {
|
|
547
|
+
db.exec(`ALTER TABLE scenes ADD COLUMN chapter_id TEXT;`);
|
|
548
|
+
}
|
|
549
|
+
if (!sceneColumns.some(c => c.name === "scene_role")) {
|
|
550
|
+
db.exec(`ALTER TABLE scenes ADD COLUMN scene_role TEXT;`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
db.exec(`
|
|
554
|
+
CREATE TABLE IF NOT EXISTS chapters (
|
|
555
|
+
chapter_id TEXT NOT NULL,
|
|
556
|
+
project_id TEXT NOT NULL REFERENCES projects(project_id),
|
|
557
|
+
title TEXT NOT NULL,
|
|
558
|
+
sort_index INTEGER NOT NULL,
|
|
559
|
+
logline TEXT,
|
|
560
|
+
source_path TEXT,
|
|
561
|
+
source_checksum TEXT,
|
|
562
|
+
metadata_stale INTEGER NOT NULL DEFAULT 0,
|
|
563
|
+
updated_at TEXT NOT NULL,
|
|
564
|
+
PRIMARY KEY (chapter_id, project_id),
|
|
565
|
+
UNIQUE (project_id, sort_index)
|
|
566
|
+
);
|
|
567
|
+
`);
|
|
568
|
+
|
|
569
|
+
db.exec(`
|
|
570
|
+
CREATE TABLE IF NOT EXISTS epigraphs (
|
|
571
|
+
epigraph_id TEXT NOT NULL,
|
|
572
|
+
project_id TEXT NOT NULL REFERENCES projects(project_id),
|
|
573
|
+
chapter_id TEXT NOT NULL,
|
|
574
|
+
body TEXT NOT NULL,
|
|
575
|
+
file_path TEXT NOT NULL,
|
|
576
|
+
prose_checksum TEXT,
|
|
577
|
+
metadata_stale INTEGER NOT NULL DEFAULT 0,
|
|
578
|
+
updated_at TEXT NOT NULL,
|
|
579
|
+
PRIMARY KEY (epigraph_id, project_id),
|
|
580
|
+
UNIQUE (project_id, chapter_id)
|
|
581
|
+
);
|
|
582
|
+
`);
|
|
583
|
+
|
|
584
|
+
db.exec(`
|
|
585
|
+
CREATE TABLE IF NOT EXISTS epigraph_characters (
|
|
586
|
+
epigraph_id TEXT NOT NULL,
|
|
587
|
+
project_id TEXT NOT NULL,
|
|
588
|
+
character_id TEXT NOT NULL,
|
|
589
|
+
PRIMARY KEY (epigraph_id, project_id, character_id)
|
|
590
|
+
);
|
|
591
|
+
`);
|
|
592
|
+
|
|
593
|
+
db.exec(`
|
|
594
|
+
CREATE TABLE IF NOT EXISTS epigraph_tags (
|
|
595
|
+
epigraph_id TEXT NOT NULL,
|
|
596
|
+
project_id TEXT NOT NULL,
|
|
597
|
+
tag TEXT NOT NULL,
|
|
598
|
+
PRIMARY KEY (epigraph_id, project_id, tag)
|
|
599
|
+
);
|
|
600
|
+
`);
|
|
601
|
+
},
|
|
602
|
+
// 9: backfill canonical chapters and scene chapter_id values from legacy scene chapter fields
|
|
603
|
+
(db) => {
|
|
604
|
+
const sceneColumns = db.prepare(`PRAGMA table_info(scenes)`).all();
|
|
605
|
+
const hasChapterId = sceneColumns.some((column) => column.name === "chapter_id");
|
|
606
|
+
const hasLegacyChapter = sceneColumns.some((column) => column.name === "chapter");
|
|
607
|
+
const hasLegacyChapterTitle = sceneColumns.some((column) => column.name === "chapter_title");
|
|
608
|
+
if (!hasChapterId || !hasLegacyChapter || !hasLegacyChapterTitle) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const legacyChapters = db.prepare(`
|
|
613
|
+
SELECT
|
|
614
|
+
project_id,
|
|
615
|
+
chapter AS sort_index,
|
|
616
|
+
COALESCE(NULLIF(TRIM(chapter_title), ''), '') AS chapter_title,
|
|
617
|
+
COALESCE(NULLIF(TRIM(file_path), ''), '') AS file_path,
|
|
618
|
+
COALESCE(NULLIF(TRIM(updated_at), ''), '') AS updated_at
|
|
619
|
+
FROM scenes
|
|
620
|
+
WHERE chapter IS NOT NULL
|
|
621
|
+
ORDER BY project_id, chapter, timeline_position, scene_id
|
|
622
|
+
`).all();
|
|
623
|
+
|
|
624
|
+
const chapterRows = new Map();
|
|
625
|
+
for (const row of legacyChapters) {
|
|
626
|
+
const key = `${row.project_id}::${row.sort_index}`;
|
|
627
|
+
if (chapterRows.has(key)) continue;
|
|
628
|
+
const title = row.chapter_title || `Chapter ${row.sort_index}`;
|
|
629
|
+
chapterRows.set(key, {
|
|
630
|
+
chapter_id: formatLegacyChapterId(row.sort_index, title),
|
|
631
|
+
project_id: row.project_id,
|
|
632
|
+
title,
|
|
633
|
+
sort_index: row.sort_index,
|
|
634
|
+
source_path: row.file_path ? row.file_path.replace(/[\\/][^\\/]+$/, "") : null,
|
|
635
|
+
updated_at: row.updated_at || new Date().toISOString(),
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const insertChapter = db.prepare(`
|
|
640
|
+
INSERT INTO chapters (
|
|
641
|
+
chapter_id, project_id, title, sort_index, logline, source_path, source_checksum, metadata_stale, updated_at
|
|
642
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
643
|
+
ON CONFLICT (project_id, sort_index) DO UPDATE SET
|
|
644
|
+
title = COALESCE(chapters.title, excluded.title),
|
|
645
|
+
source_path = COALESCE(chapters.source_path, excluded.source_path),
|
|
646
|
+
updated_at = CASE
|
|
647
|
+
WHEN chapters.updated_at IS NULL OR chapters.updated_at = '' THEN excluded.updated_at
|
|
648
|
+
ELSE chapters.updated_at
|
|
649
|
+
END
|
|
650
|
+
`);
|
|
651
|
+
|
|
652
|
+
for (const chapter of chapterRows.values()) {
|
|
653
|
+
insertChapter.run(
|
|
654
|
+
chapter.chapter_id,
|
|
655
|
+
chapter.project_id,
|
|
656
|
+
chapter.title,
|
|
657
|
+
chapter.sort_index,
|
|
658
|
+
null,
|
|
659
|
+
chapter.source_path,
|
|
660
|
+
null,
|
|
661
|
+
0,
|
|
662
|
+
chapter.updated_at
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
db.exec(`
|
|
667
|
+
UPDATE scenes
|
|
668
|
+
SET chapter_id = (
|
|
669
|
+
SELECT c.chapter_id
|
|
670
|
+
FROM chapters c
|
|
671
|
+
WHERE c.project_id = scenes.project_id
|
|
672
|
+
AND c.sort_index = scenes.chapter
|
|
673
|
+
)
|
|
674
|
+
WHERE chapter IS NOT NULL
|
|
675
|
+
AND (chapter_id IS NULL OR chapter_id = '');
|
|
676
|
+
`);
|
|
677
|
+
},
|
|
487
678
|
];
|
|
488
679
|
|
|
489
680
|
// The version every database should reach after openDb. Not the current DB value —
|
package/src/core/helpers.js
CHANGED
|
@@ -105,6 +105,7 @@ export function resolveBatchTargetScenes(dbHandle, {
|
|
|
105
105
|
sceneIds,
|
|
106
106
|
part,
|
|
107
107
|
chapter,
|
|
108
|
+
chapterId,
|
|
108
109
|
onlyStale,
|
|
109
110
|
}) {
|
|
110
111
|
const projectExists = Boolean(
|
|
@@ -139,6 +140,10 @@ export function resolveBatchTargetScenes(dbHandle, {
|
|
|
139
140
|
conditions.push("chapter = ?");
|
|
140
141
|
params.push(chapter);
|
|
141
142
|
}
|
|
143
|
+
if (chapterId !== undefined) {
|
|
144
|
+
conditions.push("chapter_id = ?");
|
|
145
|
+
params.push(chapterId);
|
|
146
|
+
}
|
|
142
147
|
if (onlyStale) {
|
|
143
148
|
conditions.push("metadata_stale = 1");
|
|
144
149
|
}
|
|
@@ -147,7 +152,7 @@ export function resolveBatchTargetScenes(dbHandle, {
|
|
|
147
152
|
SELECT scene_id, project_id, file_path
|
|
148
153
|
FROM scenes
|
|
149
154
|
WHERE ${conditions.join(" AND ")}
|
|
150
|
-
ORDER BY part, chapter, timeline_position
|
|
155
|
+
ORDER BY part, chapter, timeline_position, scene_id
|
|
151
156
|
`;
|
|
152
157
|
|
|
153
158
|
return {
|
|
@@ -125,6 +125,7 @@ export function buildReviewBundlePlan(dbHandle, {
|
|
|
125
125
|
profile,
|
|
126
126
|
part,
|
|
127
127
|
chapter,
|
|
128
|
+
chapter_id,
|
|
128
129
|
chapters,
|
|
129
130
|
tag,
|
|
130
131
|
scene_ids,
|
|
@@ -167,11 +168,12 @@ export function buildReviewBundlePlan(dbHandle, {
|
|
|
167
168
|
assertProfile(profile);
|
|
168
169
|
assertStrictness(strictness);
|
|
169
170
|
assertFormat(format);
|
|
170
|
-
|
|
171
|
+
const chapterFilterCount = [chapter !== undefined, chapter_id !== undefined, chapters !== undefined].filter(Boolean).length;
|
|
172
|
+
if (chapterFilterCount > 1) {
|
|
171
173
|
throw new ReviewBundlePlanError(
|
|
172
174
|
"INVALID_CHAPTER_FILTER",
|
|
173
|
-
"Use
|
|
174
|
-
{ chapter, chapters }
|
|
175
|
+
"Use one of chapter, chapter_id, or chapters.",
|
|
176
|
+
{ chapter, chapter_id, chapters }
|
|
175
177
|
);
|
|
176
178
|
}
|
|
177
179
|
let normalizedChapters;
|
|
@@ -222,6 +224,10 @@ export function buildReviewBundlePlan(dbHandle, {
|
|
|
222
224
|
conditions.push("s.chapter = ?");
|
|
223
225
|
conditionParams.push(chapter);
|
|
224
226
|
}
|
|
227
|
+
if (chapter_id !== undefined) {
|
|
228
|
+
conditions.push("s.chapter_id = ?");
|
|
229
|
+
conditionParams.push(chapter_id);
|
|
230
|
+
}
|
|
225
231
|
if (Array.isArray(normalizedChapters) && normalizedChapters.length > 0) {
|
|
226
232
|
const placeholders = normalizedChapters.map(() => "?").join(",");
|
|
227
233
|
conditions.push(`s.chapter IN (${placeholders})`);
|
|
@@ -259,6 +265,7 @@ export function buildReviewBundlePlan(dbHandle, {
|
|
|
259
265
|
filters: {
|
|
260
266
|
...(part !== undefined ? { part } : {}),
|
|
261
267
|
...(chapter !== undefined ? { chapter } : {}),
|
|
268
|
+
...(chapter_id !== undefined ? { chapter_id } : {}),
|
|
262
269
|
...(Array.isArray(normalizedChapters) ? { chapters: normalizedChapters } : {}),
|
|
263
270
|
...(tag ? { tag } : {}),
|
|
264
271
|
...(Array.isArray(scene_ids) ? { scene_ids } : {}),
|
|
@@ -345,6 +352,7 @@ export function buildReviewBundlePlan(dbHandle, {
|
|
|
345
352
|
const appliedFilters = {
|
|
346
353
|
...(part !== undefined ? { part } : {}),
|
|
347
354
|
...(chapter !== undefined ? { chapter } : {}),
|
|
355
|
+
...(chapter_id !== undefined ? { chapter_id } : {}),
|
|
348
356
|
...(Array.isArray(normalizedChapters) ? { chapters: normalizedChapters } : {}),
|
|
349
357
|
...(tag ? { tag } : {}),
|
|
350
358
|
...(Array.isArray(normalizedSceneIds) ? { scene_ids: normalizedSceneIds } : {}),
|
|
@@ -75,19 +75,23 @@ function loadBundleSceneRowsWithTags(dbHandle, projectId, sceneIds) {
|
|
|
75
75
|
const placeholders = chunk.map(() => "?").join(",");
|
|
76
76
|
const chunkRows = dbHandle.prepare(`
|
|
77
77
|
SELECT
|
|
78
|
-
scene_id,
|
|
79
|
-
project_id,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
78
|
+
s.scene_id,
|
|
79
|
+
s.project_id,
|
|
80
|
+
s.chapter_id,
|
|
81
|
+
s.scene_role,
|
|
82
|
+
s.title,
|
|
83
|
+
s.part,
|
|
84
|
+
s.chapter,
|
|
85
|
+
COALESCE(c.title, s.chapter_title) AS chapter_title,
|
|
86
|
+
s.timeline_position,
|
|
87
|
+
s.logline,
|
|
88
|
+
s.pov,
|
|
89
|
+
s.save_the_cat_beat,
|
|
90
|
+
s.file_path
|
|
89
91
|
FROM scenes
|
|
90
|
-
|
|
92
|
+
s
|
|
93
|
+
LEFT JOIN chapters c ON c.chapter_id = s.chapter_id AND c.project_id = s.project_id
|
|
94
|
+
WHERE s.project_id = ? AND s.scene_id IN (${placeholders})
|
|
91
95
|
`).all(projectId, ...chunk);
|
|
92
96
|
rows.push(...chunkRows);
|
|
93
97
|
}
|
|
@@ -145,6 +149,26 @@ function loadBundleSceneRowsWithTags(dbHandle, projectId, sceneIds) {
|
|
|
145
149
|
return orderedRows;
|
|
146
150
|
}
|
|
147
151
|
|
|
152
|
+
function loadEpigraphsByChapter(dbHandle, projectId) {
|
|
153
|
+
const rows = dbHandle.prepare(`
|
|
154
|
+
SELECT e.epigraph_id, e.chapter_id, e.body, c.title AS chapter_title, c.sort_index AS chapter
|
|
155
|
+
FROM epigraphs e
|
|
156
|
+
JOIN chapters c ON c.chapter_id = e.chapter_id AND c.project_id = e.project_id
|
|
157
|
+
WHERE e.project_id = ?
|
|
158
|
+
ORDER BY c.sort_index, e.epigraph_id
|
|
159
|
+
`).all(projectId);
|
|
160
|
+
|
|
161
|
+
return new Map(rows.map(row => [row.chapter_id, {
|
|
162
|
+
scene_id: row.epigraph_id,
|
|
163
|
+
chapter_id: row.chapter_id,
|
|
164
|
+
chapter: row.chapter,
|
|
165
|
+
chapter_title: row.chapter_title,
|
|
166
|
+
title: "Epigraph",
|
|
167
|
+
prose: String(row.body ?? "").trim(),
|
|
168
|
+
tags: ["epigraph"],
|
|
169
|
+
}]));
|
|
170
|
+
}
|
|
171
|
+
|
|
148
172
|
function normalizeRelativePath(inputPath) {
|
|
149
173
|
return String(inputPath).replace(/\\/g, "/").replace(/^\.\//, "");
|
|
150
174
|
}
|
|
@@ -381,6 +405,7 @@ function renderProseWithInlineEmphasis(doc, prose, {
|
|
|
381
405
|
}
|
|
382
406
|
|
|
383
407
|
function isEpigraphScene(scene) {
|
|
408
|
+
if (scene?.entity_kind === "epigraph") return true;
|
|
384
409
|
const tags = Array.isArray(scene?.tags)
|
|
385
410
|
? scene.tags
|
|
386
411
|
.map(tag => String(tag ?? "").trim().toLowerCase())
|
|
@@ -392,6 +417,10 @@ function isEpigraphScene(scene) {
|
|
|
392
417
|
return /^epigraph(?:\b|\s|[-:])/i.test(title);
|
|
393
418
|
}
|
|
394
419
|
|
|
420
|
+
function isCanonicalEpigraphInsert(scene) {
|
|
421
|
+
return scene?.entity_kind === "epigraph";
|
|
422
|
+
}
|
|
423
|
+
|
|
395
424
|
function renderSceneBlock(scene, options) {
|
|
396
425
|
const {
|
|
397
426
|
profile,
|
|
@@ -402,7 +431,9 @@ function renderSceneBlock(scene, options) {
|
|
|
402
431
|
} = options;
|
|
403
432
|
|
|
404
433
|
const isBetaProfile = profile === "beta_reader_personalized";
|
|
405
|
-
const
|
|
434
|
+
const isOutlineProfile = profile === "outline_discussion";
|
|
435
|
+
const isEpigraph = (isBetaProfile || isOutlineProfile) && isEpigraphScene(scene);
|
|
436
|
+
const isCanonicalEpigraph = isCanonicalEpigraphInsert(scene);
|
|
406
437
|
|
|
407
438
|
const parts = [];
|
|
408
439
|
|
|
@@ -421,6 +452,11 @@ function renderSceneBlock(scene, options) {
|
|
|
421
452
|
}
|
|
422
453
|
|
|
423
454
|
if (profile === "outline_discussion") {
|
|
455
|
+
if (isCanonicalEpigraph) {
|
|
456
|
+
const prose = normalizeHardWrappedProse(scene.prose ?? "");
|
|
457
|
+
if (prose) parts.push(prose);
|
|
458
|
+
return parts.join("\n\n");
|
|
459
|
+
}
|
|
424
460
|
const summaryParts = [];
|
|
425
461
|
if (scene.pov) summaryParts.push(`POV: ${scene.pov}`);
|
|
426
462
|
if (scene.save_the_cat_beat) summaryParts.push(`Beat: ${scene.save_the_cat_beat}`);
|
|
@@ -535,6 +571,7 @@ export function renderReviewBundleMarkdown(dbHandle, plan, { generatedAt, syncDi
|
|
|
535
571
|
|
|
536
572
|
const sceneIds = plan.ordering.map(row => row.scene_id);
|
|
537
573
|
const rows = loadBundleSceneRowsWithTags(dbHandle, plan.resolved_scope.project_id, sceneIds);
|
|
574
|
+
const epigraphsByChapter = loadEpigraphsByChapter(dbHandle, plan.resolved_scope.project_id);
|
|
538
575
|
const sections = [];
|
|
539
576
|
const recipientName = plan.resolved_scope?.options?.recipient_name;
|
|
540
577
|
const recipientDisplayName = normalizeRecipientDisplayName(recipientName);
|
|
@@ -586,12 +623,24 @@ export function renderReviewBundleMarkdown(dbHandle, plan, { generatedAt, syncDi
|
|
|
586
623
|
const showChapterHeading = isBetaProfile
|
|
587
624
|
&& Boolean(scene.chapter_title)
|
|
588
625
|
&& (!prevScene || prevScene.chapter !== scene.chapter);
|
|
626
|
+
if ((!prevScene || prevScene.chapter_id !== scene.chapter_id) && scene.chapter_id && epigraphsByChapter.has(scene.chapter_id)) {
|
|
627
|
+
sections.push(renderSceneBlock({
|
|
628
|
+
...epigraphsByChapter.get(scene.chapter_id),
|
|
629
|
+
entity_kind: "epigraph",
|
|
630
|
+
}, {
|
|
631
|
+
profile,
|
|
632
|
+
includeSceneIds: false,
|
|
633
|
+
includeMetadataSidebar: false,
|
|
634
|
+
includeParagraphAnchors: false,
|
|
635
|
+
showChapterHeading,
|
|
636
|
+
}));
|
|
637
|
+
}
|
|
589
638
|
sections.push(renderSceneBlock(withProse, {
|
|
590
639
|
profile,
|
|
591
640
|
includeSceneIds,
|
|
592
641
|
includeMetadataSidebar,
|
|
593
642
|
includeParagraphAnchors,
|
|
594
|
-
showChapterHeading,
|
|
643
|
+
showChapterHeading: showChapterHeading && !epigraphsByChapter.has(scene.chapter_id),
|
|
595
644
|
}));
|
|
596
645
|
}
|
|
597
646
|
|
|
@@ -623,6 +672,7 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
623
672
|
|
|
624
673
|
const sceneIds = plan.ordering.map(row => row.scene_id);
|
|
625
674
|
const rows = loadBundleSceneRowsWithTags(dbHandle, plan.resolved_scope.project_id, sceneIds);
|
|
675
|
+
const epigraphsByChapter = loadEpigraphsByChapter(dbHandle, plan.resolved_scope.project_id);
|
|
626
676
|
const recipientName = plan.resolved_scope?.options?.recipient_name;
|
|
627
677
|
const recipientDisplayName = normalizeRecipientDisplayName(recipientName);
|
|
628
678
|
const footerRecipientDisplayName = sanitizeFooterRecipientDisplayName(recipientDisplayName);
|
|
@@ -858,6 +908,28 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
858
908
|
}
|
|
859
909
|
}
|
|
860
910
|
|
|
911
|
+
if ((!prevScene || prevScene.chapter_id !== scene.chapter_id) && scene.chapter_id && epigraphsByChapter.has(scene.chapter_id)) {
|
|
912
|
+
const epigraph = epigraphsByChapter.get(scene.chapter_id);
|
|
913
|
+
const textWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
|
914
|
+
const epigraphWidth = Math.round(textWidth * 0.68);
|
|
915
|
+
const epigraphIndent = Math.round((textWidth - epigraphWidth) / 2);
|
|
916
|
+
const savedX = doc.x;
|
|
917
|
+
doc.moveDown(isBetaProfile ? 0.5 : 0.8);
|
|
918
|
+
doc.x = doc.page.margins.left + epigraphIndent;
|
|
919
|
+
renderProseWithInlineEmphasis(doc, epigraph.prose, {
|
|
920
|
+
bodyFont: italicFont,
|
|
921
|
+
italicFont: bodyFont,
|
|
922
|
+
fontSize: proseFontSize,
|
|
923
|
+
align: "left",
|
|
924
|
+
width: epigraphWidth,
|
|
925
|
+
lineGap: proseLineGap,
|
|
926
|
+
paragraphGap: 0,
|
|
927
|
+
blankLineMoveDown: 0.65,
|
|
928
|
+
});
|
|
929
|
+
doc.x = savedX;
|
|
930
|
+
doc.moveDown(1.0);
|
|
931
|
+
}
|
|
932
|
+
|
|
861
933
|
// Skip title for epigraphs in beta and outline profiles.
|
|
862
934
|
const isEpigraph = (isBetaProfile || isOutlineProfile) && isEpigraphScene(scene);
|
|
863
935
|
if (!isEpigraph) {
|