@hanna84/mcp-writing 3.7.0 → 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 +14 -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
|
@@ -27,8 +27,9 @@ export function registerReviewBundleTools(s, {
|
|
|
27
27
|
project_id: z.string().describe("Project ID to scope the review bundle (e.g. 'test-novel')."),
|
|
28
28
|
profile: z.enum(REVIEW_BUNDLE_PROFILES).describe("Bundle profile: outline_discussion, editor_detailed, or beta_reader_personalized."),
|
|
29
29
|
part: z.number().int().optional().describe("Optional part filter."),
|
|
30
|
-
chapter: z.number().int().optional().describe("Optional chapter filter."),
|
|
31
|
-
|
|
30
|
+
chapter: z.number().int().optional().describe("Optional compatibility chapter filter."),
|
|
31
|
+
chapter_id: z.string().optional().describe("Optional canonical chapter identifier."),
|
|
32
|
+
chapters: z.array(z.number().int()).min(1).optional().describe("Optional chapter-set filter. Use this for one/few specific chapters. Do not combine with chapter or chapter_id."),
|
|
32
33
|
tag: z.string().optional().describe("Optional tag filter (exact match)."),
|
|
33
34
|
scene_ids: z.array(z.string()).optional().describe("Optional explicit scene_id allowlist. Intersects with other filters."),
|
|
34
35
|
strictness: z.enum(REVIEW_BUNDLE_STRICTNESS).optional().describe("Strictness mode: warn (default) or fail."),
|
|
@@ -47,6 +48,7 @@ export function registerReviewBundleTools(s, {
|
|
|
47
48
|
profile,
|
|
48
49
|
part,
|
|
49
50
|
chapter,
|
|
51
|
+
chapter_id,
|
|
50
52
|
chapters,
|
|
51
53
|
tag,
|
|
52
54
|
scene_ids,
|
|
@@ -72,6 +74,7 @@ export function registerReviewBundleTools(s, {
|
|
|
72
74
|
profile,
|
|
73
75
|
part,
|
|
74
76
|
chapter,
|
|
77
|
+
chapter_id,
|
|
75
78
|
chapters,
|
|
76
79
|
tag,
|
|
77
80
|
scene_ids,
|
|
@@ -121,8 +124,9 @@ export function registerReviewBundleTools(s, {
|
|
|
121
124
|
profile: z.enum(REVIEW_BUNDLE_PROFILES).describe("Bundle profile: outline_discussion, editor_detailed, or beta_reader_personalized."),
|
|
122
125
|
output_dir: z.string().describe("Directory path to write bundle artifacts into."),
|
|
123
126
|
part: z.number().int().optional().describe("Optional part filter."),
|
|
124
|
-
chapter: z.number().int().optional().describe("Optional chapter filter."),
|
|
125
|
-
|
|
127
|
+
chapter: z.number().int().optional().describe("Optional compatibility chapter filter."),
|
|
128
|
+
chapter_id: z.string().optional().describe("Optional canonical chapter identifier."),
|
|
129
|
+
chapters: z.array(z.number().int()).min(1).optional().describe("Optional chapter-set filter. Use this for one/few specific chapters. Do not combine with chapter or chapter_id."),
|
|
126
130
|
tag: z.string().optional().describe("Optional tag filter (exact match)."),
|
|
127
131
|
scene_ids: z.array(z.string()).optional().describe("Optional explicit scene_id allowlist. Intersects with other filters."),
|
|
128
132
|
strictness: z.enum(REVIEW_BUNDLE_STRICTNESS).optional().describe("Strictness mode: warn (default) or fail."),
|
|
@@ -143,6 +147,7 @@ export function registerReviewBundleTools(s, {
|
|
|
143
147
|
output_dir,
|
|
144
148
|
part,
|
|
145
149
|
chapter,
|
|
150
|
+
chapter_id,
|
|
146
151
|
chapters,
|
|
147
152
|
tag,
|
|
148
153
|
scene_ids,
|
|
@@ -182,6 +187,7 @@ export function registerReviewBundleTools(s, {
|
|
|
182
187
|
profile,
|
|
183
188
|
part,
|
|
184
189
|
chapter,
|
|
190
|
+
chapter_id,
|
|
185
191
|
chapters,
|
|
186
192
|
tag,
|
|
187
193
|
scene_ids,
|
package/src/tools/search.js
CHANGED
|
@@ -39,6 +39,67 @@ function readSceneEntityIdsFromMetadata({ scenePath, syncDir }) {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function resolveChapterByCompatibilityKey(db, { projectId, chapterNumber, chapterId }) {
|
|
43
|
+
if (!projectId) return null;
|
|
44
|
+
if (chapterId) {
|
|
45
|
+
return db.prepare(`
|
|
46
|
+
SELECT chapter_id, project_id, title, sort_index, logline, metadata_stale
|
|
47
|
+
FROM chapters
|
|
48
|
+
WHERE project_id = ? AND chapter_id = ?
|
|
49
|
+
`).get(projectId, chapterId);
|
|
50
|
+
}
|
|
51
|
+
if (chapterNumber == null) return null;
|
|
52
|
+
const canonicalChapter = db.prepare(`
|
|
53
|
+
SELECT chapter_id, project_id, title, sort_index, logline, metadata_stale
|
|
54
|
+
FROM chapters
|
|
55
|
+
WHERE project_id = ? AND sort_index = ?
|
|
56
|
+
`).get(projectId, chapterNumber);
|
|
57
|
+
if (canonicalChapter) return canonicalChapter;
|
|
58
|
+
|
|
59
|
+
return db.prepare(`
|
|
60
|
+
SELECT chapter_id, project_id, chapter_title AS title, chapter AS sort_index, NULL AS logline, MAX(metadata_stale) AS metadata_stale
|
|
61
|
+
FROM scenes
|
|
62
|
+
WHERE project_id = ? AND chapter = ? AND chapter_id IS NOT NULL
|
|
63
|
+
GROUP BY chapter_id, project_id, chapter_title, chapter
|
|
64
|
+
ORDER BY chapter_id
|
|
65
|
+
LIMIT 1
|
|
66
|
+
`).get(projectId, chapterNumber);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveValidatedChapterFilter(db, { projectId, chapterNumber, chapterId }) {
|
|
70
|
+
if (!projectId) return { chapter: null };
|
|
71
|
+
if (!chapterId && chapterNumber == null) return { chapter: null };
|
|
72
|
+
|
|
73
|
+
const resolvedById = chapterId
|
|
74
|
+
? resolveChapterByCompatibilityKey(db, { projectId, chapterId })
|
|
75
|
+
: null;
|
|
76
|
+
const resolvedByNumber = chapterNumber != null
|
|
77
|
+
? resolveChapterByCompatibilityKey(db, { projectId, chapterNumber })
|
|
78
|
+
: null;
|
|
79
|
+
|
|
80
|
+
if (chapterId && chapterNumber != null) {
|
|
81
|
+
if (!resolvedById || !resolvedByNumber) {
|
|
82
|
+
return {
|
|
83
|
+
error: {
|
|
84
|
+
code: "NOT_FOUND",
|
|
85
|
+
message: "Chapter not found for the provided project and identifier.",
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (resolvedById.chapter_id !== resolvedByNumber.chapter_id) {
|
|
90
|
+
return {
|
|
91
|
+
error: {
|
|
92
|
+
code: "VALIDATION_ERROR",
|
|
93
|
+
message: "chapter_id and chapter must refer to the same canonical chapter when both are provided.",
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return { chapter: resolvedById };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { chapter: resolvedById ?? resolvedByNumber ?? null };
|
|
101
|
+
}
|
|
102
|
+
|
|
42
103
|
function selectApplyCandidates(enrichedCandidates, selectedDocIds, maxApply) {
|
|
43
104
|
const selectedSet = selectedDocIds ? new Set(selectedDocIds) : null;
|
|
44
105
|
const chosenByDocId = new Map();
|
|
@@ -70,21 +131,43 @@ export function registerSearchTools(s, {
|
|
|
70
131
|
// ---- find_scenes ---------------------------------------------------------
|
|
71
132
|
s.tool(
|
|
72
133
|
"find_scenes",
|
|
73
|
-
"Find scenes by filtering on character, Save the Cat beat, tags,
|
|
134
|
+
"Find scenes by filtering on character, Save the Cat beat, tags, chapter identity, numeric compatibility chapter, or POV. Returns ordered scene metadata only — no prose. Most filters are optional and combinable. `chapter_id` requires `project_id`, and mixed `chapter_id`/`chapter` filters must resolve to the same canonical chapter. Supports pagination via page/page_size and auto-paginates large result sets with total_count. Warns if any matching scenes have stale metadata. Response shape note: always returns a structured envelope (`results`, `total_count`, with pagination fields when paging is active).",
|
|
74
135
|
{
|
|
75
136
|
project_id: z.string().optional().describe("Project ID (e.g. 'the-lamb'). Use to scope results to one project."),
|
|
76
137
|
character: z.string().optional().describe("A character_id (e.g. 'char-mira-nystrom'). Returns only scenes that character appears in. Use list_characters first to find valid IDs."),
|
|
77
138
|
beat: z.string().optional().describe("Save the Cat beat name (e.g. 'Opening Image'). Exact match."),
|
|
78
139
|
tag: z.string().optional().describe("Scene tag to filter by. Exact match."),
|
|
79
140
|
part: z.number().int().optional().describe("Part number (integer, e.g. 1). Chapters are numbered globally across the whole project."),
|
|
80
|
-
chapter: z.number().int().optional().describe("
|
|
141
|
+
chapter: z.number().int().optional().describe("Compatibility chapter number resolved from canonical chapter sort order."),
|
|
142
|
+
chapter_id: z.string().optional().describe("Canonical chapter identifier. Requires project_id. Use list_chapters to find valid values."),
|
|
81
143
|
pov: z.string().optional().describe("POV character_id. Use list_characters first to find valid IDs."),
|
|
82
144
|
page: z.number().int().min(1).optional().describe("Optional page number for paginated responses (1-based)."),
|
|
83
145
|
page_size: z.number().int().min(1).max(200).optional().describe("Optional page size for paginated responses (default: 20, max: 200)."),
|
|
84
146
|
},
|
|
85
|
-
async ({ project_id, character, beat, tag, part, chapter, pov, page, page_size }) => {
|
|
147
|
+
async ({ project_id, character, beat, tag, part, chapter, chapter_id, pov, page, page_size }) => {
|
|
148
|
+
if (chapter_id && !project_id) {
|
|
149
|
+
return errorResponse(
|
|
150
|
+
"VALIDATION_ERROR",
|
|
151
|
+
"Provide project_id when filtering by chapter_id.",
|
|
152
|
+
{ chapter_id }
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const resolvedChapterFilter = project_id && (chapter_id || chapter != null)
|
|
156
|
+
? resolveValidatedChapterFilter(db, { projectId: project_id, chapterNumber: chapter, chapterId: chapter_id })
|
|
157
|
+
: { chapter: null };
|
|
158
|
+
if (resolvedChapterFilter.error) {
|
|
159
|
+
return errorResponse(
|
|
160
|
+
resolvedChapterFilter.error.code,
|
|
161
|
+
resolvedChapterFilter.error.message,
|
|
162
|
+
{
|
|
163
|
+
project_id: project_id ?? null,
|
|
164
|
+
chapter_id: chapter_id ?? null,
|
|
165
|
+
chapter: chapter ?? null,
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
}
|
|
86
169
|
let query = `
|
|
87
|
-
SELECT DISTINCT s.scene_id, s.project_id, s.title, s.part, s.chapter, s.chapter_title, s.pov,
|
|
170
|
+
SELECT DISTINCT s.scene_id, s.project_id, s.chapter_id, s.title, s.part, s.chapter, s.chapter_title, s.pov,
|
|
88
171
|
s.logline, s.scene_change, s.causality, s.stakes, s.scene_functions,
|
|
89
172
|
s.save_the_cat_beat, s.timeline_position, s.story_time,
|
|
90
173
|
s.word_count, s.metadata_stale
|
|
@@ -105,12 +188,16 @@ export function registerSearchTools(s, {
|
|
|
105
188
|
if (project_id) { conditions.push(`s.project_id = ?`); params.push(project_id); }
|
|
106
189
|
if (beat) { conditions.push(`s.save_the_cat_beat = ?`); params.push(beat); }
|
|
107
190
|
if (part) { conditions.push(`s.part = ?`); params.push(part); }
|
|
108
|
-
if (
|
|
191
|
+
if (resolvedChapterFilter.chapter) {
|
|
192
|
+
conditions.push(`s.chapter_id = ?`);
|
|
193
|
+
params.push(resolvedChapterFilter.chapter.chapter_id);
|
|
194
|
+
} else if (chapter_id) { conditions.push(`s.chapter_id = ?`); params.push(chapter_id); }
|
|
195
|
+
else if (chapter) { conditions.push(`s.chapter = ?`); params.push(chapter); }
|
|
109
196
|
if (pov) { conditions.push(`s.pov = ?`); params.push(pov); }
|
|
110
197
|
|
|
111
198
|
if (joins.length) query += " " + joins.join(" ");
|
|
112
199
|
if (conditions.length) query += " WHERE " + conditions.join(" AND ");
|
|
113
|
-
query += " ORDER BY s.part, s.chapter, s.timeline_position";
|
|
200
|
+
query += " ORDER BY s.part, s.chapter, s.timeline_position, s.scene_id";
|
|
114
201
|
|
|
115
202
|
const rows = db.prepare(query).all(...params);
|
|
116
203
|
if (rows.length === 0) {
|
|
@@ -229,21 +316,48 @@ export function registerSearchTools(s, {
|
|
|
229
316
|
// ---- get_chapter_prose ---------------------------------------------------
|
|
230
317
|
s.tool(
|
|
231
318
|
"get_chapter_prose",
|
|
232
|
-
`Load the full prose for every scene in a chapter, concatenated in order. Expensive — only use when you need to read an entire chapter. Capped at ${MAX_CHAPTER_SCENES} scenes. Use find_scenes first to confirm the chapter exists.`,
|
|
319
|
+
`Load the full prose for every scene in a chapter, concatenated in order. Provide chapter_id or chapter, plus project_id. Canonical targeting uses chapter_id; numeric chapter remains available as a compatibility alias resolved from canonical sort order. Expensive — only use when you need to read an entire chapter. Capped at ${MAX_CHAPTER_SCENES} scenes. Use find_scenes first to confirm the chapter exists.`,
|
|
233
320
|
{
|
|
234
321
|
project_id: z.string().describe("Project ID (e.g. 'the-lamb')."),
|
|
235
|
-
|
|
236
|
-
chapter: z.number().int().describe("
|
|
322
|
+
chapter_id: z.string().optional().describe("Canonical chapter identifier."),
|
|
323
|
+
chapter: z.number().int().optional().describe("Compatibility chapter number resolved from canonical sort order."),
|
|
237
324
|
},
|
|
238
|
-
async ({ project_id,
|
|
325
|
+
async ({ project_id, chapter_id, chapter }) => {
|
|
326
|
+
if (!chapter_id && chapter == null) {
|
|
327
|
+
return errorResponse("VALIDATION_ERROR", "Provide chapter_id or chapter.");
|
|
328
|
+
}
|
|
329
|
+
const resolvedChapterFilter = resolveValidatedChapterFilter(db, {
|
|
330
|
+
projectId: project_id,
|
|
331
|
+
chapterNumber: chapter,
|
|
332
|
+
chapterId: chapter_id,
|
|
333
|
+
});
|
|
334
|
+
if (resolvedChapterFilter.error) {
|
|
335
|
+
return errorResponse(
|
|
336
|
+
resolvedChapterFilter.error.code,
|
|
337
|
+
resolvedChapterFilter.error.message,
|
|
338
|
+
{
|
|
339
|
+
project_id,
|
|
340
|
+
chapter_id: chapter_id ?? null,
|
|
341
|
+
chapter: chapter ?? null,
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const resolvedChapter = resolvedChapterFilter.chapter;
|
|
346
|
+
if (!resolvedChapter) {
|
|
347
|
+
return errorResponse("NOT_FOUND", "Chapter not found for the provided project and identifier.", {
|
|
348
|
+
project_id,
|
|
349
|
+
chapter_id: chapter_id ?? null,
|
|
350
|
+
chapter: chapter ?? null,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
239
353
|
const allScenes = db.prepare(`
|
|
240
354
|
SELECT scene_id, title, file_path FROM scenes
|
|
241
|
-
WHERE project_id = ? AND
|
|
242
|
-
ORDER BY timeline_position
|
|
243
|
-
`).all(project_id,
|
|
355
|
+
WHERE project_id = ? AND chapter_id = ?
|
|
356
|
+
ORDER BY timeline_position, scene_id
|
|
357
|
+
`).all(project_id, resolvedChapter.chapter_id);
|
|
244
358
|
|
|
245
359
|
if (allScenes.length === 0) {
|
|
246
|
-
return errorResponse("NO_RESULTS", `No scenes found for
|
|
360
|
+
return errorResponse("NO_RESULTS", `No scenes found for chapter '${resolvedChapter.title}'.`);
|
|
247
361
|
}
|
|
248
362
|
|
|
249
363
|
const truncated = allScenes.length > MAX_CHAPTER_SCENES;
|
|
@@ -266,6 +380,7 @@ export function registerSearchTools(s, {
|
|
|
266
380
|
...(truncated
|
|
267
381
|
? { warning: `Chapter has ${allScenes.length} scenes — only the first ${MAX_CHAPTER_SCENES} were loaded. Set MAX_CHAPTER_SCENES to increase this limit.` }
|
|
268
382
|
: {}),
|
|
383
|
+
chapter: resolvedChapter,
|
|
269
384
|
next_step: truncated
|
|
270
385
|
? "Narrow with find_scenes and inspect key scenes individually with get_scene_prose before expanding chapter scope."
|
|
271
386
|
: "If you only need a subset, switch to find_scenes + get_scene_prose for tighter context control.",
|
|
@@ -274,6 +389,96 @@ export function registerSearchTools(s, {
|
|
|
274
389
|
}
|
|
275
390
|
);
|
|
276
391
|
|
|
392
|
+
// ---- list_chapters ------------------------------------------------------
|
|
393
|
+
s.tool(
|
|
394
|
+
"list_chapters",
|
|
395
|
+
"List canonical chapters for a project. Returns chapter_id plus compatibility sort order so callers can migrate from numeric chapter targeting without losing orientation.",
|
|
396
|
+
{
|
|
397
|
+
project_id: z.string().describe("Project ID."),
|
|
398
|
+
},
|
|
399
|
+
async ({ project_id }) => {
|
|
400
|
+
const rows = db.prepare(`
|
|
401
|
+
SELECT chapter_id, project_id, title, sort_index, logline, metadata_stale
|
|
402
|
+
FROM chapters
|
|
403
|
+
WHERE project_id = ?
|
|
404
|
+
ORDER BY sort_index, chapter_id
|
|
405
|
+
`).all(project_id);
|
|
406
|
+
|
|
407
|
+
if (rows.length === 0) {
|
|
408
|
+
return errorResponse("NO_RESULTS", `No chapters found for project '${project_id}'.`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
content: [{
|
|
413
|
+
type: "text",
|
|
414
|
+
text: JSON.stringify({
|
|
415
|
+
results: rows,
|
|
416
|
+
total_count: rows.length,
|
|
417
|
+
}, null, 2),
|
|
418
|
+
}],
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
// ---- find_epigraphs -----------------------------------------------------
|
|
424
|
+
s.tool(
|
|
425
|
+
"find_epigraphs",
|
|
426
|
+
"List canonical epigraphs for a project, optionally narrowed to a canonical chapter_id or compatibility chapter number.",
|
|
427
|
+
{
|
|
428
|
+
project_id: z.string().describe("Project ID."),
|
|
429
|
+
chapter_id: z.string().optional().describe("Canonical chapter identifier."),
|
|
430
|
+
chapter: z.number().int().optional().describe("Compatibility chapter number resolved from canonical sort order."),
|
|
431
|
+
},
|
|
432
|
+
async ({ project_id, chapter_id, chapter }) => {
|
|
433
|
+
const resolvedChapterFilter = (chapter_id || chapter != null)
|
|
434
|
+
? resolveValidatedChapterFilter(db, { projectId: project_id, chapterNumber: chapter, chapterId: chapter_id })
|
|
435
|
+
: { chapter: null };
|
|
436
|
+
if (resolvedChapterFilter.error) {
|
|
437
|
+
return errorResponse(
|
|
438
|
+
resolvedChapterFilter.error.code,
|
|
439
|
+
resolvedChapterFilter.error.message,
|
|
440
|
+
{
|
|
441
|
+
project_id,
|
|
442
|
+
chapter_id: chapter_id ?? null,
|
|
443
|
+
chapter: chapter ?? null,
|
|
444
|
+
}
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
const resolvedChapter = resolvedChapterFilter.chapter;
|
|
448
|
+
if ((chapter_id || chapter != null) && !resolvedChapter) {
|
|
449
|
+
return errorResponse("NOT_FOUND", "Chapter not found for the provided epigraph filter.", {
|
|
450
|
+
project_id,
|
|
451
|
+
chapter_id: chapter_id ?? null,
|
|
452
|
+
chapter: chapter ?? null,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const rows = db.prepare(`
|
|
457
|
+
SELECT e.epigraph_id, e.project_id, e.chapter_id, c.title AS chapter_title, c.sort_index AS chapter,
|
|
458
|
+
e.body, e.metadata_stale
|
|
459
|
+
FROM epigraphs e
|
|
460
|
+
JOIN chapters c ON c.chapter_id = e.chapter_id AND c.project_id = e.project_id
|
|
461
|
+
WHERE e.project_id = ?
|
|
462
|
+
AND (? IS NULL OR e.chapter_id = ?)
|
|
463
|
+
ORDER BY c.sort_index, e.epigraph_id
|
|
464
|
+
`).all(project_id, resolvedChapter?.chapter_id ?? null, resolvedChapter?.chapter_id ?? null);
|
|
465
|
+
|
|
466
|
+
if (rows.length === 0) {
|
|
467
|
+
return errorResponse("NO_RESULTS", `No epigraphs found for project '${project_id}'.`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
content: [{
|
|
472
|
+
type: "text",
|
|
473
|
+
text: JSON.stringify({
|
|
474
|
+
results: rows,
|
|
475
|
+
total_count: rows.length,
|
|
476
|
+
}, null, 2),
|
|
477
|
+
}],
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
|
|
277
482
|
// ---- get_arc -------------------------------------------------------------
|
|
278
483
|
s.tool(
|
|
279
484
|
"get_arc",
|
|
@@ -532,7 +737,7 @@ export function registerSearchTools(s, {
|
|
|
532
737
|
|
|
533
738
|
if (!shouldPaginate) {
|
|
534
739
|
const rows = db.prepare(`
|
|
535
|
-
SELECT f.scene_id, f.project_id, s.title, s.logline, s.part, s.chapter, s.chapter_title, s.metadata_stale
|
|
740
|
+
SELECT f.scene_id, f.project_id, s.chapter_id, s.title, s.logline, s.part, s.chapter, s.chapter_title, s.metadata_stale
|
|
536
741
|
FROM scenes_fts f
|
|
537
742
|
JOIN scenes s ON s.scene_id = f.scene_id AND s.project_id = f.project_id
|
|
538
743
|
WHERE scenes_fts MATCH ?
|
|
@@ -557,7 +762,7 @@ export function registerSearchTools(s, {
|
|
|
557
762
|
const offset = (normalizedPage - 1) * safePageSize;
|
|
558
763
|
|
|
559
764
|
const rows = db.prepare(`
|
|
560
|
-
SELECT f.scene_id, f.project_id, s.title, s.logline, s.part, s.chapter, s.chapter_title, s.metadata_stale
|
|
765
|
+
SELECT f.scene_id, f.project_id, s.chapter_id, s.title, s.logline, s.part, s.chapter, s.chapter_title, s.metadata_stale
|
|
561
766
|
FROM scenes_fts f
|
|
562
767
|
JOIN scenes s ON s.scene_id = f.scene_id AND s.project_id = f.project_id
|
|
563
768
|
WHERE scenes_fts MATCH ?
|
|
@@ -853,7 +1058,7 @@ export function registerSearchTools(s, {
|
|
|
853
1058
|
st.beat AS thread_beat, s.timeline_position, s.story_time, s.metadata_stale
|
|
854
1059
|
FROM scenes s
|
|
855
1060
|
JOIN scene_threads st ON st.scene_id = s.scene_id AND st.project_id = s.project_id AND st.thread_id = ?
|
|
856
|
-
ORDER BY s.part, s.chapter, s.timeline_position
|
|
1061
|
+
ORDER BY s.part, s.chapter, s.timeline_position, s.scene_id
|
|
857
1062
|
`).all(thread_id);
|
|
858
1063
|
const staleCount = rows.filter(r => r.metadata_stale).length;
|
|
859
1064
|
const warning = staleCount > 0 ? `${staleCount} scene(s) have stale metadata.` : undefined;
|
|
@@ -899,11 +1104,12 @@ export function registerSearchTools(s, {
|
|
|
899
1104
|
s.part, s.chapter, s.chapter_title, s.timeline_position, s.title AS scene_title
|
|
900
1105
|
FROM character_relationships r
|
|
901
1106
|
LEFT JOIN scenes s ON s.scene_id = r.scene_id
|
|
1107
|
+
AND (r.scene_id IS NULL OR s.project_id = COALESCE(?, s.project_id))
|
|
902
1108
|
WHERE r.from_character = ? AND r.to_character = ?
|
|
903
1109
|
`;
|
|
904
|
-
const params = [from_character, to_character];
|
|
1110
|
+
const params = [project_id ?? null, from_character, to_character];
|
|
905
1111
|
if (project_id) { query += ` AND (s.project_id = ? OR r.scene_id IS NULL)`; params.push(project_id); }
|
|
906
|
-
query += ` ORDER BY s.part, s.chapter, s.timeline_position`;
|
|
1112
|
+
query += ` ORDER BY s.part, s.chapter, s.timeline_position, s.scene_id`;
|
|
907
1113
|
|
|
908
1114
|
const rows = db.prepare(query).all(...params);
|
|
909
1115
|
if (rows.length === 0) {
|
package/src/tools/styleguide.js
CHANGED
|
@@ -383,6 +383,7 @@ export function registerStyleguideTools(s, {
|
|
|
383
383
|
scene_ids: z.array(z.string()).optional().describe("Optional scene_id allowlist to analyze."),
|
|
384
384
|
part: z.number().int().optional().describe("Optional part filter."),
|
|
385
385
|
chapter: z.number().int().optional().describe("Optional chapter filter."),
|
|
386
|
+
chapter_id: z.string().optional().describe("Optional canonical chapter identifier."),
|
|
386
387
|
max_scenes: z.number().int().positive().optional().describe("Maximum number of scenes to analyze (default: 50)."),
|
|
387
388
|
min_agreement: z.number().min(0).max(1).optional().describe("Minimum agreement ratio for suggested fields (default: 0.6)."),
|
|
388
389
|
min_evidence: z.number().int().positive().optional().describe("Minimum number of observed scenes per field before suggesting it (default: 3)."),
|
|
@@ -393,6 +394,7 @@ export function registerStyleguideTools(s, {
|
|
|
393
394
|
scene_ids,
|
|
394
395
|
part,
|
|
395
396
|
chapter,
|
|
397
|
+
chapter_id,
|
|
396
398
|
max_scenes = 50,
|
|
397
399
|
min_agreement = 0.6,
|
|
398
400
|
min_evidence = 3,
|
|
@@ -408,6 +410,7 @@ export function registerStyleguideTools(s, {
|
|
|
408
410
|
sceneIds: scene_ids,
|
|
409
411
|
part,
|
|
410
412
|
chapter,
|
|
413
|
+
chapterId: chapter_id,
|
|
411
414
|
onlyStale: false,
|
|
412
415
|
});
|
|
413
416
|
if (!targetResolution.ok) {
|
|
@@ -419,7 +422,7 @@ export function registerStyleguideTools(s, {
|
|
|
419
422
|
return errorResponse(
|
|
420
423
|
"NOT_FOUND",
|
|
421
424
|
`No scenes were found for project '${project_id}' with the requested filters.`,
|
|
422
|
-
{ project_id, scene_ids: scene_ids ?? null, part: part ?? null, chapter: chapter ?? null }
|
|
425
|
+
{ project_id, scene_ids: scene_ids ?? null, part: part ?? null, chapter: chapter ?? null, chapter_id: chapter_id ?? null }
|
|
423
426
|
);
|
|
424
427
|
}
|
|
425
428
|
|
|
@@ -630,6 +633,7 @@ export function registerStyleguideTools(s, {
|
|
|
630
633
|
scene_ids: z.array(z.string()).optional().describe("Optional scene_id allowlist to analyze."),
|
|
631
634
|
part: z.number().int().optional().describe("Optional part filter."),
|
|
632
635
|
chapter: z.number().int().optional().describe("Optional chapter filter."),
|
|
636
|
+
chapter_id: z.string().optional().describe("Optional canonical chapter identifier."),
|
|
633
637
|
max_scenes: z.number().int().positive().optional().describe("Maximum number of scenes to analyze (default: 50)."),
|
|
634
638
|
min_agreement: z.number().min(0).max(1).optional().describe("Minimum agreement ratio for suggested updates (default: 0.6)."),
|
|
635
639
|
include_clean_scenes: z.boolean().optional().describe("If true, include scenes with no detected drift in scene_results."),
|
|
@@ -639,6 +643,7 @@ export function registerStyleguideTools(s, {
|
|
|
639
643
|
scene_ids,
|
|
640
644
|
part,
|
|
641
645
|
chapter,
|
|
646
|
+
chapter_id,
|
|
642
647
|
max_scenes = 50,
|
|
643
648
|
min_agreement = 0.6,
|
|
644
649
|
include_clean_scenes = false,
|
|
@@ -675,6 +680,7 @@ export function registerStyleguideTools(s, {
|
|
|
675
680
|
sceneIds: scene_ids,
|
|
676
681
|
part,
|
|
677
682
|
chapter,
|
|
683
|
+
chapterId: chapter_id,
|
|
678
684
|
onlyStale: false,
|
|
679
685
|
});
|
|
680
686
|
if (!targetResolution.ok) {
|
package/src/tools/sync.js
CHANGED
|
@@ -334,6 +334,7 @@ export function registerSyncTools(s, {
|
|
|
334
334
|
scene_ids: z.array(z.string()).optional().describe("Optional allowlist of scene IDs to process before other filters are applied."),
|
|
335
335
|
part: z.number().int().optional().describe("Optional part number filter."),
|
|
336
336
|
chapter: z.number().int().optional().describe("Optional chapter number filter."),
|
|
337
|
+
chapter_id: z.string().optional().describe("Optional canonical chapter identifier."),
|
|
337
338
|
only_stale: z.boolean().optional().describe("If true, only process scenes currently marked metadata_stale."),
|
|
338
339
|
dry_run: z.boolean().optional().describe("If true (default), returns preview results without writing sidecars."),
|
|
339
340
|
replace_mode: z.enum(["merge", "replace"]).optional().describe("merge (default): add inferred IDs; replace: overwrite characters with inferred IDs."),
|
|
@@ -346,6 +347,7 @@ export function registerSyncTools(s, {
|
|
|
346
347
|
scene_ids,
|
|
347
348
|
part,
|
|
348
349
|
chapter,
|
|
350
|
+
chapter_id,
|
|
349
351
|
only_stale = false,
|
|
350
352
|
dry_run = true,
|
|
351
353
|
replace_mode = "merge",
|
|
@@ -386,6 +388,7 @@ export function registerSyncTools(s, {
|
|
|
386
388
|
sceneIds: scene_ids,
|
|
387
389
|
part,
|
|
388
390
|
chapter,
|
|
391
|
+
chapterId: chapter_id,
|
|
389
392
|
onlyStale: Boolean(only_stale),
|
|
390
393
|
});
|
|
391
394
|
if (!targetResolution.ok) {
|