@hanna84/mcp-writing 3.11.0 → 3.12.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 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.12.0](https://github.com/hannasdev/mcp-writing/compare/v3.11.0...v3.12.0)
8
+
9
+ - feat(structure): normalize chapter compatibility resolution [`#204`](https://github.com/hannasdev/mcp-writing/pull/204)
10
+
7
11
  #### [v3.11.0](https://github.com/hannasdev/mcp-writing/compare/v3.10.0...v3.11.0)
8
12
 
13
+ > 18 May 2026
14
+
9
15
  - feat(structure): add explicit scene chapter assignment [`#203`](https://github.com/hannasdev/mcp-writing/pull/203)
16
+ - Release 3.11.0 [`7e73355`](https://github.com/hannasdev/mcp-writing/commit/7e73355bb4bb38845daf90e0d438e3c7e3c9c268)
10
17
 
11
18
  #### [v3.10.0](https://github.com/hannasdev/mcp-writing/compare/v3.9.5...v3.10.0)
12
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanna84/mcp-writing",
3
- "version": "3.11.0",
3
+ "version": "3.12.0",
4
4
  "description": "MCP service for AI-assisted reasoning and editing on long-form fiction projects",
5
5
  "homepage": "https://hannasdev.github.io/mcp-writing/",
6
6
  "type": "module",
@@ -1,29 +1,85 @@
1
- export function resolveChapterByCompatibilityKey(db, { projectId, chapterNumber, chapterId }) {
2
- if (!projectId) return null;
3
- if (chapterId) {
4
- return db.prepare(`
5
- SELECT chapter_id, project_id, title, sort_index, logline, metadata_stale
6
- FROM chapters
7
- WHERE project_id = ? AND chapter_id = ?
8
- `).get(projectId, chapterId);
9
- }
10
- if (chapterNumber == null) return null;
1
+ function chapterNotFoundError({ chapterNumber, chapterId }) {
2
+ return {
3
+ code: "NOT_FOUND",
4
+ message: "Chapter not found for the provided project and identifier.",
5
+ details: {
6
+ chapter: chapterNumber ?? null,
7
+ chapter_id: chapterId ?? null,
8
+ },
9
+ };
10
+ }
11
+
12
+ function ambiguousChapterError({ projectId, chapterNumber, candidates }) {
13
+ return {
14
+ code: "AMBIGUOUS_CHAPTER",
15
+ message: `Compatibility chapter ${chapterNumber} resolves to multiple canonical chapter identities in project '${projectId}'. Use chapter_id instead.`,
16
+ details: {
17
+ project_id: projectId,
18
+ chapter: chapterNumber,
19
+ candidate_chapter_ids: candidates.map(row => row.chapter_id),
20
+ },
21
+ };
22
+ }
23
+
24
+ function selectCanonicalChapterById(db, { projectId, chapterId }) {
25
+ return db.prepare(`
26
+ SELECT chapter_id, project_id, title, sort_index, logline, metadata_stale
27
+ FROM chapters
28
+ WHERE project_id = ? AND chapter_id = ?
29
+ `).get(projectId, chapterId);
30
+ }
11
31
 
12
- const canonicalChapter = db.prepare(`
32
+ function selectCanonicalChaptersByNumber(db, { projectId, chapterNumber }) {
33
+ return db.prepare(`
13
34
  SELECT chapter_id, project_id, title, sort_index, logline, metadata_stale
14
35
  FROM chapters
15
36
  WHERE project_id = ? AND sort_index = ?
16
- `).get(projectId, chapterNumber);
17
- if (canonicalChapter) return canonicalChapter;
37
+ ORDER BY chapter_id
38
+ `).all(projectId, chapterNumber);
39
+ }
18
40
 
41
+ function selectSceneDerivedChaptersByNumber(db, { projectId, chapterNumber }) {
19
42
  return db.prepare(`
20
- SELECT chapter_id, project_id, chapter_title AS title, chapter AS sort_index, NULL AS logline, MAX(metadata_stale) AS metadata_stale
43
+ SELECT
44
+ chapter_id,
45
+ project_id,
46
+ COALESCE(MAX(chapter_title), chapter_id) AS title,
47
+ chapter AS sort_index,
48
+ NULL AS logline,
49
+ MAX(metadata_stale) AS metadata_stale
21
50
  FROM scenes
22
- WHERE project_id = ? AND chapter = ? AND chapter_id IS NOT NULL
23
- GROUP BY chapter_id, project_id, chapter_title, chapter
51
+ WHERE project_id = ?
52
+ AND chapter = ?
53
+ AND chapter_id IS NOT NULL
54
+ AND chapter_id != ''
55
+ GROUP BY chapter_id, project_id, chapter
24
56
  ORDER BY chapter_id
25
- LIMIT 1
26
- `).get(projectId, chapterNumber);
57
+ `).all(projectId, chapterNumber);
58
+ }
59
+
60
+ export function resolveChapterByCompatibilityKey(db, { projectId, chapterNumber, chapterId }) {
61
+ const result = resolveChapterByCompatibilityKeyDetailed(db, { projectId, chapterNumber, chapterId });
62
+ return result?.chapter ?? null;
63
+ }
64
+
65
+ export function resolveChapterByCompatibilityKeyDetailed(db, { projectId, chapterNumber, chapterId }) {
66
+ if (!projectId) return { chapter: null };
67
+ if (chapterId) {
68
+ return { chapter: selectCanonicalChapterById(db, { projectId, chapterId }) ?? null };
69
+ }
70
+ if (chapterNumber == null) return { chapter: null };
71
+
72
+ const canonicalChapters = selectCanonicalChaptersByNumber(db, { projectId, chapterNumber });
73
+ if (canonicalChapters.length > 1) {
74
+ return { error: ambiguousChapterError({ projectId, chapterNumber, candidates: canonicalChapters }) };
75
+ }
76
+ if (canonicalChapters.length === 1) return { chapter: canonicalChapters[0] };
77
+
78
+ const sceneDerivedChapters = selectSceneDerivedChaptersByNumber(db, { projectId, chapterNumber });
79
+ if (sceneDerivedChapters.length > 1) {
80
+ return { error: ambiguousChapterError({ projectId, chapterNumber, candidates: sceneDerivedChapters }) };
81
+ }
82
+ return { chapter: sceneDerivedChapters[0] ?? null };
27
83
  }
28
84
 
29
85
  export function resolveValidatedChapterFilter(db, { projectId, chapterNumber, chapterId }) {
@@ -31,31 +87,103 @@ export function resolveValidatedChapterFilter(db, { projectId, chapterNumber, ch
31
87
  if (!chapterId && chapterNumber == null) return { chapter: null };
32
88
 
33
89
  const resolvedById = chapterId
34
- ? resolveChapterByCompatibilityKey(db, { projectId, chapterId })
90
+ ? resolveChapterByCompatibilityKeyDetailed(db, { projectId, chapterId })
35
91
  : null;
36
92
  const resolvedByNumber = chapterNumber != null
37
- ? resolveChapterByCompatibilityKey(db, { projectId, chapterNumber })
93
+ ? resolveChapterByCompatibilityKeyDetailed(db, { projectId, chapterNumber })
38
94
  : null;
39
95
 
96
+ if (resolvedById?.error) return { error: resolvedById.error };
97
+ if (resolvedByNumber?.error) return { error: resolvedByNumber.error };
98
+
99
+ if (chapterId && !resolvedById?.chapter) {
100
+ return { error: chapterNotFoundError({ chapterNumber, chapterId }) };
101
+ }
102
+ if (chapterNumber != null && !resolvedByNumber?.chapter) {
103
+ return { error: chapterNotFoundError({ chapterNumber, chapterId }) };
104
+ }
105
+
40
106
  if (chapterId && chapterNumber != null) {
41
- if (!resolvedById || !resolvedByNumber) {
107
+ if (resolvedById.chapter.chapter_id !== resolvedByNumber.chapter.chapter_id) {
42
108
  return {
43
109
  error: {
44
- code: "NOT_FOUND",
45
- message: "Chapter not found for the provided project and identifier.",
110
+ code: "VALIDATION_ERROR",
111
+ message: "chapter_id and chapter must refer to the same canonical chapter when both are provided.",
112
+ details: {
113
+ chapter_id: resolvedById.chapter.chapter_id,
114
+ chapter: chapterNumber,
115
+ resolved_chapter_id: resolvedByNumber.chapter.chapter_id,
116
+ },
46
117
  },
47
118
  };
48
119
  }
49
- if (resolvedById.chapter_id !== resolvedByNumber.chapter_id) {
120
+ return { chapter: resolvedById.chapter };
121
+ }
122
+
123
+ return { chapter: resolvedById?.chapter ?? resolvedByNumber?.chapter ?? null };
124
+ }
125
+
126
+ export function resolveValidatedChapterNumberFilters(db, { projectId, chapterNumbers }) {
127
+ if (!projectId || chapterNumbers == null) return { chapters: [] };
128
+ const invalidChapterNumbers = chapterNumbers.filter(value => !Number.isInteger(value));
129
+ if (invalidChapterNumbers.length > 0) {
130
+ return {
131
+ error: {
132
+ code: "VALIDATION_ERROR",
133
+ message: "chapters must contain only integer chapter numbers.",
134
+ details: {
135
+ project_id: projectId,
136
+ invalid_chapters: invalidChapterNumbers,
137
+ requested_chapters: chapterNumbers,
138
+ },
139
+ },
140
+ };
141
+ }
142
+ const normalizedChapterNumbers = Array.from(new Set(chapterNumbers)).sort((a, b) => a - b);
143
+ const chapters = [];
144
+ const seenChapterIds = new Set();
145
+
146
+ for (const chapterNumber of normalizedChapterNumbers) {
147
+ const resolved = resolveValidatedChapterFilter(db, {
148
+ projectId,
149
+ chapterNumber,
150
+ });
151
+
152
+ if (resolved.error) {
50
153
  return {
51
154
  error: {
52
- code: "VALIDATION_ERROR",
53
- message: "chapter_id and chapter must refer to the same canonical chapter when both are provided.",
155
+ ...resolved.error,
156
+ details: {
157
+ ...(resolved.error.details ?? {}),
158
+ project_id: projectId,
159
+ requested_chapters: normalizedChapterNumbers,
160
+ },
54
161
  },
55
162
  };
56
163
  }
57
- return { chapter: resolvedById };
164
+
165
+ if (!resolved.chapter) {
166
+ return {
167
+ error: {
168
+ code: "NOT_FOUND",
169
+ message: "Chapter not found for the provided project and identifier.",
170
+ details: {
171
+ project_id: projectId,
172
+ chapter: chapterNumber,
173
+ requested_chapters: normalizedChapterNumbers,
174
+ },
175
+ },
176
+ };
177
+ }
178
+
179
+ if (!seenChapterIds.has(resolved.chapter.chapter_id)) {
180
+ chapters.push(resolved.chapter);
181
+ seenChapterIds.add(resolved.chapter.chapter_id);
182
+ }
58
183
  }
59
184
 
60
- return { chapter: resolvedById ?? resolvedByNumber ?? null };
185
+ return {
186
+ chapters,
187
+ chapter_numbers: normalizedChapterNumbers,
188
+ };
61
189
  }
@@ -156,12 +156,6 @@ export function resolveBatchTargetScenes(dbHandle, {
156
156
  if (resolvedChapterFilter.chapter) {
157
157
  conditions.push("chapter_id = ?");
158
158
  params.push(resolvedChapterFilter.chapter.chapter_id);
159
- } else if (chapterId !== undefined) {
160
- conditions.push("chapter_id = ?");
161
- params.push(chapterId);
162
- } else if (chapter !== undefined) {
163
- conditions.push("chapter = ?");
164
- params.push(chapter);
165
159
  }
166
160
  if (onlyStale) {
167
161
  conditions.push("metadata_stale = 1");
@@ -1,3 +1,8 @@
1
+ import {
2
+ resolveValidatedChapterFilter,
3
+ resolveValidatedChapterNumberFilters,
4
+ } from "../core/chapter-resolution.js";
5
+
1
6
  const MAX_SORT_VALUE = Number.MAX_SAFE_INTEGER;
2
7
  const MAX_SCENE_ID_FILTER_PARAMS = 900;
3
8
  const MAX_CHAPTER_FILTER_PARAMS = 900;
@@ -33,7 +38,7 @@ function sceneSort(a, b) {
33
38
  const partDiff = normalizeSortNumber(a.part) - normalizeSortNumber(b.part);
34
39
  if (partDiff !== 0) return partDiff;
35
40
 
36
- const chapterDiff = normalizeSortNumber(a.chapter) - normalizeSortNumber(b.chapter);
41
+ const chapterDiff = normalizeSortNumber(a.canonical_chapter_sort ?? a.chapter) - normalizeSortNumber(b.canonical_chapter_sort ?? b.chapter);
37
42
  if (chapterDiff !== 0) return chapterDiff;
38
43
 
39
44
  const timelineDiff = normalizeSortNumber(a.timeline_position) - normalizeSortNumber(b.timeline_position);
@@ -201,6 +206,40 @@ export function buildReviewBundlePlan(dbHandle, {
201
206
  throw new ReviewBundlePlanError("NOT_FOUND", `Project '${project_id}' not found.`);
202
207
  }
203
208
 
209
+ const resolvedChapterFilter = (chapter !== undefined || chapter_id !== undefined)
210
+ ? resolveValidatedChapterFilter(dbHandle, {
211
+ projectId: project_id,
212
+ chapterNumber: chapter,
213
+ chapterId: chapter_id,
214
+ })
215
+ : { chapter: null };
216
+ if (resolvedChapterFilter.error) {
217
+ throw new ReviewBundlePlanError(
218
+ resolvedChapterFilter.error.code,
219
+ resolvedChapterFilter.error.message,
220
+ {
221
+ ...(resolvedChapterFilter.error.details ?? {}),
222
+ project_id,
223
+ chapter: chapter ?? null,
224
+ chapter_id: chapter_id ?? null,
225
+ }
226
+ );
227
+ }
228
+
229
+ const resolvedChapterSet = Array.isArray(normalizedChapters)
230
+ ? resolveValidatedChapterNumberFilters(dbHandle, {
231
+ projectId: project_id,
232
+ chapterNumbers: normalizedChapters,
233
+ })
234
+ : { chapters: [] };
235
+ if (resolvedChapterSet.error) {
236
+ throw new ReviewBundlePlanError(
237
+ resolvedChapterSet.error.code,
238
+ resolvedChapterSet.error.message,
239
+ resolvedChapterSet.error.details
240
+ );
241
+ }
242
+
204
243
  const requestedSceneIds = resolveRequestedSceneIds(dbHandle, project_id, scene_ids);
205
244
  const conditions = ["s.project_id = ?"];
206
245
  const joins = [];
@@ -220,24 +259,22 @@ export function buildReviewBundlePlan(dbHandle, {
220
259
  conditions.push("s.part = ?");
221
260
  conditionParams.push(part);
222
261
  }
223
- if (chapter !== undefined) {
224
- conditions.push("s.chapter = ?");
225
- conditionParams.push(chapter);
226
- }
227
- if (chapter_id !== undefined) {
262
+ if (resolvedChapterFilter.chapter) {
228
263
  conditions.push("s.chapter_id = ?");
229
- conditionParams.push(chapter_id);
264
+ conditionParams.push(resolvedChapterFilter.chapter.chapter_id);
230
265
  }
231
- if (Array.isArray(normalizedChapters) && normalizedChapters.length > 0) {
232
- const placeholders = normalizedChapters.map(() => "?").join(",");
233
- conditions.push(`s.chapter IN (${placeholders})`);
234
- conditionParams.push(...normalizedChapters);
266
+ if (resolvedChapterSet.chapters.length > 0) {
267
+ const placeholders = resolvedChapterSet.chapters.map(() => "?").join(",");
268
+ conditions.push(`s.chapter_id IN (${placeholders})`);
269
+ conditionParams.push(...resolvedChapterSet.chapters.map(row => row.chapter_id));
235
270
  }
236
271
 
237
272
  let query = `
238
273
  SELECT DISTINCT
239
274
  s.scene_id,
240
275
  s.project_id,
276
+ s.chapter_id,
277
+ c.sort_index AS canonical_chapter_sort,
241
278
  s.title,
242
279
  s.part,
243
280
  s.chapter,
@@ -248,6 +285,7 @@ export function buildReviewBundlePlan(dbHandle, {
248
285
  s.save_the_cat_beat,
249
286
  s.metadata_stale
250
287
  FROM scenes s
288
+ LEFT JOIN chapters c ON c.project_id = s.project_id AND c.chapter_id = s.chapter_id
251
289
  `;
252
290
 
253
291
  if (joins.length > 0) {
@@ -354,6 +392,10 @@ export function buildReviewBundlePlan(dbHandle, {
354
392
  ...(chapter !== undefined ? { chapter } : {}),
355
393
  ...(chapter_id !== undefined ? { chapter_id } : {}),
356
394
  ...(Array.isArray(normalizedChapters) ? { chapters: normalizedChapters } : {}),
395
+ ...(resolvedChapterFilter.chapter ? { resolved_chapter_id: resolvedChapterFilter.chapter.chapter_id } : {}),
396
+ ...(resolvedChapterSet.chapters.length > 0
397
+ ? { resolved_chapter_ids: resolvedChapterSet.chapters.map(row => row.chapter_id) }
398
+ : {}),
357
399
  ...(tag ? { tag } : {}),
358
400
  ...(Array.isArray(normalizedSceneIds) ? { scene_ids: normalizedSceneIds } : {}),
359
401
  };
@@ -389,9 +431,11 @@ export function buildReviewBundlePlan(dbHandle, {
389
431
  ordering: rows.map(row => ({
390
432
  scene_id: row.scene_id,
391
433
  project_id: row.project_id,
434
+ chapter_id: row.chapter_id,
392
435
  title: row.title,
393
436
  part: row.part,
394
437
  chapter: row.chapter,
438
+ canonical_chapter_sort: row.canonical_chapter_sort,
395
439
  timeline_position: row.timeline_position,
396
440
  metadata_stale: Number(row.metadata_stale) === 1,
397
441
  })),
@@ -27,9 +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 compatibility chapter filter."),
30
+ chapter: z.number().int().optional().describe("Optional compatibility chapter number resolved through canonical chapter identity."),
31
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
+ chapters: z.array(z.number().int()).min(1).optional().describe("Optional compatibility chapter-set filter resolved through canonical chapter identities. Use this for one/few specific chapters. Do not combine with chapter or chapter_id."),
33
33
  tag: z.string().optional().describe("Optional tag filter (exact match)."),
34
34
  scene_ids: z.array(z.string()).optional().describe("Optional explicit scene_id allowlist. Intersects with other filters."),
35
35
  strictness: z.enum(REVIEW_BUNDLE_STRICTNESS).optional().describe("Strictness mode: warn (default) or fail."),
@@ -124,9 +124,9 @@ export function registerReviewBundleTools(s, {
124
124
  profile: z.enum(REVIEW_BUNDLE_PROFILES).describe("Bundle profile: outline_discussion, editor_detailed, or beta_reader_personalized."),
125
125
  output_dir: z.string().describe("Directory path to write bundle artifacts into."),
126
126
  part: z.number().int().optional().describe("Optional part filter."),
127
- chapter: z.number().int().optional().describe("Optional compatibility chapter filter."),
127
+ chapter: z.number().int().optional().describe("Optional compatibility chapter number resolved through canonical chapter identity."),
128
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."),
129
+ chapters: z.array(z.number().int()).min(1).optional().describe("Optional compatibility chapter-set filter resolved through canonical chapter identities. Use this for one/few specific chapters. Do not combine with chapter or chapter_id."),
130
130
  tag: z.string().optional().describe("Optional tag filter (exact match)."),
131
131
  scene_ids: z.array(z.string()).optional().describe("Optional explicit scene_id allowlist. Intersects with other filters."),
132
132
  strictness: z.enum(REVIEW_BUNDLE_STRICTNESS).optional().describe("Strictness mode: warn (default) or fail."),
@@ -131,8 +131,10 @@ export function registerSearchTools(s, {
131
131
  if (resolvedChapterFilter.chapter) {
132
132
  conditions.push(`s.chapter_id = ?`);
133
133
  params.push(resolvedChapterFilter.chapter.chapter_id);
134
- } else if (chapter_id) { conditions.push(`s.chapter_id = ?`); params.push(chapter_id); }
135
- else if (chapter) { conditions.push(`s.chapter = ?`); params.push(chapter); }
134
+ } else if (chapter != null && !project_id) {
135
+ conditions.push(`s.chapter = ?`);
136
+ params.push(chapter);
137
+ }
136
138
  if (pov) { conditions.push(`s.pov = ?`); params.push(pov); }
137
139
 
138
140
  if (joins.length) query += " " + joins.join(" ");
@@ -382,7 +382,7 @@ export function registerStyleguideTools(s, {
382
382
  project_id: z.string().describe("Project ID to analyze (e.g. 'the-lamb' or 'universe-1/book-1')."),
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
- chapter: z.number().int().optional().describe("Optional chapter filter."),
385
+ chapter: z.number().int().optional().describe("Optional compatibility chapter number resolved through canonical chapter identity."),
386
386
  chapter_id: z.string().optional().describe("Optional canonical chapter identifier."),
387
387
  max_scenes: z.number().int().positive().optional().describe("Maximum number of scenes to analyze (default: 50)."),
388
388
  min_agreement: z.number().min(0).max(1).optional().describe("Minimum agreement ratio for suggested fields (default: 0.6)."),
@@ -632,7 +632,7 @@ export function registerStyleguideTools(s, {
632
632
  project_id: z.string().describe("Project ID to analyze (e.g. 'the-lamb' or 'universe-1/book-1')."),
633
633
  scene_ids: z.array(z.string()).optional().describe("Optional scene_id allowlist to analyze."),
634
634
  part: z.number().int().optional().describe("Optional part filter."),
635
- chapter: z.number().int().optional().describe("Optional chapter filter."),
635
+ chapter: z.number().int().optional().describe("Optional compatibility chapter number resolved through canonical chapter identity."),
636
636
  chapter_id: z.string().optional().describe("Optional canonical chapter identifier."),
637
637
  max_scenes: z.number().int().positive().optional().describe("Maximum number of scenes to analyze (default: 50)."),
638
638
  min_agreement: z.number().min(0).max(1).optional().describe("Minimum agreement ratio for suggested updates (default: 0.6)."),
package/src/tools/sync.js CHANGED
@@ -355,7 +355,7 @@ export function registerSyncTools(s, {
355
355
  project_id: z.string().describe("Project ID (e.g. 'the-lamb' or 'universe-1/book-1-the-lamb')."),
356
356
  scene_ids: z.array(z.string()).optional().describe("Optional allowlist of scene IDs to process before other filters are applied."),
357
357
  part: z.number().int().optional().describe("Optional part number filter."),
358
- chapter: z.number().int().optional().describe("Optional chapter number filter."),
358
+ chapter: z.number().int().optional().describe("Optional compatibility chapter number resolved through canonical chapter identity."),
359
359
  chapter_id: z.string().optional().describe("Optional canonical chapter identifier."),
360
360
  only_stale: z.boolean().optional().describe("If true, only process scenes currently marked metadata_stale."),
361
361
  dry_run: z.boolean().optional().describe("If true (default), returns preview results without writing sidecars."),