@hanna84/mcp-writing 2.9.5 → 2.9.7
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 +9 -863
- package/package.json +1 -1
- package/tools/review-bundles.js +207 -0
- package/tools/styleguide.js +687 -0
package/index.js
CHANGED
|
@@ -13,39 +13,16 @@ import yaml from "js-yaml";
|
|
|
13
13
|
import { z } from "zod";
|
|
14
14
|
import { openDb } from "./db.js";
|
|
15
15
|
import { syncAll, isSyncDirWritable, getSyncOwnershipDiagnostics, getFileWriteDiagnostics, readMeta, indexSceneFile, sidecarPath, isStructuralProjectId } from "./sync.js";
|
|
16
|
-
import { isGitAvailable, isGitRepository, initGitRepository, createSnapshot, listSnapshots, getSceneProseAtCommit
|
|
16
|
+
import { isGitAvailable, isGitRepository, initGitRepository, createSnapshot, listSnapshots, getSceneProseAtCommit } from "./git.js";
|
|
17
17
|
import { renderCharacterArcTemplate, renderCharacterSheetTemplate, renderPlaceSheetTemplate, slugifyEntityName } from "./world-entity-templates.js";
|
|
18
|
-
import { validateProjectId } from "./importer.js";
|
|
19
18
|
import { ASYNC_PROGRESS_PREFIX } from "./async-progress.js";
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
STYLEGUIDE_ENUMS,
|
|
23
|
-
buildStyleguideConfigDraft,
|
|
24
|
-
previewStyleguideConfigUpdate,
|
|
25
|
-
resolveStyleguideConfig,
|
|
26
|
-
summarizeStyleguideConfig,
|
|
27
|
-
updateStyleguideConfig,
|
|
28
|
-
} from "./prose-styleguide.js";
|
|
29
|
-
import {
|
|
30
|
-
detectStyleguideSignals,
|
|
31
|
-
analyzeSceneStyleguideDrift,
|
|
32
|
-
suggestStyleguideUpdatesFromScenes,
|
|
33
|
-
} from "./prose-styleguide-drift.js";
|
|
34
|
-
import {
|
|
35
|
-
PROSE_STYLEGUIDE_SKILL_BASENAME,
|
|
36
|
-
PROSE_STYLEGUIDE_SKILL_DIRNAME,
|
|
37
|
-
buildProseStyleguideSkill,
|
|
38
|
-
} from "./prose-styleguide-skill.js";
|
|
39
|
-
import {
|
|
40
|
-
REVIEW_BUNDLE_PROFILES,
|
|
41
|
-
REVIEW_BUNDLE_STRICTNESS,
|
|
42
|
-
ReviewBundlePlanError,
|
|
43
|
-
buildReviewBundlePlan,
|
|
44
|
-
createReviewBundleArtifacts,
|
|
45
|
-
} from "./review-bundles.js";
|
|
19
|
+
import { STYLEGUIDE_CONFIG_BASENAME } from "./prose-styleguide.js";
|
|
20
|
+
import { ReviewBundlePlanError } from "./review-bundles.js";
|
|
46
21
|
import { registerSyncTools } from "./tools/sync.js";
|
|
47
22
|
import { registerSearchTools } from "./tools/search.js";
|
|
48
23
|
import { registerMetadataTools } from "./tools/metadata.js";
|
|
24
|
+
import { registerReviewBundleTools } from "./tools/review-bundles.js";
|
|
25
|
+
import { registerStyleguideTools } from "./tools/styleguide.js";
|
|
49
26
|
|
|
50
27
|
const SYNC_DIR = process.env.WRITING_SYNC_DIR ?? "./sync";
|
|
51
28
|
const DB_PATH = process.env.DB_PATH ?? "./writing.db";
|
|
@@ -1043,10 +1020,14 @@ function createMcpServer() {
|
|
|
1043
1020
|
readSupportingNotesForEntity,
|
|
1044
1021
|
readEntityMetadata,
|
|
1045
1022
|
createCanonicalWorldEntity,
|
|
1023
|
+
resolveOutputDirWithinSync,
|
|
1024
|
+
isPathCandidateInsideSyncDir,
|
|
1046
1025
|
};
|
|
1047
1026
|
registerSyncTools(s, toolContext);
|
|
1048
1027
|
registerSearchTools(s, toolContext);
|
|
1049
1028
|
registerMetadataTools(s, toolContext);
|
|
1029
|
+
registerReviewBundleTools(s, toolContext);
|
|
1030
|
+
registerStyleguideTools(s, toolContext);
|
|
1050
1031
|
|
|
1051
1032
|
// ---- get_runtime_config --------------------------------------------------
|
|
1052
1033
|
s.tool(
|
|
@@ -1071,841 +1052,6 @@ function createMcpServer() {
|
|
|
1071
1052
|
);
|
|
1072
1053
|
|
|
1073
1054
|
// ---- prose styleguide ---------------------------------------------------
|
|
1074
|
-
s.tool(
|
|
1075
|
-
"setup_prose_styleguide_config",
|
|
1076
|
-
"Create prose-styleguide.config.yaml at sync root or project root using language defaults plus optional explicit overrides.",
|
|
1077
|
-
{
|
|
1078
|
-
scope: z.enum(["sync_root", "project_root"]).optional().describe("Config write target scope. Defaults to project_root when project_id is supplied, otherwise sync_root."),
|
|
1079
|
-
project_id: z.string().optional().describe("Project ID when writing project_root config (e.g. 'the-lamb' or 'universe-1/book-1')."),
|
|
1080
|
-
language: z.enum(STYLEGUIDE_ENUMS.language).describe("Primary writing language. Seeds language-specific defaults."),
|
|
1081
|
-
overrides: z.object({
|
|
1082
|
-
spelling: z.enum(STYLEGUIDE_ENUMS.spelling).optional(),
|
|
1083
|
-
quotation_style: z.enum(STYLEGUIDE_ENUMS.quotation_style).optional(),
|
|
1084
|
-
quotation_style_nested: z.enum(STYLEGUIDE_ENUMS.quotation_style_nested).optional(),
|
|
1085
|
-
em_dash_spacing: z.enum(STYLEGUIDE_ENUMS.em_dash_spacing).optional(),
|
|
1086
|
-
ellipsis_style: z.enum(STYLEGUIDE_ENUMS.ellipsis_style).optional(),
|
|
1087
|
-
abbreviation_periods: z.enum(STYLEGUIDE_ENUMS.abbreviation_periods).optional(),
|
|
1088
|
-
oxford_comma: z.enum(STYLEGUIDE_ENUMS.oxford_comma).optional(),
|
|
1089
|
-
numbers: z.enum(STYLEGUIDE_ENUMS.numbers).optional(),
|
|
1090
|
-
date_format: z.enum(STYLEGUIDE_ENUMS.date_format).optional(),
|
|
1091
|
-
time_format: z.enum(STYLEGUIDE_ENUMS.time_format).optional(),
|
|
1092
|
-
tense: z.string().optional(),
|
|
1093
|
-
pov: z.enum(STYLEGUIDE_ENUMS.pov).optional(),
|
|
1094
|
-
dialogue_tags: z.enum(STYLEGUIDE_ENUMS.dialogue_tags).optional(),
|
|
1095
|
-
sentence_fragments: z.enum(STYLEGUIDE_ENUMS.sentence_fragments).optional(),
|
|
1096
|
-
}).optional().describe("Optional overrides layered on top of language defaults."),
|
|
1097
|
-
voice_notes: z.string().optional().describe("Optional freeform voice notes to include in config."),
|
|
1098
|
-
overwrite: z.boolean().optional().describe("If true, replaces an existing config file at the target location."),
|
|
1099
|
-
},
|
|
1100
|
-
async ({ scope, project_id, language, overrides = {}, voice_notes, overwrite = false }) => {
|
|
1101
|
-
const resolvedScope = scope ?? (project_id ? "project_root" : "sync_root");
|
|
1102
|
-
|
|
1103
|
-
if (project_id !== undefined) {
|
|
1104
|
-
const projectIdCheck = validateProjectId(project_id);
|
|
1105
|
-
if (!projectIdCheck.ok) {
|
|
1106
|
-
return errorResponse("INVALID_PROJECT_ID", projectIdCheck.reason, { project_id });
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
if (resolvedScope === "project_root" && !project_id) {
|
|
1111
|
-
return errorResponse(
|
|
1112
|
-
"PROJECT_ID_REQUIRED",
|
|
1113
|
-
"project_id is required when scope=project_root."
|
|
1114
|
-
);
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
if (!SYNC_DIR_WRITABLE) {
|
|
1118
|
-
return errorResponse(
|
|
1119
|
-
"SYNC_DIR_NOT_WRITABLE",
|
|
1120
|
-
"Cannot write styleguide config because WRITING_SYNC_DIR is not writable in this runtime.",
|
|
1121
|
-
{ sync_dir: SYNC_DIR_ABS }
|
|
1122
|
-
);
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
const targetPath = resolvedScope === "sync_root"
|
|
1126
|
-
? path.join(SYNC_DIR, STYLEGUIDE_CONFIG_BASENAME)
|
|
1127
|
-
: path.join(resolveProjectRoot(project_id), STYLEGUIDE_CONFIG_BASENAME);
|
|
1128
|
-
|
|
1129
|
-
if (!isPathCandidateInsideSyncDir(targetPath)) {
|
|
1130
|
-
return errorResponse(
|
|
1131
|
-
"INVALID_CONFIG_PATH",
|
|
1132
|
-
"Resolved styleguide config path must be inside WRITING_SYNC_DIR.",
|
|
1133
|
-
{ target_path: path.resolve(targetPath), sync_dir: SYNC_DIR_ABS }
|
|
1134
|
-
);
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
if (fs.existsSync(targetPath) && !overwrite) {
|
|
1138
|
-
return errorResponse(
|
|
1139
|
-
"STYLEGUIDE_CONFIG_EXISTS",
|
|
1140
|
-
"Styleguide config already exists at target path. Set overwrite=true to replace it.",
|
|
1141
|
-
{ target_path: path.resolve(targetPath) }
|
|
1142
|
-
);
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
const draft = buildStyleguideConfigDraft({
|
|
1146
|
-
language,
|
|
1147
|
-
overrides,
|
|
1148
|
-
voice_notes,
|
|
1149
|
-
});
|
|
1150
|
-
if (!draft.ok) {
|
|
1151
|
-
return errorResponse(
|
|
1152
|
-
draft.error.code,
|
|
1153
|
-
draft.error.message,
|
|
1154
|
-
draft.error.details
|
|
1155
|
-
);
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
1159
|
-
fs.writeFileSync(targetPath, yaml.dump(draft.config, { lineWidth: 120 }), "utf8");
|
|
1160
|
-
|
|
1161
|
-
return jsonResponse({
|
|
1162
|
-
ok: true,
|
|
1163
|
-
scope: resolvedScope,
|
|
1164
|
-
file_path: path.resolve(targetPath),
|
|
1165
|
-
config: draft.config,
|
|
1166
|
-
inferred_defaults: draft.inferred_defaults,
|
|
1167
|
-
warnings: draft.warnings,
|
|
1168
|
-
next_step: "Config created. Call update_prose_styleguide_config to apply field updates.",
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1171
|
-
);
|
|
1172
|
-
|
|
1173
|
-
s.tool(
|
|
1174
|
-
"get_prose_styleguide_config",
|
|
1175
|
-
"Resolve prose-styleguide.config.yaml with cascading precedence (sync root, then universe root, then project root). Applies language-derived defaults and nested quotation defaults when omitted.",
|
|
1176
|
-
{
|
|
1177
|
-
project_id: z.string().optional().describe("Optional project ID for project-scoped resolution (e.g. 'the-lamb' or 'universe-1/book-1')."),
|
|
1178
|
-
},
|
|
1179
|
-
async ({ project_id }) => {
|
|
1180
|
-
if (project_id !== undefined) {
|
|
1181
|
-
const projectIdCheck = validateProjectId(project_id);
|
|
1182
|
-
if (!projectIdCheck.ok) {
|
|
1183
|
-
return errorResponse("INVALID_PROJECT_ID", projectIdCheck.reason, { project_id });
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
const resolved = resolveStyleguideConfig({
|
|
1188
|
-
syncDir: SYNC_DIR,
|
|
1189
|
-
projectId: project_id,
|
|
1190
|
-
});
|
|
1191
|
-
|
|
1192
|
-
if (!resolved.ok) {
|
|
1193
|
-
return errorResponse(
|
|
1194
|
-
resolved.error.code,
|
|
1195
|
-
resolved.error.message,
|
|
1196
|
-
resolved.error.details
|
|
1197
|
-
);
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
return jsonResponse({
|
|
1201
|
-
ok: true,
|
|
1202
|
-
styleguide: resolved,
|
|
1203
|
-
next_step: resolved.setup_required
|
|
1204
|
-
? "No prose-styleguide.config.yaml was found. Call setup_prose_styleguide_config (with language e.g. 'en') to create one at sync root or project root."
|
|
1205
|
-
: "Config resolved successfully.",
|
|
1206
|
-
});
|
|
1207
|
-
}
|
|
1208
|
-
);
|
|
1209
|
-
|
|
1210
|
-
s.tool(
|
|
1211
|
-
"summarize_prose_styleguide_config",
|
|
1212
|
-
"Summarize the currently resolved prose styleguide config in plain language for review or confirmation.",
|
|
1213
|
-
{
|
|
1214
|
-
project_id: z.string().optional().describe("Optional project ID for project-scoped resolution (e.g. 'the-lamb' or 'universe-1/book-1')."),
|
|
1215
|
-
},
|
|
1216
|
-
async ({ project_id }) => {
|
|
1217
|
-
if (project_id !== undefined) {
|
|
1218
|
-
const projectIdCheck = validateProjectId(project_id);
|
|
1219
|
-
if (!projectIdCheck.ok) {
|
|
1220
|
-
return errorResponse("INVALID_PROJECT_ID", projectIdCheck.reason, { project_id });
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
const resolved = resolveStyleguideConfig({
|
|
1225
|
-
syncDir: SYNC_DIR,
|
|
1226
|
-
projectId: project_id,
|
|
1227
|
-
});
|
|
1228
|
-
if (!resolved.ok) {
|
|
1229
|
-
return errorResponse(
|
|
1230
|
-
resolved.error.code,
|
|
1231
|
-
resolved.error.message,
|
|
1232
|
-
resolved.error.details
|
|
1233
|
-
);
|
|
1234
|
-
}
|
|
1235
|
-
if (resolved.setup_required || !resolved.resolved_config) {
|
|
1236
|
-
return errorResponse(
|
|
1237
|
-
"STYLEGUIDE_CONFIG_REQUIRED",
|
|
1238
|
-
"Cannot summarize prose styleguide config before prose-styleguide.config.yaml is set up.",
|
|
1239
|
-
{
|
|
1240
|
-
project_id: project_id ?? null,
|
|
1241
|
-
next_step: "Run setup_prose_styleguide_config or bootstrap_prose_styleguide_config.",
|
|
1242
|
-
}
|
|
1243
|
-
);
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
const summary = summarizeStyleguideConfig({
|
|
1247
|
-
resolvedConfig: resolved.resolved_config,
|
|
1248
|
-
inferredDefaults: resolved.inferred_defaults,
|
|
1249
|
-
});
|
|
1250
|
-
if (!summary.ok) {
|
|
1251
|
-
return errorResponse(summary.error.code, summary.error.message);
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
return jsonResponse({
|
|
1255
|
-
ok: true,
|
|
1256
|
-
project_id: project_id ?? null,
|
|
1257
|
-
summary_text: summary.summary_text,
|
|
1258
|
-
summary_lines: summary.summary_lines,
|
|
1259
|
-
styleguide: resolved,
|
|
1260
|
-
});
|
|
1261
|
-
}
|
|
1262
|
-
);
|
|
1263
|
-
|
|
1264
|
-
s.tool(
|
|
1265
|
-
"bootstrap_prose_styleguide_config",
|
|
1266
|
-
"Detect dominant prose conventions from existing scenes and suggest initial prose-styleguide config values.",
|
|
1267
|
-
{
|
|
1268
|
-
project_id: z.string().describe("Project ID to analyze (e.g. 'the-lamb' or 'universe-1/book-1')."),
|
|
1269
|
-
scene_ids: z.array(z.string()).optional().describe("Optional scene_id allowlist to analyze."),
|
|
1270
|
-
part: z.number().int().optional().describe("Optional part filter."),
|
|
1271
|
-
chapter: z.number().int().optional().describe("Optional chapter filter."),
|
|
1272
|
-
max_scenes: z.number().int().positive().optional().describe("Maximum number of scenes to analyze (default: 50)."),
|
|
1273
|
-
min_agreement: z.number().min(0).max(1).optional().describe("Minimum agreement ratio for suggested fields (default: 0.6)."),
|
|
1274
|
-
min_evidence: z.number().int().positive().optional().describe("Minimum number of observed scenes per field before suggesting it (default: 3)."),
|
|
1275
|
-
include_scene_signals: z.boolean().optional().describe("If true, include per-scene detected signals in the response."),
|
|
1276
|
-
},
|
|
1277
|
-
async ({
|
|
1278
|
-
project_id,
|
|
1279
|
-
scene_ids,
|
|
1280
|
-
part,
|
|
1281
|
-
chapter,
|
|
1282
|
-
max_scenes = 50,
|
|
1283
|
-
min_agreement = 0.6,
|
|
1284
|
-
min_evidence = 3,
|
|
1285
|
-
include_scene_signals = false,
|
|
1286
|
-
}) => {
|
|
1287
|
-
const projectIdCheck = validateProjectId(project_id);
|
|
1288
|
-
if (!projectIdCheck.ok) {
|
|
1289
|
-
return errorResponse("INVALID_PROJECT_ID", projectIdCheck.reason, { project_id });
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
const targetResolution = resolveBatchTargetScenes(db, {
|
|
1293
|
-
projectId: project_id,
|
|
1294
|
-
sceneIds: scene_ids,
|
|
1295
|
-
part,
|
|
1296
|
-
chapter,
|
|
1297
|
-
onlyStale: false,
|
|
1298
|
-
});
|
|
1299
|
-
if (!targetResolution.ok) {
|
|
1300
|
-
return errorResponse(targetResolution.code, targetResolution.message, targetResolution.details);
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
const targetScenes = targetResolution.rows;
|
|
1304
|
-
if (targetScenes.length === 0) {
|
|
1305
|
-
return errorResponse(
|
|
1306
|
-
"NOT_FOUND",
|
|
1307
|
-
`No scenes were found for project '${project_id}' with the requested filters.`,
|
|
1308
|
-
{ project_id, scene_ids: scene_ids ?? null, part: part ?? null, chapter: chapter ?? null }
|
|
1309
|
-
);
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
if (targetScenes.length > max_scenes) {
|
|
1313
|
-
return errorResponse(
|
|
1314
|
-
"VALIDATION_ERROR",
|
|
1315
|
-
`Matched ${targetScenes.length} scenes, which exceeds max_scenes=${max_scenes}.`,
|
|
1316
|
-
{
|
|
1317
|
-
matched_scenes: targetScenes.length,
|
|
1318
|
-
max_scenes,
|
|
1319
|
-
project_id,
|
|
1320
|
-
next_step: maxScenesNextStep(targetScenes.length),
|
|
1321
|
-
}
|
|
1322
|
-
);
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
const sceneSignals = [];
|
|
1326
|
-
let unreadableScenes = 0;
|
|
1327
|
-
|
|
1328
|
-
for (const scene of targetScenes) {
|
|
1329
|
-
try {
|
|
1330
|
-
const raw = fs.readFileSync(scene.file_path, "utf8");
|
|
1331
|
-
const prose = matter(raw).content;
|
|
1332
|
-
sceneSignals.push({
|
|
1333
|
-
scene_id: scene.scene_id,
|
|
1334
|
-
observed: detectStyleguideSignals(prose),
|
|
1335
|
-
});
|
|
1336
|
-
} catch {
|
|
1337
|
-
unreadableScenes += 1;
|
|
1338
|
-
sceneSignals.push({
|
|
1339
|
-
scene_id: scene.scene_id,
|
|
1340
|
-
observed: {},
|
|
1341
|
-
});
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
const suggestedConfig = suggestStyleguideUpdatesFromScenes({
|
|
1346
|
-
sceneAnalyses: sceneSignals,
|
|
1347
|
-
resolvedConfig: null,
|
|
1348
|
-
minAgreement: min_agreement,
|
|
1349
|
-
minEvidence: min_evidence,
|
|
1350
|
-
});
|
|
1351
|
-
|
|
1352
|
-
return jsonResponse({
|
|
1353
|
-
ok: true,
|
|
1354
|
-
project_id,
|
|
1355
|
-
checked_scenes: sceneSignals.length,
|
|
1356
|
-
unreadable_scenes: unreadableScenes,
|
|
1357
|
-
suggested_config: suggestedConfig,
|
|
1358
|
-
next_step: `To apply: (1) If no project-scoped config exists yet, call setup_prose_styleguide_config first with scope=project_root, project_id=${project_id}, and language (e.g. 'en'). (2) Then call update_prose_styleguide_config with the fields from suggested_config you want to apply.`,
|
|
1359
|
-
scene_signals: include_scene_signals ? sceneSignals : undefined,
|
|
1360
|
-
});
|
|
1361
|
-
}
|
|
1362
|
-
);
|
|
1363
|
-
|
|
1364
|
-
s.tool(
|
|
1365
|
-
"update_prose_styleguide_config",
|
|
1366
|
-
"Update an existing prose-styleguide.config.yaml at sync-root or project-root scope by writing only explicit field changes.",
|
|
1367
|
-
{
|
|
1368
|
-
scope: z.enum(["sync_root", "project_root"]).describe("Config scope to update."),
|
|
1369
|
-
project_id: z.string().optional().describe("Project ID when updating project_root config (e.g. 'the-lamb' or 'universe-1/book-1')."),
|
|
1370
|
-
updates: z.object({
|
|
1371
|
-
language: z.enum(STYLEGUIDE_ENUMS.language).optional(),
|
|
1372
|
-
spelling: z.enum(STYLEGUIDE_ENUMS.spelling).optional(),
|
|
1373
|
-
quotation_style: z.enum(STYLEGUIDE_ENUMS.quotation_style).optional(),
|
|
1374
|
-
quotation_style_nested: z.enum(STYLEGUIDE_ENUMS.quotation_style_nested).optional(),
|
|
1375
|
-
em_dash_spacing: z.enum(STYLEGUIDE_ENUMS.em_dash_spacing).optional(),
|
|
1376
|
-
ellipsis_style: z.enum(STYLEGUIDE_ENUMS.ellipsis_style).optional(),
|
|
1377
|
-
abbreviation_periods: z.enum(STYLEGUIDE_ENUMS.abbreviation_periods).optional(),
|
|
1378
|
-
oxford_comma: z.enum(STYLEGUIDE_ENUMS.oxford_comma).optional(),
|
|
1379
|
-
numbers: z.enum(STYLEGUIDE_ENUMS.numbers).optional(),
|
|
1380
|
-
date_format: z.enum(STYLEGUIDE_ENUMS.date_format).optional(),
|
|
1381
|
-
time_format: z.enum(STYLEGUIDE_ENUMS.time_format).optional(),
|
|
1382
|
-
tense: z.string().optional(),
|
|
1383
|
-
pov: z.enum(STYLEGUIDE_ENUMS.pov).optional(),
|
|
1384
|
-
dialogue_tags: z.enum(STYLEGUIDE_ENUMS.dialogue_tags).optional(),
|
|
1385
|
-
sentence_fragments: z.enum(STYLEGUIDE_ENUMS.sentence_fragments).optional(),
|
|
1386
|
-
voice_notes: z.string().optional(),
|
|
1387
|
-
}).strict().describe("Explicit config field changes to write at the selected scope."),
|
|
1388
|
-
},
|
|
1389
|
-
async ({ scope, project_id, updates }) => {
|
|
1390
|
-
if (project_id !== undefined) {
|
|
1391
|
-
const projectIdCheck = validateProjectId(project_id);
|
|
1392
|
-
if (!projectIdCheck.ok) {
|
|
1393
|
-
return errorResponse("INVALID_PROJECT_ID", projectIdCheck.reason, { project_id });
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
if (scope === "project_root" && !project_id) {
|
|
1398
|
-
return errorResponse(
|
|
1399
|
-
"PROJECT_ID_REQUIRED",
|
|
1400
|
-
"project_id is required when scope=project_root."
|
|
1401
|
-
);
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
if (!SYNC_DIR_WRITABLE) {
|
|
1405
|
-
return errorResponse(
|
|
1406
|
-
"SYNC_DIR_NOT_WRITABLE",
|
|
1407
|
-
"Cannot update styleguide config because WRITING_SYNC_DIR is not writable in this runtime.",
|
|
1408
|
-
{ sync_dir: SYNC_DIR_ABS }
|
|
1409
|
-
);
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
const updated = updateStyleguideConfig({
|
|
1413
|
-
syncDir: SYNC_DIR,
|
|
1414
|
-
scope,
|
|
1415
|
-
projectId: project_id,
|
|
1416
|
-
updates,
|
|
1417
|
-
});
|
|
1418
|
-
if (!updated.ok) {
|
|
1419
|
-
return errorResponse(
|
|
1420
|
-
updated.error.code,
|
|
1421
|
-
updated.error.message,
|
|
1422
|
-
updated.error.details
|
|
1423
|
-
);
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
return jsonResponse({
|
|
1427
|
-
ok: true,
|
|
1428
|
-
scope: updated.scope,
|
|
1429
|
-
project_id: updated.project_id,
|
|
1430
|
-
file_path: path.resolve(updated.file_path),
|
|
1431
|
-
config: updated.config,
|
|
1432
|
-
changed_fields: updated.changed_fields,
|
|
1433
|
-
noop: Boolean(updated.noop),
|
|
1434
|
-
message: updated.message,
|
|
1435
|
-
warnings: updated.warnings,
|
|
1436
|
-
});
|
|
1437
|
-
}
|
|
1438
|
-
);
|
|
1439
|
-
|
|
1440
|
-
s.tool(
|
|
1441
|
-
"preview_prose_styleguide_config_update",
|
|
1442
|
-
"Preview how explicit updates would change an existing prose-styleguide.config.yaml without writing any files.",
|
|
1443
|
-
{
|
|
1444
|
-
scope: z.enum(["sync_root", "project_root"]).describe("Config scope to preview updates for."),
|
|
1445
|
-
project_id: z.string().optional().describe("Project ID when previewing project_root config updates (e.g. 'the-lamb' or 'universe-1/book-1')."),
|
|
1446
|
-
updates: z.object({
|
|
1447
|
-
language: z.enum(STYLEGUIDE_ENUMS.language).optional(),
|
|
1448
|
-
spelling: z.enum(STYLEGUIDE_ENUMS.spelling).optional(),
|
|
1449
|
-
quotation_style: z.enum(STYLEGUIDE_ENUMS.quotation_style).optional(),
|
|
1450
|
-
quotation_style_nested: z.enum(STYLEGUIDE_ENUMS.quotation_style_nested).optional(),
|
|
1451
|
-
em_dash_spacing: z.enum(STYLEGUIDE_ENUMS.em_dash_spacing).optional(),
|
|
1452
|
-
ellipsis_style: z.enum(STYLEGUIDE_ENUMS.ellipsis_style).optional(),
|
|
1453
|
-
abbreviation_periods: z.enum(STYLEGUIDE_ENUMS.abbreviation_periods).optional(),
|
|
1454
|
-
oxford_comma: z.enum(STYLEGUIDE_ENUMS.oxford_comma).optional(),
|
|
1455
|
-
numbers: z.enum(STYLEGUIDE_ENUMS.numbers).optional(),
|
|
1456
|
-
date_format: z.enum(STYLEGUIDE_ENUMS.date_format).optional(),
|
|
1457
|
-
time_format: z.enum(STYLEGUIDE_ENUMS.time_format).optional(),
|
|
1458
|
-
tense: z.string().optional(),
|
|
1459
|
-
pov: z.enum(STYLEGUIDE_ENUMS.pov).optional(),
|
|
1460
|
-
dialogue_tags: z.enum(STYLEGUIDE_ENUMS.dialogue_tags).optional(),
|
|
1461
|
-
sentence_fragments: z.enum(STYLEGUIDE_ENUMS.sentence_fragments).optional(),
|
|
1462
|
-
voice_notes: z.string().optional(),
|
|
1463
|
-
}).strict().describe("Explicit config field changes to preview at the selected scope."),
|
|
1464
|
-
},
|
|
1465
|
-
async ({ scope, project_id, updates }) => {
|
|
1466
|
-
if (project_id !== undefined) {
|
|
1467
|
-
const projectIdCheck = validateProjectId(project_id);
|
|
1468
|
-
if (!projectIdCheck.ok) {
|
|
1469
|
-
return errorResponse("INVALID_PROJECT_ID", projectIdCheck.reason, { project_id });
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
if (scope === "project_root" && !project_id) {
|
|
1474
|
-
return errorResponse(
|
|
1475
|
-
"PROJECT_ID_REQUIRED",
|
|
1476
|
-
"project_id is required when scope=project_root."
|
|
1477
|
-
);
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
const preview = previewStyleguideConfigUpdate({
|
|
1481
|
-
syncDir: SYNC_DIR,
|
|
1482
|
-
scope,
|
|
1483
|
-
projectId: project_id,
|
|
1484
|
-
updates,
|
|
1485
|
-
});
|
|
1486
|
-
if (!preview.ok) {
|
|
1487
|
-
return errorResponse(
|
|
1488
|
-
preview.error.code,
|
|
1489
|
-
preview.error.message,
|
|
1490
|
-
preview.error.details
|
|
1491
|
-
);
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
return jsonResponse({
|
|
1495
|
-
ok: true,
|
|
1496
|
-
scope: preview.scope,
|
|
1497
|
-
project_id: preview.project_id,
|
|
1498
|
-
file_path: path.resolve(preview.file_path),
|
|
1499
|
-
current_config: preview.current_config,
|
|
1500
|
-
next_config: preview.config,
|
|
1501
|
-
changed_fields: preview.changed_fields,
|
|
1502
|
-
noop: preview.changed_fields.length === 0,
|
|
1503
|
-
message: preview.changed_fields.length === 0
|
|
1504
|
-
? "No changes detected for requested styleguide updates."
|
|
1505
|
-
: "Preview generated.",
|
|
1506
|
-
warnings: preview.warnings,
|
|
1507
|
-
});
|
|
1508
|
-
}
|
|
1509
|
-
);
|
|
1510
|
-
|
|
1511
|
-
s.tool(
|
|
1512
|
-
"check_prose_styleguide_drift",
|
|
1513
|
-
"Detect styleguide drift by comparing declared config conventions against observed signals in scene prose.",
|
|
1514
|
-
{
|
|
1515
|
-
project_id: z.string().describe("Project ID to analyze (e.g. 'the-lamb' or 'universe-1/book-1')."),
|
|
1516
|
-
scene_ids: z.array(z.string()).optional().describe("Optional scene_id allowlist to analyze."),
|
|
1517
|
-
part: z.number().int().optional().describe("Optional part filter."),
|
|
1518
|
-
chapter: z.number().int().optional().describe("Optional chapter filter."),
|
|
1519
|
-
max_scenes: z.number().int().positive().optional().describe("Maximum number of scenes to analyze (default: 50)."),
|
|
1520
|
-
min_agreement: z.number().min(0).max(1).optional().describe("Minimum agreement ratio for suggested updates (default: 0.6)."),
|
|
1521
|
-
include_clean_scenes: z.boolean().optional().describe("If true, include scenes with no detected drift in scene_results."),
|
|
1522
|
-
},
|
|
1523
|
-
async ({
|
|
1524
|
-
project_id,
|
|
1525
|
-
scene_ids,
|
|
1526
|
-
part,
|
|
1527
|
-
chapter,
|
|
1528
|
-
max_scenes = 50,
|
|
1529
|
-
min_agreement = 0.6,
|
|
1530
|
-
include_clean_scenes = false,
|
|
1531
|
-
}) => {
|
|
1532
|
-
const projectIdCheck = validateProjectId(project_id);
|
|
1533
|
-
if (!projectIdCheck.ok) {
|
|
1534
|
-
return errorResponse("INVALID_PROJECT_ID", projectIdCheck.reason, { project_id });
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
const resolved = resolveStyleguideConfig({
|
|
1538
|
-
syncDir: SYNC_DIR,
|
|
1539
|
-
projectId: project_id,
|
|
1540
|
-
});
|
|
1541
|
-
if (!resolved.ok) {
|
|
1542
|
-
return errorResponse(
|
|
1543
|
-
resolved.error.code,
|
|
1544
|
-
resolved.error.message,
|
|
1545
|
-
resolved.error.details
|
|
1546
|
-
);
|
|
1547
|
-
}
|
|
1548
|
-
if (resolved.setup_required || !resolved.resolved_config) {
|
|
1549
|
-
return errorResponse(
|
|
1550
|
-
"STYLEGUIDE_CONFIG_REQUIRED",
|
|
1551
|
-
"Cannot check prose styleguide drift before prose-styleguide.config.yaml is set up.",
|
|
1552
|
-
{
|
|
1553
|
-
project_id,
|
|
1554
|
-
next_step: "Run setup_prose_styleguide_config or bootstrap_prose_styleguide_config.",
|
|
1555
|
-
}
|
|
1556
|
-
);
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
const targetResolution = resolveBatchTargetScenes(db, {
|
|
1560
|
-
projectId: project_id,
|
|
1561
|
-
sceneIds: scene_ids,
|
|
1562
|
-
part,
|
|
1563
|
-
chapter,
|
|
1564
|
-
onlyStale: false,
|
|
1565
|
-
});
|
|
1566
|
-
if (!targetResolution.ok) {
|
|
1567
|
-
return errorResponse(targetResolution.code, targetResolution.message, targetResolution.details);
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
const targetScenes = targetResolution.rows;
|
|
1571
|
-
if (targetScenes.length > max_scenes) {
|
|
1572
|
-
return errorResponse(
|
|
1573
|
-
"VALIDATION_ERROR",
|
|
1574
|
-
`Matched ${targetScenes.length} scenes, which exceeds max_scenes=${max_scenes}.`,
|
|
1575
|
-
{
|
|
1576
|
-
matched_scenes: targetScenes.length,
|
|
1577
|
-
max_scenes,
|
|
1578
|
-
project_id,
|
|
1579
|
-
next_step: maxScenesNextStep(targetScenes.length),
|
|
1580
|
-
}
|
|
1581
|
-
);
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
const sceneAnalyses = [];
|
|
1585
|
-
for (const scene of targetScenes) {
|
|
1586
|
-
let prose;
|
|
1587
|
-
try {
|
|
1588
|
-
const raw = fs.readFileSync(scene.file_path, "utf8");
|
|
1589
|
-
prose = matter(raw).content;
|
|
1590
|
-
} catch {
|
|
1591
|
-
sceneAnalyses.push({
|
|
1592
|
-
scene_id: scene.scene_id,
|
|
1593
|
-
observed: {},
|
|
1594
|
-
drift: [{ field: "scene_file", declared: "readable", observed: "unreadable" }],
|
|
1595
|
-
});
|
|
1596
|
-
continue;
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
const analysis = analyzeSceneStyleguideDrift({
|
|
1600
|
-
prose,
|
|
1601
|
-
resolvedConfig: resolved.resolved_config,
|
|
1602
|
-
});
|
|
1603
|
-
sceneAnalyses.push({
|
|
1604
|
-
scene_id: scene.scene_id,
|
|
1605
|
-
observed: analysis.observed,
|
|
1606
|
-
drift: analysis.drift,
|
|
1607
|
-
});
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
const suggestedUpdates = suggestStyleguideUpdatesFromScenes({
|
|
1611
|
-
sceneAnalyses,
|
|
1612
|
-
resolvedConfig: resolved.resolved_config,
|
|
1613
|
-
minAgreement: min_agreement,
|
|
1614
|
-
});
|
|
1615
|
-
|
|
1616
|
-
const filteredScenes = include_clean_scenes
|
|
1617
|
-
? sceneAnalyses
|
|
1618
|
-
: sceneAnalyses.filter((scene) => scene.drift.length > 0);
|
|
1619
|
-
|
|
1620
|
-
const driftByField = {};
|
|
1621
|
-
for (const scene of sceneAnalyses) {
|
|
1622
|
-
for (const entry of scene.drift) {
|
|
1623
|
-
driftByField[entry.field] = (driftByField[entry.field] ?? 0) + 1;
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
return jsonResponse({
|
|
1628
|
-
ok: true,
|
|
1629
|
-
project_id,
|
|
1630
|
-
checked_scenes: sceneAnalyses.length,
|
|
1631
|
-
scenes_with_drift: sceneAnalyses.filter((scene) => scene.drift.length > 0).length,
|
|
1632
|
-
drift_by_field: driftByField,
|
|
1633
|
-
scene_results: filteredScenes,
|
|
1634
|
-
suggested_updates: suggestedUpdates,
|
|
1635
|
-
});
|
|
1636
|
-
}
|
|
1637
|
-
);
|
|
1638
|
-
|
|
1639
|
-
s.tool(
|
|
1640
|
-
"setup_prose_styleguide_skill",
|
|
1641
|
-
"Generate skills/prose-styleguide.md from the resolved prose styleguide config and universal craft rules.",
|
|
1642
|
-
{
|
|
1643
|
-
project_id: z.string().optional().describe("Optional project ID for scoped config resolution (e.g. 'the-lamb' or 'universe-1/book-1')."),
|
|
1644
|
-
overwrite: z.boolean().optional().describe("If true, replaces an existing skills/prose-styleguide.md file."),
|
|
1645
|
-
},
|
|
1646
|
-
async ({ project_id, overwrite = false }) => {
|
|
1647
|
-
if (project_id !== undefined) {
|
|
1648
|
-
const projectIdCheck = validateProjectId(project_id);
|
|
1649
|
-
if (!projectIdCheck.ok) {
|
|
1650
|
-
return errorResponse("INVALID_PROJECT_ID", projectIdCheck.reason, { project_id });
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
if (!SYNC_DIR_WRITABLE) {
|
|
1655
|
-
return errorResponse(
|
|
1656
|
-
"SYNC_DIR_NOT_WRITABLE",
|
|
1657
|
-
"Cannot write prose styleguide skill because WRITING_SYNC_DIR is not writable in this runtime.",
|
|
1658
|
-
{ sync_dir: SYNC_DIR_ABS }
|
|
1659
|
-
);
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
const resolved = resolveStyleguideConfig({
|
|
1663
|
-
syncDir: SYNC_DIR,
|
|
1664
|
-
projectId: project_id,
|
|
1665
|
-
});
|
|
1666
|
-
if (!resolved.ok) {
|
|
1667
|
-
return errorResponse(
|
|
1668
|
-
resolved.error.code,
|
|
1669
|
-
resolved.error.message,
|
|
1670
|
-
resolved.error.details
|
|
1671
|
-
);
|
|
1672
|
-
}
|
|
1673
|
-
if (resolved.setup_required || !resolved.resolved_config) {
|
|
1674
|
-
return errorResponse(
|
|
1675
|
-
"STYLEGUIDE_CONFIG_REQUIRED",
|
|
1676
|
-
"Cannot generate prose-styleguide.md before prose-styleguide.config.yaml is set up.",
|
|
1677
|
-
{
|
|
1678
|
-
project_id: project_id ?? null,
|
|
1679
|
-
next_step: "Run setup_prose_styleguide_config or bootstrap_prose_styleguide_config first.",
|
|
1680
|
-
}
|
|
1681
|
-
);
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
const skillPath = path.join(SYNC_DIR, PROSE_STYLEGUIDE_SKILL_DIRNAME, PROSE_STYLEGUIDE_SKILL_BASENAME);
|
|
1685
|
-
if (!isPathCandidateInsideSyncDir(skillPath)) {
|
|
1686
|
-
return errorResponse(
|
|
1687
|
-
"INVALID_SKILL_PATH",
|
|
1688
|
-
"Resolved prose styleguide skill path must be inside WRITING_SYNC_DIR.",
|
|
1689
|
-
{ target_path: path.resolve(skillPath), sync_dir: SYNC_DIR_ABS }
|
|
1690
|
-
);
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
if (fs.existsSync(skillPath) && !overwrite) {
|
|
1694
|
-
return errorResponse(
|
|
1695
|
-
"STYLEGUIDE_SKILL_EXISTS",
|
|
1696
|
-
"skills/prose-styleguide.md already exists. Set overwrite=true to replace it.",
|
|
1697
|
-
{ target_path: path.resolve(skillPath) }
|
|
1698
|
-
);
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
const generated = buildProseStyleguideSkill({
|
|
1702
|
-
resolvedConfig: resolved.resolved_config,
|
|
1703
|
-
sources: resolved.sources,
|
|
1704
|
-
projectId: project_id ?? null,
|
|
1705
|
-
});
|
|
1706
|
-
if (!generated.ok) {
|
|
1707
|
-
return errorResponse(generated.error.code, generated.error.message);
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
fs.mkdirSync(path.dirname(skillPath), { recursive: true });
|
|
1711
|
-
fs.writeFileSync(skillPath, generated.markdown, "utf8");
|
|
1712
|
-
|
|
1713
|
-
return jsonResponse({
|
|
1714
|
-
ok: true,
|
|
1715
|
-
file_path: path.resolve(skillPath),
|
|
1716
|
-
project_id: project_id ?? null,
|
|
1717
|
-
injected_rules: generated.injected_rules,
|
|
1718
|
-
source_count: resolved.sources.length,
|
|
1719
|
-
});
|
|
1720
|
-
}
|
|
1721
|
-
);
|
|
1722
|
-
|
|
1723
|
-
// ---- preview_review_bundle ----------------------------------------------
|
|
1724
|
-
s.tool(
|
|
1725
|
-
"preview_review_bundle",
|
|
1726
|
-
"Dry-run planning tool for review bundles. Resolves scene scope, deterministic ordering, warnings, and planned output filenames without writing files. Rendering options are accepted for API consistency and reflected in resolved_scope.options, but do not change planning output.",
|
|
1727
|
-
{
|
|
1728
|
-
project_id: z.string().describe("Project ID to scope the review bundle (e.g. 'test-novel')."),
|
|
1729
|
-
profile: z.enum(REVIEW_BUNDLE_PROFILES).describe("Bundle profile: outline_discussion, editor_detailed, or beta_reader_personalized."),
|
|
1730
|
-
part: z.number().int().optional().describe("Optional part filter."),
|
|
1731
|
-
chapter: z.number().int().optional().describe("Optional chapter filter."),
|
|
1732
|
-
tag: z.string().optional().describe("Optional tag filter (exact match)."),
|
|
1733
|
-
scene_ids: z.array(z.string()).optional().describe("Optional explicit scene_id allowlist. Intersects with other filters."),
|
|
1734
|
-
strictness: z.enum(REVIEW_BUNDLE_STRICTNESS).optional().describe("Strictness mode: warn (default) or fail."),
|
|
1735
|
-
include_scene_ids: z.boolean().optional().describe("Rendering option (default true). Echoed in resolved_scope.options for downstream rendering; does not change planning results."),
|
|
1736
|
-
include_metadata_sidebar: z.boolean().optional().describe("Rendering option (default false). Echoed in resolved_scope.options for downstream rendering; does not change planning results."),
|
|
1737
|
-
include_paragraph_anchors: z.boolean().optional().describe("Rendering option (default false). Echoed in resolved_scope.options for downstream rendering; does not change planning results."),
|
|
1738
|
-
recipient_name: z.string().optional().describe("Optional recipient display name for beta_reader_personalized profile."),
|
|
1739
|
-
bundle_name: z.string().optional().describe("Optional output bundle base name override (slugified in planned outputs)."),
|
|
1740
|
-
format: z.enum(["pdf", "markdown", "both"]).optional().describe("Planned output format: pdf (default), markdown, or both. Affects planned_outputs filenames only; preview_review_bundle does not render artifacts."),
|
|
1741
|
-
},
|
|
1742
|
-
async ({
|
|
1743
|
-
project_id,
|
|
1744
|
-
profile,
|
|
1745
|
-
part,
|
|
1746
|
-
chapter,
|
|
1747
|
-
tag,
|
|
1748
|
-
scene_ids,
|
|
1749
|
-
strictness = "warn",
|
|
1750
|
-
include_scene_ids = true,
|
|
1751
|
-
include_metadata_sidebar = false,
|
|
1752
|
-
include_paragraph_anchors = false,
|
|
1753
|
-
recipient_name,
|
|
1754
|
-
bundle_name,
|
|
1755
|
-
format = "pdf",
|
|
1756
|
-
}) => {
|
|
1757
|
-
const projectIdCheck = validateProjectId(project_id);
|
|
1758
|
-
if (!projectIdCheck.ok) {
|
|
1759
|
-
return errorResponse("INVALID_PROJECT_ID", projectIdCheck.reason, { project_id });
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
try {
|
|
1763
|
-
const plan = buildReviewBundlePlan(db, {
|
|
1764
|
-
project_id,
|
|
1765
|
-
profile,
|
|
1766
|
-
part,
|
|
1767
|
-
chapter,
|
|
1768
|
-
tag,
|
|
1769
|
-
scene_ids,
|
|
1770
|
-
strictness,
|
|
1771
|
-
include_scene_ids,
|
|
1772
|
-
include_metadata_sidebar,
|
|
1773
|
-
include_paragraph_anchors,
|
|
1774
|
-
recipient_name,
|
|
1775
|
-
bundle_name,
|
|
1776
|
-
format,
|
|
1777
|
-
});
|
|
1778
|
-
return jsonResponse(plan);
|
|
1779
|
-
} catch (error) {
|
|
1780
|
-
if (error instanceof ReviewBundlePlanError) {
|
|
1781
|
-
return errorResponse(error.code, error.message, error.details);
|
|
1782
|
-
}
|
|
1783
|
-
return errorResponse(
|
|
1784
|
-
"PREVIEW_FAILED",
|
|
1785
|
-
error instanceof Error ? error.message : "Failed to generate review bundle preview."
|
|
1786
|
-
);
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
);
|
|
1790
|
-
|
|
1791
|
-
// ---- create_review_bundle -----------------------------------------------
|
|
1792
|
-
s.tool(
|
|
1793
|
-
"create_review_bundle",
|
|
1794
|
-
"Generate review bundle artifacts (PDF/markdown) from planned scene scope. Writes files only under output_dir and returns manifest/provenance details.",
|
|
1795
|
-
{
|
|
1796
|
-
project_id: z.string().describe("Project ID to scope the review bundle (e.g. 'test-novel')."),
|
|
1797
|
-
profile: z.enum(REVIEW_BUNDLE_PROFILES).describe("Bundle profile: outline_discussion, editor_detailed, or beta_reader_personalized."),
|
|
1798
|
-
output_dir: z.string().describe("Directory path to write bundle artifacts into."),
|
|
1799
|
-
part: z.number().int().optional().describe("Optional part filter."),
|
|
1800
|
-
chapter: z.number().int().optional().describe("Optional chapter filter."),
|
|
1801
|
-
tag: z.string().optional().describe("Optional tag filter (exact match)."),
|
|
1802
|
-
scene_ids: z.array(z.string()).optional().describe("Optional explicit scene_id allowlist. Intersects with other filters."),
|
|
1803
|
-
strictness: z.enum(REVIEW_BUNDLE_STRICTNESS).optional().describe("Strictness mode: warn (default) or fail."),
|
|
1804
|
-
include_scene_ids: z.boolean().optional().describe("Include scene IDs in headings (default true). Applies to both PDF and markdown."),
|
|
1805
|
-
include_metadata_sidebar: z.boolean().optional().describe("Include metadata sidebar in markdown output (default false). Markdown only — no effect on PDF."),
|
|
1806
|
-
include_paragraph_anchors: z.boolean().optional().describe("Include paragraph anchors in markdown output (default false). Markdown only — no effect on PDF."),
|
|
1807
|
-
recipient_name: z.string().optional().describe("Optional recipient display name for beta_reader_personalized profile."),
|
|
1808
|
-
bundle_name: z.string().optional().describe("Optional output bundle base name override (slugified in filenames)."),
|
|
1809
|
-
source_commit: z.string().optional().describe("Optional explicit source commit for provenance. Defaults to current HEAD when available."),
|
|
1810
|
-
format: z.enum(["pdf", "markdown", "both"]).optional().describe("Output format: pdf (default), markdown, or both."),
|
|
1811
|
-
},
|
|
1812
|
-
async ({
|
|
1813
|
-
project_id,
|
|
1814
|
-
profile,
|
|
1815
|
-
output_dir,
|
|
1816
|
-
part,
|
|
1817
|
-
chapter,
|
|
1818
|
-
tag,
|
|
1819
|
-
scene_ids,
|
|
1820
|
-
strictness = "warn",
|
|
1821
|
-
include_scene_ids = true,
|
|
1822
|
-
include_metadata_sidebar = false,
|
|
1823
|
-
include_paragraph_anchors = false,
|
|
1824
|
-
recipient_name,
|
|
1825
|
-
bundle_name,
|
|
1826
|
-
source_commit,
|
|
1827
|
-
format = "pdf",
|
|
1828
|
-
}) => {
|
|
1829
|
-
const projectIdCheck = validateProjectId(project_id);
|
|
1830
|
-
if (!projectIdCheck.ok) {
|
|
1831
|
-
return errorResponse("INVALID_PROJECT_ID", projectIdCheck.reason, { project_id });
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
try {
|
|
1835
|
-
const { resolvedOutputDir, relativeToSyncDir } = resolveOutputDirWithinSync(output_dir);
|
|
1836
|
-
const outputDirSegments = relativeToSyncDir
|
|
1837
|
-
.split(path.sep)
|
|
1838
|
-
.filter(Boolean)
|
|
1839
|
-
.map(segment => segment.toLowerCase());
|
|
1840
|
-
if (outputDirSegments.includes("scenes")) {
|
|
1841
|
-
return errorResponse(
|
|
1842
|
-
"INVALID_OUTPUT_DIR",
|
|
1843
|
-
"output_dir cannot be inside a scenes directory. Choose a dedicated export folder under WRITING_SYNC_DIR.",
|
|
1844
|
-
{ output_dir: resolvedOutputDir }
|
|
1845
|
-
);
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
const plan = buildReviewBundlePlan(db, {
|
|
1849
|
-
project_id,
|
|
1850
|
-
profile,
|
|
1851
|
-
part,
|
|
1852
|
-
chapter,
|
|
1853
|
-
tag,
|
|
1854
|
-
scene_ids,
|
|
1855
|
-
strictness,
|
|
1856
|
-
include_scene_ids,
|
|
1857
|
-
include_metadata_sidebar,
|
|
1858
|
-
include_paragraph_anchors,
|
|
1859
|
-
recipient_name,
|
|
1860
|
-
bundle_name,
|
|
1861
|
-
format,
|
|
1862
|
-
});
|
|
1863
|
-
|
|
1864
|
-
if (!plan.strictness_result.can_proceed) {
|
|
1865
|
-
return errorResponse(
|
|
1866
|
-
"STRICTNESS_BLOCKED",
|
|
1867
|
-
"Bundle generation blocked by strictness policy.",
|
|
1868
|
-
{ strictness_result: plan.strictness_result, warning_summary: plan.warning_summary }
|
|
1869
|
-
);
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
const provenanceCommit = source_commit ?? (GIT_ENABLED ? getHeadCommitHash(SYNC_DIR) : null);
|
|
1873
|
-
const artifacts = await createReviewBundleArtifacts(db, {
|
|
1874
|
-
plan,
|
|
1875
|
-
output_dir: resolvedOutputDir,
|
|
1876
|
-
source_commit: provenanceCommit,
|
|
1877
|
-
syncDir: SYNC_DIR_ABS,
|
|
1878
|
-
});
|
|
1879
|
-
|
|
1880
|
-
return jsonResponse({
|
|
1881
|
-
ok: true,
|
|
1882
|
-
bundle_id: artifacts.bundle_id,
|
|
1883
|
-
output_paths: artifacts.output_paths,
|
|
1884
|
-
summary: {
|
|
1885
|
-
scene_count: plan.summary.scene_count,
|
|
1886
|
-
profile: plan.profile,
|
|
1887
|
-
applied_filters: plan.resolved_scope.filters,
|
|
1888
|
-
},
|
|
1889
|
-
warnings: plan.warnings,
|
|
1890
|
-
warning_summary: plan.warning_summary,
|
|
1891
|
-
provenance: {
|
|
1892
|
-
source_commit: provenanceCommit,
|
|
1893
|
-
generated_at: artifacts.generated_at,
|
|
1894
|
-
project_id: plan.resolved_scope.project_id,
|
|
1895
|
-
},
|
|
1896
|
-
});
|
|
1897
|
-
} catch (error) {
|
|
1898
|
-
if (error instanceof ReviewBundlePlanError) {
|
|
1899
|
-
return errorResponse(error.code, error.message, error.details);
|
|
1900
|
-
}
|
|
1901
|
-
return errorResponse(
|
|
1902
|
-
"CREATE_BUNDLE_FAILED",
|
|
1903
|
-
error instanceof Error ? error.message : "Failed to create review bundle artifacts."
|
|
1904
|
-
);
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
);
|
|
1908
|
-
|
|
1909
1055
|
// ---- PHASE 3: Prose Editing (git-backed) --------------------------------
|
|
1910
1056
|
|
|
1911
1057
|
// ---- propose_edit --------------------------------------------------------
|