@hanna84/mcp-writing 3.11.0 → 3.13.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/package.json +1 -1
- package/src/core/chapter-resolution.js +156 -28
- package/src/core/helpers.js +0 -6
- package/src/review-bundles/review-bundles-planner.js +55 -11
- package/src/structure/chapter-commands.js +512 -0
- package/src/structure/scene-chapter-assignment.js +70 -0
- package/src/tools/metadata.js +581 -1
- package/src/tools/review-bundles.js +4 -4
- package/src/tools/search.js +4 -2
- package/src/tools/styleguide.js +2 -2
- package/src/tools/sync.js +1 -1
- package/src/workflows/workflow-catalogue.js +8 -3
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import { slugifyChapterValue } from "./structure-inference.js";
|
|
2
|
+
|
|
3
|
+
export function deriveCanonicalChapterId({ chapterId, sortIndex, title }) {
|
|
4
|
+
if (chapterId) return chapterId;
|
|
5
|
+
const slug = slugifyChapterValue(title) || `chapter-${sortIndex}`;
|
|
6
|
+
return `ch-${String(sortIndex).padStart(2, "0")}-${slug}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function buildCreateChapterPlan(db, {
|
|
10
|
+
projectId,
|
|
11
|
+
title,
|
|
12
|
+
sortIndex,
|
|
13
|
+
chapterId,
|
|
14
|
+
logline = null,
|
|
15
|
+
updatedAt = new Date().toISOString(),
|
|
16
|
+
}) {
|
|
17
|
+
const normalizedTitle = typeof title === "string" ? title.trim() : "";
|
|
18
|
+
const normalizedChapterId = typeof chapterId === "string" ? chapterId.trim() : null;
|
|
19
|
+
const normalizedLogline = typeof logline === "string" && logline.trim() ? logline.trim() : null;
|
|
20
|
+
|
|
21
|
+
if (!normalizedTitle) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
error: {
|
|
25
|
+
code: "VALIDATION_ERROR",
|
|
26
|
+
message: "Provide a non-empty chapter title.",
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!Number.isInteger(sortIndex) || sortIndex < 1) {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
error: {
|
|
35
|
+
code: "VALIDATION_ERROR",
|
|
36
|
+
message: "sort_index must be a positive integer.",
|
|
37
|
+
details: { sort_index: sortIndex },
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const project = db.prepare(`
|
|
43
|
+
SELECT project_id
|
|
44
|
+
FROM projects
|
|
45
|
+
WHERE project_id = ?
|
|
46
|
+
`).get(projectId);
|
|
47
|
+
if (!project) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
error: {
|
|
51
|
+
code: "NOT_FOUND",
|
|
52
|
+
message: `Project '${projectId}' not found.`,
|
|
53
|
+
details: { project_id: projectId },
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const resolvedChapterId = deriveCanonicalChapterId({
|
|
59
|
+
chapterId: normalizedChapterId,
|
|
60
|
+
sortIndex,
|
|
61
|
+
title: normalizedTitle,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const existingById = db.prepare(`
|
|
65
|
+
SELECT chapter_id, title, sort_index
|
|
66
|
+
FROM chapters
|
|
67
|
+
WHERE project_id = ? AND chapter_id = ?
|
|
68
|
+
`).get(projectId, resolvedChapterId);
|
|
69
|
+
if (existingById) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
error: {
|
|
73
|
+
code: "ALREADY_EXISTS",
|
|
74
|
+
message: `Chapter '${resolvedChapterId}' already exists in project '${projectId}'.`,
|
|
75
|
+
details: {
|
|
76
|
+
project_id: projectId,
|
|
77
|
+
chapter_id: resolvedChapterId,
|
|
78
|
+
existing_title: existingById.title,
|
|
79
|
+
existing_sort_index: existingById.sort_index,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const existingBySortIndex = db.prepare(`
|
|
86
|
+
SELECT chapter_id, title, sort_index
|
|
87
|
+
FROM chapters
|
|
88
|
+
WHERE project_id = ? AND sort_index = ?
|
|
89
|
+
`).get(projectId, sortIndex);
|
|
90
|
+
if (existingBySortIndex) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
error: {
|
|
94
|
+
code: "VALIDATION_ERROR",
|
|
95
|
+
message: `Chapter sort_index ${sortIndex} is already used in project '${projectId}'.`,
|
|
96
|
+
details: {
|
|
97
|
+
project_id: projectId,
|
|
98
|
+
sort_index: sortIndex,
|
|
99
|
+
existing_chapter_id: existingBySortIndex.chapter_id,
|
|
100
|
+
existing_title: existingBySortIndex.title,
|
|
101
|
+
next_step: "Use list_chapters to choose an unused sort_index, or wait for reorder_chapter when changing existing order.",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const existingByTitle = db.prepare(`
|
|
108
|
+
SELECT chapter_id, title, sort_index
|
|
109
|
+
FROM chapters
|
|
110
|
+
WHERE project_id = ? AND title = ?
|
|
111
|
+
`).get(projectId, normalizedTitle);
|
|
112
|
+
if (existingByTitle) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
error: {
|
|
116
|
+
code: "VALIDATION_ERROR",
|
|
117
|
+
message: `Chapter title '${normalizedTitle}' is already used in project '${projectId}'.`,
|
|
118
|
+
details: {
|
|
119
|
+
project_id: projectId,
|
|
120
|
+
title: normalizedTitle,
|
|
121
|
+
existing_chapter_id: existingByTitle.chapter_id,
|
|
122
|
+
existing_sort_index: existingByTitle.sort_index,
|
|
123
|
+
next_step: "Use a distinct title, or wait for rename_chapter when changing an existing chapter title.",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
chapter: {
|
|
132
|
+
chapter_id: resolvedChapterId,
|
|
133
|
+
project_id: projectId,
|
|
134
|
+
title: normalizedTitle,
|
|
135
|
+
sort_index: sortIndex,
|
|
136
|
+
logline: normalizedLogline,
|
|
137
|
+
source_path: null,
|
|
138
|
+
source_checksum: null,
|
|
139
|
+
metadata_stale: 0,
|
|
140
|
+
updated_at: updatedAt,
|
|
141
|
+
},
|
|
142
|
+
diagnostics: [
|
|
143
|
+
{
|
|
144
|
+
code: "REPRESENTATION_DEFERRED",
|
|
145
|
+
severity: "info",
|
|
146
|
+
message: "Created canonical chapter state only; no scene files, sidecars, or Scrivener-compatible folders were generated.",
|
|
147
|
+
next_step: "Use assign_scene_to_chapter to place unchaptered scenes in this chapter, then run diagnose_structure if folder-derived structure may disagree.",
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function buildRenameChapterPlan(db, {
|
|
154
|
+
projectId,
|
|
155
|
+
chapterId,
|
|
156
|
+
title,
|
|
157
|
+
updatedAt = new Date().toISOString(),
|
|
158
|
+
}) {
|
|
159
|
+
const normalizedTitle = typeof title === "string" ? title.trim() : "";
|
|
160
|
+
|
|
161
|
+
if (!normalizedTitle) {
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
error: {
|
|
165
|
+
code: "VALIDATION_ERROR",
|
|
166
|
+
message: "Provide a non-empty chapter title.",
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const chapter = db.prepare(`
|
|
172
|
+
SELECT chapter_id, project_id, title, sort_index, logline, source_path, source_checksum, metadata_stale
|
|
173
|
+
FROM chapters
|
|
174
|
+
WHERE project_id = ? AND chapter_id = ?
|
|
175
|
+
`).get(projectId, chapterId);
|
|
176
|
+
if (!chapter) {
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
error: {
|
|
180
|
+
code: "NOT_FOUND",
|
|
181
|
+
message: `Chapter '${chapterId}' not found in project '${projectId}'.`,
|
|
182
|
+
details: { project_id: projectId, chapter_id: chapterId },
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const existingByTitle = db.prepare(`
|
|
188
|
+
SELECT chapter_id, title, sort_index
|
|
189
|
+
FROM chapters
|
|
190
|
+
WHERE project_id = ? AND title = ? AND chapter_id != ?
|
|
191
|
+
`).get(projectId, normalizedTitle, chapterId);
|
|
192
|
+
if (existingByTitle) {
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
error: {
|
|
196
|
+
code: "VALIDATION_ERROR",
|
|
197
|
+
message: `Chapter title '${normalizedTitle}' is already used in project '${projectId}'.`,
|
|
198
|
+
details: {
|
|
199
|
+
project_id: projectId,
|
|
200
|
+
title: normalizedTitle,
|
|
201
|
+
existing_chapter_id: existingByTitle.chapter_id,
|
|
202
|
+
existing_sort_index: existingByTitle.sort_index,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const diagnostics = [];
|
|
209
|
+
if (chapter.source_path) {
|
|
210
|
+
diagnostics.push({
|
|
211
|
+
code: "REPRESENTATION_NOT_RENAMED",
|
|
212
|
+
severity: "warning",
|
|
213
|
+
message: "Renamed canonical chapter state and explicit scene compatibility fields only; the existing chapter source folder was not renamed.",
|
|
214
|
+
next_step: "Run diagnose_structure after sync if folder-derived structure still reports the old title.",
|
|
215
|
+
details: {
|
|
216
|
+
source_path: chapter.source_path,
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
ok: true,
|
|
223
|
+
previousChapter: chapter,
|
|
224
|
+
chapter: {
|
|
225
|
+
...chapter,
|
|
226
|
+
title: normalizedTitle,
|
|
227
|
+
updated_at: updatedAt,
|
|
228
|
+
},
|
|
229
|
+
diagnostics,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function buildReorderChapterPlan(db, {
|
|
234
|
+
projectId,
|
|
235
|
+
chapterId,
|
|
236
|
+
sortIndex,
|
|
237
|
+
updatedAt = new Date().toISOString(),
|
|
238
|
+
}) {
|
|
239
|
+
if (!Number.isInteger(sortIndex) || sortIndex < 1) {
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
error: {
|
|
243
|
+
code: "VALIDATION_ERROR",
|
|
244
|
+
message: "sort_index must be a positive integer.",
|
|
245
|
+
details: { sort_index: sortIndex },
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const chapter = db.prepare(`
|
|
251
|
+
SELECT chapter_id, project_id, title, sort_index, logline, source_path, source_checksum, metadata_stale
|
|
252
|
+
FROM chapters
|
|
253
|
+
WHERE project_id = ? AND chapter_id = ?
|
|
254
|
+
`).get(projectId, chapterId);
|
|
255
|
+
if (!chapter) {
|
|
256
|
+
return {
|
|
257
|
+
ok: false,
|
|
258
|
+
error: {
|
|
259
|
+
code: "NOT_FOUND",
|
|
260
|
+
message: `Chapter '${chapterId}' not found in project '${projectId}'.`,
|
|
261
|
+
details: { project_id: projectId, chapter_id: chapterId },
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const existingBySortIndex = db.prepare(`
|
|
267
|
+
SELECT chapter_id, title, sort_index
|
|
268
|
+
FROM chapters
|
|
269
|
+
WHERE project_id = ? AND sort_index = ? AND chapter_id != ?
|
|
270
|
+
`).get(projectId, sortIndex, chapterId);
|
|
271
|
+
if (existingBySortIndex) {
|
|
272
|
+
return {
|
|
273
|
+
ok: false,
|
|
274
|
+
error: {
|
|
275
|
+
code: "VALIDATION_ERROR",
|
|
276
|
+
message: `Chapter sort_index ${sortIndex} is already used in project '${projectId}'.`,
|
|
277
|
+
details: {
|
|
278
|
+
project_id: projectId,
|
|
279
|
+
sort_index: sortIndex,
|
|
280
|
+
existing_chapter_id: existingBySortIndex.chapter_id,
|
|
281
|
+
existing_title: existingBySortIndex.title,
|
|
282
|
+
next_step: "Choose an unused sort_index. Automatic resequencing is not part of this command yet.",
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const diagnostics = [];
|
|
289
|
+
if (chapter.source_path) {
|
|
290
|
+
diagnostics.push({
|
|
291
|
+
code: "REPRESENTATION_NOT_REORDERED",
|
|
292
|
+
severity: "warning",
|
|
293
|
+
message: "Reordered canonical chapter state and explicit scene compatibility fields only; the existing chapter source folder was not renamed or moved.",
|
|
294
|
+
next_step: "Run diagnose_structure after sync if folder-derived structure still reports the old order.",
|
|
295
|
+
details: {
|
|
296
|
+
source_path: chapter.source_path,
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
ok: true,
|
|
303
|
+
previousChapter: chapter,
|
|
304
|
+
chapter: {
|
|
305
|
+
...chapter,
|
|
306
|
+
sort_index: sortIndex,
|
|
307
|
+
updated_at: updatedAt,
|
|
308
|
+
},
|
|
309
|
+
diagnostics,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function buildAttachEpigraphPlan(db, {
|
|
314
|
+
projectId,
|
|
315
|
+
epigraphId,
|
|
316
|
+
chapterId,
|
|
317
|
+
updatedAt = new Date().toISOString(),
|
|
318
|
+
}) {
|
|
319
|
+
const normalizedEpigraphId = typeof epigraphId === "string" ? epigraphId.trim() : "";
|
|
320
|
+
const normalizedChapterId = typeof chapterId === "string" ? chapterId.trim() : "";
|
|
321
|
+
|
|
322
|
+
if (!normalizedEpigraphId) {
|
|
323
|
+
return {
|
|
324
|
+
ok: false,
|
|
325
|
+
error: {
|
|
326
|
+
code: "VALIDATION_ERROR",
|
|
327
|
+
message: "Provide a non-empty epigraph_id.",
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!normalizedChapterId) {
|
|
333
|
+
return {
|
|
334
|
+
ok: false,
|
|
335
|
+
error: {
|
|
336
|
+
code: "VALIDATION_ERROR",
|
|
337
|
+
message: "Provide a non-empty chapter_id.",
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const chapter = db.prepare(`
|
|
343
|
+
SELECT chapter_id, project_id, title, sort_index, logline, source_path, source_checksum, metadata_stale
|
|
344
|
+
FROM chapters
|
|
345
|
+
WHERE project_id = ? AND chapter_id = ?
|
|
346
|
+
`).get(projectId, normalizedChapterId);
|
|
347
|
+
if (!chapter) {
|
|
348
|
+
return {
|
|
349
|
+
ok: false,
|
|
350
|
+
error: {
|
|
351
|
+
code: "NOT_FOUND",
|
|
352
|
+
message: `Chapter '${normalizedChapterId}' not found in project '${projectId}'.`,
|
|
353
|
+
details: { project_id: projectId, chapter_id: normalizedChapterId },
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const epigraph = db.prepare(`
|
|
359
|
+
SELECT epigraph_id, project_id, chapter_id, body, file_path, prose_checksum, metadata_stale
|
|
360
|
+
FROM epigraphs
|
|
361
|
+
WHERE project_id = ? AND epigraph_id = ?
|
|
362
|
+
`).get(projectId, normalizedEpigraphId);
|
|
363
|
+
if (!epigraph) {
|
|
364
|
+
return {
|
|
365
|
+
ok: false,
|
|
366
|
+
error: {
|
|
367
|
+
code: "NOT_FOUND",
|
|
368
|
+
message: `Epigraph '${normalizedEpigraphId}' not found in project '${projectId}'.`,
|
|
369
|
+
details: { project_id: projectId, epigraph_id: normalizedEpigraphId },
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const existingForChapter = db.prepare(`
|
|
375
|
+
SELECT epigraph_id, chapter_id
|
|
376
|
+
FROM epigraphs
|
|
377
|
+
WHERE project_id = ? AND chapter_id = ? AND epigraph_id != ?
|
|
378
|
+
`).get(projectId, normalizedChapterId, normalizedEpigraphId);
|
|
379
|
+
if (existingForChapter) {
|
|
380
|
+
return {
|
|
381
|
+
ok: false,
|
|
382
|
+
error: {
|
|
383
|
+
code: "VALIDATION_ERROR",
|
|
384
|
+
message: `Chapter '${normalizedChapterId}' already has epigraph '${existingForChapter.epigraph_id}'.`,
|
|
385
|
+
details: {
|
|
386
|
+
project_id: projectId,
|
|
387
|
+
chapter_id: normalizedChapterId,
|
|
388
|
+
epigraph_id: normalizedEpigraphId,
|
|
389
|
+
existing_epigraph_id: existingForChapter.epigraph_id,
|
|
390
|
+
next_step: "Use find_epigraphs to inspect the current chapter epigraph before reattaching another one.",
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const previousChapter = db.prepare(`
|
|
397
|
+
SELECT chapter_id, title, sort_index
|
|
398
|
+
FROM chapters
|
|
399
|
+
WHERE project_id = ? AND chapter_id = ?
|
|
400
|
+
`).get(projectId, epigraph.chapter_id);
|
|
401
|
+
|
|
402
|
+
const diagnostics = [];
|
|
403
|
+
if (epigraph.file_path) {
|
|
404
|
+
diagnostics.push({
|
|
405
|
+
code: "REPRESENTATION_NOT_MOVED",
|
|
406
|
+
severity: "warning",
|
|
407
|
+
message: "Attached canonical epigraph state and explicit epigraph sidecar fields only; the existing epigraph source file was not moved.",
|
|
408
|
+
next_step: "Run diagnose_structure after sync if folder-derived structure still reports the previous chapter.",
|
|
409
|
+
details: {
|
|
410
|
+
file_path: epigraph.file_path,
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
ok: true,
|
|
417
|
+
previousChapter,
|
|
418
|
+
chapter,
|
|
419
|
+
epigraph: {
|
|
420
|
+
...epigraph,
|
|
421
|
+
chapter_id: normalizedChapterId,
|
|
422
|
+
updated_at: updatedAt,
|
|
423
|
+
},
|
|
424
|
+
diagnostics,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function insertCanonicalChapter(db, chapter) {
|
|
429
|
+
db.prepare(`
|
|
430
|
+
INSERT INTO chapters (
|
|
431
|
+
chapter_id, project_id, title, sort_index, logline, source_path, source_checksum, metadata_stale, updated_at
|
|
432
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
433
|
+
`).run(
|
|
434
|
+
chapter.chapter_id,
|
|
435
|
+
chapter.project_id,
|
|
436
|
+
chapter.title,
|
|
437
|
+
chapter.sort_index,
|
|
438
|
+
chapter.logline,
|
|
439
|
+
chapter.source_path,
|
|
440
|
+
chapter.source_checksum,
|
|
441
|
+
chapter.metadata_stale,
|
|
442
|
+
chapter.updated_at
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function renameCanonicalChapter(db, chapter) {
|
|
447
|
+
db.prepare(`
|
|
448
|
+
UPDATE chapters
|
|
449
|
+
SET title = ?,
|
|
450
|
+
updated_at = ?
|
|
451
|
+
WHERE project_id = ? AND chapter_id = ?
|
|
452
|
+
`).run(
|
|
453
|
+
chapter.title,
|
|
454
|
+
chapter.updated_at,
|
|
455
|
+
chapter.project_id,
|
|
456
|
+
chapter.chapter_id
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
db.prepare(`
|
|
460
|
+
UPDATE scenes
|
|
461
|
+
SET chapter_title = ?,
|
|
462
|
+
updated_at = ?
|
|
463
|
+
WHERE project_id = ? AND chapter_id = ?
|
|
464
|
+
`).run(
|
|
465
|
+
chapter.title,
|
|
466
|
+
chapter.updated_at,
|
|
467
|
+
chapter.project_id,
|
|
468
|
+
chapter.chapter_id
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function attachCanonicalEpigraph(db, epigraph) {
|
|
473
|
+
db.prepare(`
|
|
474
|
+
UPDATE epigraphs
|
|
475
|
+
SET chapter_id = ?,
|
|
476
|
+
updated_at = ?
|
|
477
|
+
WHERE project_id = ? AND epigraph_id = ?
|
|
478
|
+
`).run(
|
|
479
|
+
epigraph.chapter_id,
|
|
480
|
+
epigraph.updated_at,
|
|
481
|
+
epigraph.project_id,
|
|
482
|
+
epigraph.epigraph_id
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export function reorderCanonicalChapter(db, chapter) {
|
|
487
|
+
db.prepare(`
|
|
488
|
+
UPDATE chapters
|
|
489
|
+
SET sort_index = ?,
|
|
490
|
+
updated_at = ?
|
|
491
|
+
WHERE project_id = ? AND chapter_id = ?
|
|
492
|
+
`).run(
|
|
493
|
+
chapter.sort_index,
|
|
494
|
+
chapter.updated_at,
|
|
495
|
+
chapter.project_id,
|
|
496
|
+
chapter.chapter_id
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
db.prepare(`
|
|
500
|
+
UPDATE scenes
|
|
501
|
+
SET chapter = ?,
|
|
502
|
+
chapter_title = ?,
|
|
503
|
+
updated_at = ?
|
|
504
|
+
WHERE project_id = ? AND chapter_id = ?
|
|
505
|
+
`).run(
|
|
506
|
+
chapter.sort_index,
|
|
507
|
+
chapter.title,
|
|
508
|
+
chapter.updated_at,
|
|
509
|
+
chapter.project_id,
|
|
510
|
+
chapter.chapter_id
|
|
511
|
+
);
|
|
512
|
+
}
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { applySceneStructurePatch } from "./structure-inference.js";
|
|
2
2
|
|
|
3
|
+
function buildAssignedChapterPayload({ chapterId, sortIndex, title }) {
|
|
4
|
+
if (!chapterId) return null;
|
|
5
|
+
return {
|
|
6
|
+
chapter_id: chapterId,
|
|
7
|
+
sort_index: sortIndex ?? null,
|
|
8
|
+
title: title ?? null,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
3
12
|
export function buildSceneChapterAssignmentPlan(syncDir, filePath, meta = {}, { chapter } = {}) {
|
|
4
13
|
if (chapter === undefined) {
|
|
5
14
|
return {
|
|
@@ -75,3 +84,64 @@ export function buildSceneChapterAssignmentPlan(syncDir, filePath, meta = {}, {
|
|
|
75
84
|
previousChapterId: meta.chapter_id ?? null,
|
|
76
85
|
};
|
|
77
86
|
}
|
|
87
|
+
|
|
88
|
+
export function buildMoveScenePlan(syncDir, filePath, meta = {}, {
|
|
89
|
+
currentScene,
|
|
90
|
+
chapter,
|
|
91
|
+
timelinePosition,
|
|
92
|
+
} = {}) {
|
|
93
|
+
if (chapter === undefined && timelinePosition === undefined) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
error: {
|
|
97
|
+
code: "VALIDATION_ERROR",
|
|
98
|
+
message: "Provide chapter_id and/or timeline_position for move_scene.",
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (timelinePosition !== undefined && (!Number.isInteger(timelinePosition) || timelinePosition < 1)) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
error: {
|
|
107
|
+
code: "VALIDATION_ERROR",
|
|
108
|
+
message: "timeline_position must be a positive integer.",
|
|
109
|
+
details: { timeline_position: timelinePosition },
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const assignmentPlan = chapter === undefined
|
|
115
|
+
? {
|
|
116
|
+
ok: true,
|
|
117
|
+
meta,
|
|
118
|
+
assignedChapter: buildAssignedChapterPayload({
|
|
119
|
+
chapterId: meta.chapter_id ?? currentScene?.chapter_id ?? null,
|
|
120
|
+
sortIndex: meta.chapter ?? currentScene?.chapter ?? null,
|
|
121
|
+
title: meta.chapter_title ?? currentScene?.chapter_title ?? null,
|
|
122
|
+
}),
|
|
123
|
+
previousChapterId: meta.chapter_id ?? currentScene?.chapter_id ?? null,
|
|
124
|
+
}
|
|
125
|
+
: buildSceneChapterAssignmentPlan(syncDir, filePath, meta, { chapter });
|
|
126
|
+
|
|
127
|
+
if (!assignmentPlan.ok) return assignmentPlan;
|
|
128
|
+
|
|
129
|
+
const shouldCarryIndexedTimelinePosition = timelinePosition === undefined
|
|
130
|
+
&& chapter !== undefined
|
|
131
|
+
&& meta.timeline_position === undefined
|
|
132
|
+
&& currentScene?.timeline_position != null;
|
|
133
|
+
const movedMeta = {
|
|
134
|
+
...assignmentPlan.meta,
|
|
135
|
+
...(shouldCarryIndexedTimelinePosition ? { timeline_position: currentScene.timeline_position } : {}),
|
|
136
|
+
...(timelinePosition !== undefined ? { timeline_position: timelinePosition } : {}),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
ok: true,
|
|
141
|
+
meta: movedMeta,
|
|
142
|
+
assignedChapter: assignmentPlan.assignedChapter,
|
|
143
|
+
previousChapterId: assignmentPlan.previousChapterId ?? currentScene?.chapter_id ?? null,
|
|
144
|
+
previousTimelinePosition: meta.timeline_position ?? currentScene?.timeline_position ?? null,
|
|
145
|
+
timelinePosition: movedMeta.timeline_position ?? null,
|
|
146
|
+
};
|
|
147
|
+
}
|