@hanna84/mcp-writing 1.13.2 → 1.15.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 +20 -0
- package/index.js +69 -2
- package/package.json +2 -1
- package/review-bundles.js +290 -0
- package/scripts/async-job-runner.mjs +1 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,31 @@ 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
|
+
#### [v1.15.0](https://github.com/hannasdev/mcp-writing.git
|
|
8
|
+
/compare/v1.14.0...v1.15.0)
|
|
9
|
+
|
|
10
|
+
- feat: add review bundle preview planner tool [`#73`](https://github.com/hannasdev/mcp-writing.git
|
|
11
|
+
/pull/73)
|
|
12
|
+
|
|
13
|
+
#### [v1.14.0](https://github.com/hannasdev/mcp-writing.git
|
|
14
|
+
/compare/v1.13.2...v1.14.0)
|
|
15
|
+
|
|
16
|
+
> 24 April 2026
|
|
17
|
+
|
|
18
|
+
- feat(scrivener-direct): graduate from beta to stable [`#72`](https://github.com/hannasdev/mcp-writing.git
|
|
19
|
+
/pull/72)
|
|
20
|
+
- Release 1.14.0 [`4baf0c3`](https://github.com/hannasdev/mcp-writing.git
|
|
21
|
+
/commit/4baf0c38eed39337a58c8503d382dc1db6987c6a)
|
|
22
|
+
|
|
7
23
|
#### [v1.13.2](https://github.com/hannasdev/mcp-writing.git
|
|
8
24
|
/compare/v1.13.1...v1.13.2)
|
|
9
25
|
|
|
26
|
+
> 24 April 2026
|
|
27
|
+
|
|
10
28
|
- fix(scrivener-direct): add Phase E data safety hardening [`#71`](https://github.com/hannasdev/mcp-writing.git
|
|
11
29
|
/pull/71)
|
|
30
|
+
- Release 1.13.2 [`8b98c80`](https://github.com/hannasdev/mcp-writing.git
|
|
31
|
+
/commit/8b98c80e27446d41f42c88217253ab5d30b065f2)
|
|
12
32
|
|
|
13
33
|
#### [v1.13.1](https://github.com/hannasdev/mcp-writing.git
|
|
14
34
|
/compare/v1.13.0...v1.13.1)
|
package/index.js
CHANGED
|
@@ -17,6 +17,12 @@ import { isGitAvailable, isGitRepository, initGitRepository, createSnapshot, lis
|
|
|
17
17
|
import { renderCharacterArcTemplate, renderCharacterSheetTemplate, renderPlaceSheetTemplate, slugifyEntityName } from "./world-entity-templates.js";
|
|
18
18
|
import { importScrivenerSync, validateProjectId } from "./importer.js";
|
|
19
19
|
import { ASYNC_PROGRESS_PREFIX } from "./async-progress.js";
|
|
20
|
+
import {
|
|
21
|
+
REVIEW_BUNDLE_PROFILES,
|
|
22
|
+
REVIEW_BUNDLE_STRICTNESS,
|
|
23
|
+
ReviewBundlePlanError,
|
|
24
|
+
buildReviewBundlePlan,
|
|
25
|
+
} from "./review-bundles.js";
|
|
20
26
|
|
|
21
27
|
const SYNC_DIR = process.env.WRITING_SYNC_DIR ?? "./sync";
|
|
22
28
|
const DB_PATH = process.env.DB_PATH ?? "./writing.db";
|
|
@@ -980,7 +986,7 @@ function createMcpServer() {
|
|
|
980
986
|
|
|
981
987
|
s.tool(
|
|
982
988
|
"merge_scrivener_project_beta",
|
|
983
|
-
"
|
|
989
|
+
"Merge metadata directly from a Scrivener .scriv project into existing scene sidecars by starting a background job. This path is opt-in and requires sidecars to already exist (for example, from import_scrivener_sync). Returns immediately with a job_id to poll via get_async_job_status.",
|
|
984
990
|
{
|
|
985
991
|
source_project_dir: z.string().describe("Path to a Scrivener .scriv bundle directory."),
|
|
986
992
|
project_id: z.string().optional().describe("Project ID containing existing sidecars (e.g. 'the-lamb' or 'universe-1/book-1-the-lamb')."),
|
|
@@ -1052,7 +1058,6 @@ function createMcpServer() {
|
|
|
1052
1058
|
return jsonResponse({
|
|
1053
1059
|
ok: true,
|
|
1054
1060
|
async: true,
|
|
1055
|
-
beta: true,
|
|
1056
1061
|
job: toPublicJob(job, false),
|
|
1057
1062
|
next_step: "Call get_async_job_status with job_id until status is 'completed' or 'failed'.",
|
|
1058
1063
|
});
|
|
@@ -1299,6 +1304,68 @@ function createMcpServer() {
|
|
|
1299
1304
|
}
|
|
1300
1305
|
);
|
|
1301
1306
|
|
|
1307
|
+
// ---- preview_review_bundle ----------------------------------------------
|
|
1308
|
+
s.tool(
|
|
1309
|
+
"preview_review_bundle",
|
|
1310
|
+
"Dry-run planning tool for review bundles. Resolves scene scope, deterministic ordering, warnings, and planned output filenames without writing files. Note: include_scene_ids/include_metadata_sidebar/include_paragraph_anchors are advisory placeholders in Phase 4A.1 and do not alter planning semantics yet.",
|
|
1311
|
+
{
|
|
1312
|
+
project_id: z.string().describe("Project ID to scope the review bundle (e.g. 'test-novel')."),
|
|
1313
|
+
profile: z.enum(REVIEW_BUNDLE_PROFILES).describe("Bundle profile: outline_discussion or editor_detailed."),
|
|
1314
|
+
part: z.number().int().optional().describe("Optional part filter."),
|
|
1315
|
+
chapter: z.number().int().optional().describe("Optional chapter filter."),
|
|
1316
|
+
tag: z.string().optional().describe("Optional tag filter (exact match)."),
|
|
1317
|
+
scene_ids: z.array(z.string()).optional().describe("Optional explicit scene_id allowlist. Intersects with other filters."),
|
|
1318
|
+
strictness: z.enum(REVIEW_BUNDLE_STRICTNESS).optional().describe("Strictness mode: warn (default) or fail."),
|
|
1319
|
+
include_scene_ids: z.boolean().optional().describe("Advisory placeholder for later rendering behavior (default true). Included in preview output options, but does not change planning results in Phase 4A.1."),
|
|
1320
|
+
include_metadata_sidebar: z.boolean().optional().describe("Advisory placeholder for later rendering behavior (default false). Included in preview output options, but does not change planning results in Phase 4A.1."),
|
|
1321
|
+
include_paragraph_anchors: z.boolean().optional().describe("Advisory placeholder for later rendering behavior (default false). Included in preview output options, but does not change planning results in Phase 4A.1."),
|
|
1322
|
+
bundle_name: z.string().optional().describe("Optional output bundle base name override (slugified in planned outputs)."),
|
|
1323
|
+
},
|
|
1324
|
+
async ({
|
|
1325
|
+
project_id,
|
|
1326
|
+
profile,
|
|
1327
|
+
part,
|
|
1328
|
+
chapter,
|
|
1329
|
+
tag,
|
|
1330
|
+
scene_ids,
|
|
1331
|
+
strictness = "warn",
|
|
1332
|
+
include_scene_ids = true,
|
|
1333
|
+
include_metadata_sidebar = false,
|
|
1334
|
+
include_paragraph_anchors = false,
|
|
1335
|
+
bundle_name,
|
|
1336
|
+
}) => {
|
|
1337
|
+
const projectIdCheck = validateProjectId(project_id);
|
|
1338
|
+
if (!projectIdCheck.ok) {
|
|
1339
|
+
return errorResponse("INVALID_PROJECT_ID", projectIdCheck.reason, { project_id });
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
try {
|
|
1343
|
+
const plan = buildReviewBundlePlan(db, {
|
|
1344
|
+
project_id,
|
|
1345
|
+
profile,
|
|
1346
|
+
part,
|
|
1347
|
+
chapter,
|
|
1348
|
+
tag,
|
|
1349
|
+
scene_ids,
|
|
1350
|
+
strictness,
|
|
1351
|
+
include_scene_ids,
|
|
1352
|
+
include_metadata_sidebar,
|
|
1353
|
+
include_paragraph_anchors,
|
|
1354
|
+
bundle_name,
|
|
1355
|
+
});
|
|
1356
|
+
return jsonResponse(plan);
|
|
1357
|
+
} catch (error) {
|
|
1358
|
+
if (error instanceof ReviewBundlePlanError) {
|
|
1359
|
+
return errorResponse(error.code, error.message, error.details);
|
|
1360
|
+
}
|
|
1361
|
+
return errorResponse(
|
|
1362
|
+
"PREVIEW_FAILED",
|
|
1363
|
+
error instanceof Error ? error.message : "Failed to generate review bundle preview."
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
);
|
|
1368
|
+
|
|
1302
1369
|
// ---- find_scenes ---------------------------------------------------------
|
|
1303
1370
|
s.tool(
|
|
1304
1371
|
"find_scenes",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanna84/mcp-writing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "MCP service for AI-assisted reasoning and editing on long-form fiction projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"git.js",
|
|
15
15
|
"world-entity-templates.js",
|
|
16
16
|
"metadata-lint.js",
|
|
17
|
+
"review-bundles.js",
|
|
17
18
|
"scripts/",
|
|
18
19
|
"README.md",
|
|
19
20
|
"CHANGELOG.md"
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
const MAX_SORT_VALUE = Number.MAX_SAFE_INTEGER;
|
|
2
|
+
|
|
3
|
+
export const REVIEW_BUNDLE_PROFILES = ["outline_discussion", "editor_detailed"];
|
|
4
|
+
export const REVIEW_BUNDLE_STRICTNESS = ["warn", "fail"];
|
|
5
|
+
|
|
6
|
+
export class ReviewBundlePlanError extends Error {
|
|
7
|
+
constructor(code, message, details) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "ReviewBundlePlanError";
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.details = details;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeSortNumber(value) {
|
|
16
|
+
return Number.isInteger(value) ? value : MAX_SORT_VALUE;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sceneSort(a, b) {
|
|
20
|
+
const partDiff = normalizeSortNumber(a.part) - normalizeSortNumber(b.part);
|
|
21
|
+
if (partDiff !== 0) return partDiff;
|
|
22
|
+
|
|
23
|
+
const chapterDiff = normalizeSortNumber(a.chapter) - normalizeSortNumber(b.chapter);
|
|
24
|
+
if (chapterDiff !== 0) return chapterDiff;
|
|
25
|
+
|
|
26
|
+
const timelineDiff = normalizeSortNumber(a.timeline_position) - normalizeSortNumber(b.timeline_position);
|
|
27
|
+
if (timelineDiff !== 0) return timelineDiff;
|
|
28
|
+
|
|
29
|
+
return String(a.scene_id).localeCompare(String(b.scene_id));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildWarningSummary(warnings) {
|
|
33
|
+
const summary = {};
|
|
34
|
+
for (const warning of warnings) {
|
|
35
|
+
const type = warning.type ?? "unknown";
|
|
36
|
+
if (!summary[type]) {
|
|
37
|
+
summary[type] = { count: 0, examples: [] };
|
|
38
|
+
}
|
|
39
|
+
summary[type].count += 1;
|
|
40
|
+
if (summary[type].examples.length < 5) {
|
|
41
|
+
summary[type].examples.push(warning.message);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return summary;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function slugifyBundleName(value) {
|
|
48
|
+
const slug = String(value ?? "")
|
|
49
|
+
.trim()
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
52
|
+
.replace(/^-+|-+$/g, "");
|
|
53
|
+
return slug || "review-bundle";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function assertProfile(profile) {
|
|
57
|
+
if (!REVIEW_BUNDLE_PROFILES.includes(profile)) {
|
|
58
|
+
throw new ReviewBundlePlanError(
|
|
59
|
+
"INVALID_PROFILE",
|
|
60
|
+
`Unsupported review bundle profile '${profile}'.`,
|
|
61
|
+
{ supported_profiles: REVIEW_BUNDLE_PROFILES }
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function assertStrictness(strictness) {
|
|
67
|
+
if (!REVIEW_BUNDLE_STRICTNESS.includes(strictness)) {
|
|
68
|
+
throw new ReviewBundlePlanError(
|
|
69
|
+
"INVALID_STRICTNESS",
|
|
70
|
+
`Unsupported strictness '${strictness}'.`,
|
|
71
|
+
{ supported_strictness: REVIEW_BUNDLE_STRICTNESS }
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveRequestedSceneIds(dbHandle, projectId, sceneIds) {
|
|
77
|
+
if (!Array.isArray(sceneIds) || sceneIds.length === 0) {
|
|
78
|
+
return { requested: [], existing: new Set() };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const placeholders = sceneIds.map(() => "?").join(",");
|
|
82
|
+
const rows = dbHandle.prepare(
|
|
83
|
+
`SELECT scene_id FROM scenes WHERE project_id = ? AND scene_id IN (${placeholders})`
|
|
84
|
+
).all(projectId, ...sceneIds);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
requested: sceneIds,
|
|
88
|
+
existing: new Set(rows.map(row => row.scene_id)),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function buildReviewBundlePlan(dbHandle, {
|
|
93
|
+
project_id,
|
|
94
|
+
profile,
|
|
95
|
+
part,
|
|
96
|
+
chapter,
|
|
97
|
+
tag,
|
|
98
|
+
scene_ids,
|
|
99
|
+
strictness = "warn",
|
|
100
|
+
include_scene_ids = true,
|
|
101
|
+
include_metadata_sidebar = false,
|
|
102
|
+
include_paragraph_anchors = false,
|
|
103
|
+
bundle_name,
|
|
104
|
+
} = {}) {
|
|
105
|
+
if (!project_id) {
|
|
106
|
+
throw new ReviewBundlePlanError("INVALID_PROJECT_ID", "project_id is required.");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
assertProfile(profile);
|
|
110
|
+
assertStrictness(strictness);
|
|
111
|
+
|
|
112
|
+
const projectRow = dbHandle.prepare(`SELECT project_id FROM projects WHERE project_id = ?`).get(project_id);
|
|
113
|
+
if (!projectRow) {
|
|
114
|
+
throw new ReviewBundlePlanError("NOT_FOUND", `Project '${project_id}' not found.`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const requestedSceneIds = resolveRequestedSceneIds(dbHandle, project_id, scene_ids);
|
|
118
|
+
const conditions = ["s.project_id = ?"];
|
|
119
|
+
const params = [project_id];
|
|
120
|
+
const joins = [];
|
|
121
|
+
|
|
122
|
+
if (tag) {
|
|
123
|
+
joins.push("JOIN scene_tags st ON st.scene_id = s.scene_id AND st.tag = ?");
|
|
124
|
+
params.push(tag);
|
|
125
|
+
}
|
|
126
|
+
if (Array.isArray(scene_ids) && scene_ids.length > 0) {
|
|
127
|
+
const placeholders = scene_ids.map(() => "?").join(",");
|
|
128
|
+
conditions.push(`s.scene_id IN (${placeholders})`);
|
|
129
|
+
params.push(...scene_ids);
|
|
130
|
+
}
|
|
131
|
+
if (part !== undefined) {
|
|
132
|
+
conditions.push("s.part = ?");
|
|
133
|
+
params.push(part);
|
|
134
|
+
}
|
|
135
|
+
if (chapter !== undefined) {
|
|
136
|
+
conditions.push("s.chapter = ?");
|
|
137
|
+
params.push(chapter);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let query = `
|
|
141
|
+
SELECT DISTINCT
|
|
142
|
+
s.scene_id,
|
|
143
|
+
s.project_id,
|
|
144
|
+
s.title,
|
|
145
|
+
s.part,
|
|
146
|
+
s.chapter,
|
|
147
|
+
s.timeline_position,
|
|
148
|
+
s.word_count,
|
|
149
|
+
s.logline,
|
|
150
|
+
s.pov,
|
|
151
|
+
s.save_the_cat_beat,
|
|
152
|
+
s.metadata_stale
|
|
153
|
+
FROM scenes s
|
|
154
|
+
`;
|
|
155
|
+
|
|
156
|
+
if (joins.length > 0) {
|
|
157
|
+
query += ` ${joins.join(" ")}`;
|
|
158
|
+
}
|
|
159
|
+
query += ` WHERE ${conditions.join(" AND ")}`;
|
|
160
|
+
|
|
161
|
+
const rows = dbHandle.prepare(query).all(...params).sort(sceneSort);
|
|
162
|
+
if (rows.length === 0) {
|
|
163
|
+
throw new ReviewBundlePlanError(
|
|
164
|
+
"NO_RESULTS",
|
|
165
|
+
"No scenes matched the requested review bundle scope.",
|
|
166
|
+
{
|
|
167
|
+
project_id,
|
|
168
|
+
filters: {
|
|
169
|
+
...(part !== undefined ? { part } : {}),
|
|
170
|
+
...(chapter !== undefined ? { chapter } : {}),
|
|
171
|
+
...(tag ? { tag } : {}),
|
|
172
|
+
...(Array.isArray(scene_ids) ? { scene_ids } : {}),
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const includedSceneIds = new Set(rows.map(row => row.scene_id));
|
|
179
|
+
const excludedSceneIds = requestedSceneIds.requested.filter(sceneId => !includedSceneIds.has(sceneId));
|
|
180
|
+
const notFoundSceneIds = requestedSceneIds.requested.filter(sceneId => !requestedSceneIds.existing.has(sceneId));
|
|
181
|
+
const filteredOutSceneIds = excludedSceneIds.filter(sceneId => requestedSceneIds.existing.has(sceneId));
|
|
182
|
+
|
|
183
|
+
const warnings = [];
|
|
184
|
+
|
|
185
|
+
if (notFoundSceneIds.length > 0) {
|
|
186
|
+
warnings.push({
|
|
187
|
+
type: "requested_scene_ids_not_found",
|
|
188
|
+
message: `${notFoundSceneIds.length} requested scene_id value(s) do not exist in project '${project_id}'.`,
|
|
189
|
+
scene_ids: notFoundSceneIds,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (filteredOutSceneIds.length > 0) {
|
|
194
|
+
warnings.push({
|
|
195
|
+
type: "requested_scene_ids_filtered_out",
|
|
196
|
+
message: `${filteredOutSceneIds.length} requested scene_id value(s) were excluded by additional filters.`,
|
|
197
|
+
scene_ids: filteredOutSceneIds,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const staleRows = rows.filter(row => Number(row.metadata_stale) === 1);
|
|
202
|
+
if (staleRows.length > 0) {
|
|
203
|
+
warnings.push({
|
|
204
|
+
type: "metadata_stale",
|
|
205
|
+
message: `${staleRows.length} scene(s) have stale metadata and may need re-enrichment before editorial use.`,
|
|
206
|
+
count: staleRows.length,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const missingOrderingRows = rows.filter(
|
|
211
|
+
row => row.part == null || row.chapter == null || row.timeline_position == null
|
|
212
|
+
);
|
|
213
|
+
if (missingOrderingRows.length > 0) {
|
|
214
|
+
warnings.push({
|
|
215
|
+
type: "missing_ordering_fields",
|
|
216
|
+
message: `${missingOrderingRows.length} scene(s) are missing part/chapter/timeline_position metadata; fallback ordering was applied.`,
|
|
217
|
+
count: missingOrderingRows.length,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const missingWordCountRows = rows.filter(row => row.word_count == null);
|
|
222
|
+
if (missingWordCountRows.length > 0) {
|
|
223
|
+
warnings.push({
|
|
224
|
+
type: "missing_word_count",
|
|
225
|
+
message: `${missingWordCountRows.length} scene(s) are missing word_count; estimated_word_count may be low.`,
|
|
226
|
+
count: missingWordCountRows.length,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const blockers = [];
|
|
231
|
+
if (strictness === "fail" && staleRows.length > 0) {
|
|
232
|
+
blockers.push({
|
|
233
|
+
code: "STALE_METADATA",
|
|
234
|
+
message: `${staleRows.length} scene(s) are marked metadata_stale.`,
|
|
235
|
+
scene_ids: staleRows.map(row => row.scene_id),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const estimatedWordCount = rows.reduce((sum, row) => {
|
|
240
|
+
const count = Number(row.word_count);
|
|
241
|
+
return sum + (Number.isFinite(count) ? count : 0);
|
|
242
|
+
}, 0);
|
|
243
|
+
|
|
244
|
+
const safeBundleName = slugifyBundleName(bundle_name || `${project_id}-${profile}`);
|
|
245
|
+
const appliedFilters = {
|
|
246
|
+
...(part !== undefined ? { part } : {}),
|
|
247
|
+
...(chapter !== undefined ? { chapter } : {}),
|
|
248
|
+
...(tag ? { tag } : {}),
|
|
249
|
+
...(Array.isArray(scene_ids) ? { scene_ids } : {}),
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
ok: true,
|
|
254
|
+
profile,
|
|
255
|
+
resolved_scope: {
|
|
256
|
+
project_id,
|
|
257
|
+
filters: appliedFilters,
|
|
258
|
+
options: {
|
|
259
|
+
include_scene_ids: Boolean(include_scene_ids),
|
|
260
|
+
include_metadata_sidebar: Boolean(include_metadata_sidebar),
|
|
261
|
+
include_paragraph_anchors: Boolean(include_paragraph_anchors),
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
ordering: rows.map(row => ({
|
|
265
|
+
scene_id: row.scene_id,
|
|
266
|
+
project_id: row.project_id,
|
|
267
|
+
title: row.title,
|
|
268
|
+
part: row.part,
|
|
269
|
+
chapter: row.chapter,
|
|
270
|
+
timeline_position: row.timeline_position,
|
|
271
|
+
metadata_stale: Number(row.metadata_stale) === 1,
|
|
272
|
+
})),
|
|
273
|
+
summary: {
|
|
274
|
+
scene_count: rows.length,
|
|
275
|
+
estimated_word_count: estimatedWordCount,
|
|
276
|
+
excluded_scene_ids: excludedSceneIds,
|
|
277
|
+
},
|
|
278
|
+
warnings,
|
|
279
|
+
warning_summary: buildWarningSummary(warnings),
|
|
280
|
+
strictness_result: {
|
|
281
|
+
strictness,
|
|
282
|
+
can_proceed: blockers.length === 0,
|
|
283
|
+
blockers,
|
|
284
|
+
},
|
|
285
|
+
planned_outputs: [
|
|
286
|
+
`${safeBundleName}.md`,
|
|
287
|
+
`${safeBundleName}.manifest.json`,
|
|
288
|
+
],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
@@ -52,7 +52,6 @@ function normalizeImportResult(importResult) {
|
|
|
52
52
|
function normalizeMergeResult(mergeResult) {
|
|
53
53
|
return {
|
|
54
54
|
ok: true,
|
|
55
|
-
beta: true,
|
|
56
55
|
merge: {
|
|
57
56
|
source_project_dir: mergeResult.scrivPath,
|
|
58
57
|
sync_dir: mergeResult.mcpSyncDir,
|
|
@@ -77,10 +76,7 @@ function normalizeMergeResult(mergeResult) {
|
|
|
77
76
|
},
|
|
78
77
|
},
|
|
79
78
|
sync: null,
|
|
80
|
-
warnings: [
|
|
81
|
-
"BETA_FEATURE: Direct Scrivener project parsing may be sensitive to Scrivener internal format changes.",
|
|
82
|
-
"If this fails, use import_scrivener_sync with an External Folder Sync export as the stable fallback.",
|
|
83
|
-
],
|
|
79
|
+
warnings: [],
|
|
84
80
|
};
|
|
85
81
|
}
|
|
86
82
|
|