@hanna84/mcp-writing 3.4.3 → 3.5.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/review-bundles/review-bundles-planner.js +50 -0
- package/src/review-bundles/review-bundles-renderer.js +120 -8
- package/src/review-bundles/review-bundles-writer.js +6 -2
- package/src/review-bundles/review-bundles.js +4 -0
- package/src/tools/review-bundles.js +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,23 @@ 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.5.0](https://github.com/hannasdev/mcp-writing/compare/v3.4.4...v3.5.0)
|
|
8
|
+
|
|
9
|
+
- feat: add accountable beta-reader bundle fingerprinting [`#182`](https://github.com/hannasdev/mcp-writing/pull/182)
|
|
10
|
+
|
|
11
|
+
#### [v3.4.4](https://github.com/hannasdev/mcp-writing/compare/v3.4.3...v3.4.4)
|
|
12
|
+
|
|
13
|
+
> 8 May 2026
|
|
14
|
+
|
|
15
|
+
- chore(copilot): tune instructions based on analysis [`#181`](https://github.com/hannasdev/mcp-writing/pull/181)
|
|
16
|
+
- Release 3.4.4 [`c76d472`](https://github.com/hannasdev/mcp-writing/commit/c76d47214be566dd52d5260f97b29e078e1047b6)
|
|
17
|
+
|
|
7
18
|
#### [v3.4.3](https://github.com/hannasdev/mcp-writing/compare/v3.4.2...v3.4.3)
|
|
8
19
|
|
|
20
|
+
> 7 May 2026
|
|
21
|
+
|
|
9
22
|
- chore: track VS Code existing-config setup rollout and add guardrail test [`#180`](https://github.com/hannasdev/mcp-writing/pull/180)
|
|
23
|
+
- Release 3.4.3 [`8cfa011`](https://github.com/hannasdev/mcp-writing/commit/8cfa011e523b746055f984bb6fc61de1881eb0a3)
|
|
10
24
|
|
|
11
25
|
#### [v3.4.2](https://github.com/hannasdev/mcp-writing/compare/v3.4.1...v3.4.2)
|
|
12
26
|
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const MAX_SORT_VALUE = Number.MAX_SAFE_INTEGER;
|
|
2
2
|
const MAX_SCENE_ID_FILTER_PARAMS = 900;
|
|
3
|
+
const MAX_CHAPTER_FILTER_PARAMS = 900;
|
|
3
4
|
|
|
4
5
|
export const REVIEW_BUNDLE_PROFILES = ["outline_discussion", "editor_detailed", "beta_reader_personalized"];
|
|
5
6
|
export const REVIEW_BUNDLE_STRICTNESS = ["warn", "fail"];
|
|
@@ -124,12 +125,14 @@ export function buildReviewBundlePlan(dbHandle, {
|
|
|
124
125
|
profile,
|
|
125
126
|
part,
|
|
126
127
|
chapter,
|
|
128
|
+
chapters,
|
|
127
129
|
tag,
|
|
128
130
|
scene_ids,
|
|
129
131
|
strictness = "warn",
|
|
130
132
|
include_scene_ids = true,
|
|
131
133
|
include_metadata_sidebar = false,
|
|
132
134
|
include_paragraph_anchors = false,
|
|
135
|
+
beta_accountability,
|
|
133
136
|
bundle_name,
|
|
134
137
|
recipient_name,
|
|
135
138
|
format = "pdf",
|
|
@@ -148,10 +151,46 @@ export function buildReviewBundlePlan(dbHandle, {
|
|
|
148
151
|
}
|
|
149
152
|
);
|
|
150
153
|
}
|
|
154
|
+
if (Array.isArray(chapters) && chapters.length > MAX_CHAPTER_FILTER_PARAMS) {
|
|
155
|
+
throw new ReviewBundlePlanError(
|
|
156
|
+
"CHAPTERS_FILTER_TOO_LARGE",
|
|
157
|
+
`chapters supports at most ${MAX_CHAPTER_FILTER_PARAMS} entries per request.`,
|
|
158
|
+
{
|
|
159
|
+
max_chapters: MAX_CHAPTER_FILTER_PARAMS,
|
|
160
|
+
received_chapters: chapters.length,
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
}
|
|
151
164
|
|
|
152
165
|
assertProfile(profile);
|
|
153
166
|
assertStrictness(strictness);
|
|
154
167
|
assertFormat(format);
|
|
168
|
+
if (chapter !== undefined && chapters !== undefined) {
|
|
169
|
+
throw new ReviewBundlePlanError(
|
|
170
|
+
"INVALID_CHAPTER_FILTER",
|
|
171
|
+
"Use either chapter or chapters, not both.",
|
|
172
|
+
{ chapter, chapters }
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
let normalizedChapters;
|
|
176
|
+
if (chapters !== undefined) {
|
|
177
|
+
if (!Array.isArray(chapters) || chapters.length === 0) {
|
|
178
|
+
throw new ReviewBundlePlanError(
|
|
179
|
+
"INVALID_CHAPTER_FILTER",
|
|
180
|
+
"chapters must be a non-empty array of integers when provided.",
|
|
181
|
+
{ chapters }
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (!chapters.every(value => Number.isInteger(value))) {
|
|
185
|
+
throw new ReviewBundlePlanError(
|
|
186
|
+
"INVALID_CHAPTER_FILTER",
|
|
187
|
+
"chapters must contain only integer chapter numbers.",
|
|
188
|
+
{ chapters }
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
// Normalize to a stable set to avoid plan drift from duplicated or reordered values.
|
|
192
|
+
normalizedChapters = Array.from(new Set(chapters)).sort((a, b) => a - b);
|
|
193
|
+
}
|
|
155
194
|
|
|
156
195
|
const projectRow = dbHandle.prepare(`SELECT project_id FROM projects WHERE project_id = ?`).get(project_id);
|
|
157
196
|
if (!projectRow) {
|
|
@@ -181,6 +220,11 @@ export function buildReviewBundlePlan(dbHandle, {
|
|
|
181
220
|
conditions.push("s.chapter = ?");
|
|
182
221
|
conditionParams.push(chapter);
|
|
183
222
|
}
|
|
223
|
+
if (Array.isArray(normalizedChapters) && normalizedChapters.length > 0) {
|
|
224
|
+
const placeholders = normalizedChapters.map(() => "?").join(",");
|
|
225
|
+
conditions.push(`s.chapter IN (${placeholders})`);
|
|
226
|
+
conditionParams.push(...normalizedChapters);
|
|
227
|
+
}
|
|
184
228
|
|
|
185
229
|
let query = `
|
|
186
230
|
SELECT DISTINCT
|
|
@@ -213,6 +257,7 @@ export function buildReviewBundlePlan(dbHandle, {
|
|
|
213
257
|
filters: {
|
|
214
258
|
...(part !== undefined ? { part } : {}),
|
|
215
259
|
...(chapter !== undefined ? { chapter } : {}),
|
|
260
|
+
...(Array.isArray(normalizedChapters) ? { chapters: normalizedChapters } : {}),
|
|
216
261
|
...(tag ? { tag } : {}),
|
|
217
262
|
...(Array.isArray(scene_ids) ? { scene_ids } : {}),
|
|
218
263
|
},
|
|
@@ -293,9 +338,13 @@ export function buildReviewBundlePlan(dbHandle, {
|
|
|
293
338
|
const appliedFilters = {
|
|
294
339
|
...(part !== undefined ? { part } : {}),
|
|
295
340
|
...(chapter !== undefined ? { chapter } : {}),
|
|
341
|
+
...(Array.isArray(normalizedChapters) ? { chapters: normalizedChapters } : {}),
|
|
296
342
|
...(tag ? { tag } : {}),
|
|
297
343
|
...(Array.isArray(scene_ids) ? { scene_ids } : {}),
|
|
298
344
|
};
|
|
345
|
+
const resolvedBetaAccountability = profile === "beta_reader_personalized"
|
|
346
|
+
? Boolean(beta_accountability ?? true)
|
|
347
|
+
: false;
|
|
299
348
|
|
|
300
349
|
return {
|
|
301
350
|
ok: true,
|
|
@@ -307,6 +356,7 @@ export function buildReviewBundlePlan(dbHandle, {
|
|
|
307
356
|
include_scene_ids: Boolean(include_scene_ids),
|
|
308
357
|
include_metadata_sidebar: Boolean(include_metadata_sidebar),
|
|
309
358
|
include_paragraph_anchors: Boolean(include_paragraph_anchors),
|
|
359
|
+
beta_accountability: resolvedBetaAccountability,
|
|
310
360
|
...(resolvedRecipientName ? { recipient_name: resolvedRecipientName } : {}),
|
|
311
361
|
},
|
|
312
362
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
3
4
|
import matter from "gray-matter";
|
|
4
5
|
import PDFDocument from "pdfkit";
|
|
5
6
|
import { ReviewBundlePlanError, normalizeRecipientDisplayName } from "./review-bundles-planner.js";
|
|
@@ -296,6 +297,55 @@ function renderSceneBlock(scene, options) {
|
|
|
296
297
|
return parts.join("\n\n");
|
|
297
298
|
}
|
|
298
299
|
|
|
300
|
+
function normalizeFingerprintFilters(filters) {
|
|
301
|
+
const normalized = { ...(filters ?? {}) };
|
|
302
|
+
if (Array.isArray(normalized.scene_ids)) {
|
|
303
|
+
normalized.scene_ids = [...new Set(normalized.scene_ids.map(sceneId => String(sceneId)))].sort();
|
|
304
|
+
}
|
|
305
|
+
return normalized;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function stableSerializeForFingerprint(value) {
|
|
309
|
+
if (Array.isArray(value)) {
|
|
310
|
+
return value.map(item => stableSerializeForFingerprint(item));
|
|
311
|
+
}
|
|
312
|
+
if (value && typeof value === "object") {
|
|
313
|
+
return Object.keys(value)
|
|
314
|
+
.sort()
|
|
315
|
+
.reduce((result, key) => {
|
|
316
|
+
result[key] = stableSerializeForFingerprint(value[key]);
|
|
317
|
+
return result;
|
|
318
|
+
}, {});
|
|
319
|
+
}
|
|
320
|
+
return value;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function buildFingerprintSeed(plan, generatedAt, recipientDisplayName) {
|
|
324
|
+
const base = {
|
|
325
|
+
project_id: plan.resolved_scope?.project_id ?? "",
|
|
326
|
+
profile: plan.profile ?? "",
|
|
327
|
+
recipient_name: recipientDisplayName ?? "",
|
|
328
|
+
filters: normalizeFingerprintFilters(plan.resolved_scope?.filters),
|
|
329
|
+
scene_ids: (plan.ordering ?? []).map(row => row.scene_id),
|
|
330
|
+
generated_at: generatedAt ?? "",
|
|
331
|
+
};
|
|
332
|
+
return JSON.stringify(stableSerializeForFingerprint(base));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function buildFingerprintSeedHash(seed) {
|
|
336
|
+
return crypto.createHash("sha256").update(String(seed)).digest("hex");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function buildPageFingerprintToken({ seedHash, pageNumber }) {
|
|
340
|
+
const digest = crypto
|
|
341
|
+
.createHash("sha256")
|
|
342
|
+
.update(`${seedHash}|page:${pageNumber}`)
|
|
343
|
+
.digest("hex")
|
|
344
|
+
.slice(0, 12)
|
|
345
|
+
.toUpperCase();
|
|
346
|
+
return `BR-${digest}-P${String(pageNumber).padStart(3, "0")}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
299
349
|
export function renderReviewBundleMarkdown(dbHandle, plan, { generatedAt, syncDir: syncDirOpt } = {}) {
|
|
300
350
|
const profile = plan.profile;
|
|
301
351
|
const includeSceneIds = Boolean(plan.resolved_scope?.options?.include_scene_ids);
|
|
@@ -365,6 +415,11 @@ export function renderReviewBundleMarkdown(dbHandle, plan, { generatedAt, syncDi
|
|
|
365
415
|
}
|
|
366
416
|
|
|
367
417
|
export function renderReviewBundlePdf(dbHandle, plan, { generatedAt, syncDir: syncDirOpt } = {}) {
|
|
418
|
+
return renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt, syncDir: syncDirOpt })
|
|
419
|
+
.then(result => result.pdf_buffer);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt, syncDir: syncDirOpt } = {}) {
|
|
368
423
|
const profile = plan.profile;
|
|
369
424
|
const includeSceneIds = Boolean(plan.resolved_scope?.options?.include_scene_ids);
|
|
370
425
|
const syncDir = syncDirOpt ?? process.env.WRITING_SYNC_DIR ?? null;
|
|
@@ -373,16 +428,57 @@ export function renderReviewBundlePdf(dbHandle, plan, { generatedAt, syncDir: sy
|
|
|
373
428
|
const rows = loadBundleSceneRows(dbHandle, plan.resolved_scope.project_id, sceneIds);
|
|
374
429
|
const recipientName = plan.resolved_scope?.options?.recipient_name;
|
|
375
430
|
const recipientDisplayName = normalizeRecipientDisplayName(recipientName);
|
|
376
|
-
|
|
431
|
+
const betaAccountabilityEnabled = profile === "beta_reader_personalized"
|
|
432
|
+
&& Boolean(plan.resolved_scope?.options?.beta_accountability);
|
|
433
|
+
const effectiveGeneratedAt = generatedAt ?? new Date().toISOString();
|
|
434
|
+
const fingerprintSeed = betaAccountabilityEnabled
|
|
435
|
+
? buildFingerprintSeed(plan, effectiveGeneratedAt, recipientDisplayName)
|
|
436
|
+
: null;
|
|
437
|
+
const fingerprintSeedHash = fingerprintSeed ? buildFingerprintSeedHash(fingerprintSeed) : null;
|
|
438
|
+
const pageTokens = [];
|
|
439
|
+
let pageNumber = 0;
|
|
440
|
+
|
|
441
|
+
const pdfOptions = profile === "beta_reader_personalized"
|
|
442
|
+
? {
|
|
443
|
+
size: [432, 648], // 6x9in in PDF points
|
|
444
|
+
margins: { top: 64, right: 58, bottom: 72, left: 58 },
|
|
445
|
+
autoFirstPage: false,
|
|
446
|
+
}
|
|
447
|
+
: {
|
|
448
|
+
size: "Letter",
|
|
449
|
+
margin: 50,
|
|
450
|
+
autoFirstPage: false,
|
|
451
|
+
};
|
|
377
452
|
const doc = new PDFDocument({
|
|
378
|
-
|
|
379
|
-
margin: 50,
|
|
453
|
+
...pdfOptions,
|
|
380
454
|
});
|
|
381
455
|
|
|
456
|
+
const drawAccountabilityFooter = () => {
|
|
457
|
+
if (!betaAccountabilityEnabled || !fingerprintSeedHash) return;
|
|
458
|
+
pageNumber += 1;
|
|
459
|
+
const token = buildPageFingerprintToken({
|
|
460
|
+
seedHash: fingerprintSeedHash,
|
|
461
|
+
pageNumber,
|
|
462
|
+
});
|
|
463
|
+
pageTokens.push({ page: pageNumber, token });
|
|
464
|
+
const footerY = doc.page.height - doc.page.margins.bottom - 12;
|
|
465
|
+
const footerWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
|
466
|
+
const footerText = `For: ${recipientDisplayName} | Fingerprint: ${token} | Page ${pageNumber}`;
|
|
467
|
+
doc.save();
|
|
468
|
+
doc.font("Helvetica").fontSize(8).fillColor("#555555");
|
|
469
|
+
doc.text(footerText, doc.page.margins.left, footerY, {
|
|
470
|
+
width: footerWidth,
|
|
471
|
+
align: "left",
|
|
472
|
+
lineBreak: false,
|
|
473
|
+
});
|
|
474
|
+
doc.restore();
|
|
475
|
+
};
|
|
476
|
+
doc.on("pageAdded", drawAccountabilityFooter);
|
|
477
|
+
|
|
382
478
|
// Register listeners before any content is written so render-time errors
|
|
383
479
|
// always reject the returned Promise.
|
|
384
|
-
const chunks = [];
|
|
385
480
|
return new Promise((resolve, reject) => {
|
|
481
|
+
const chunks = [];
|
|
386
482
|
let settled = false;
|
|
387
483
|
const fail = (err) => {
|
|
388
484
|
if (settled) return;
|
|
@@ -395,10 +491,20 @@ export function renderReviewBundlePdf(dbHandle, plan, { generatedAt, syncDir: sy
|
|
|
395
491
|
doc.on("end", () => {
|
|
396
492
|
if (settled) return;
|
|
397
493
|
settled = true;
|
|
398
|
-
resolve(
|
|
494
|
+
resolve({
|
|
495
|
+
pdf_buffer: Buffer.concat(chunks),
|
|
496
|
+
fingerprint: betaAccountabilityEnabled
|
|
497
|
+
? {
|
|
498
|
+
mode: "visible_footer",
|
|
499
|
+
recipient_display_name: recipientDisplayName,
|
|
500
|
+
page_tokens: pageTokens,
|
|
501
|
+
}
|
|
502
|
+
: null,
|
|
503
|
+
});
|
|
399
504
|
});
|
|
400
505
|
|
|
401
506
|
try {
|
|
507
|
+
doc.addPage();
|
|
402
508
|
doc.fontSize(24).font("Helvetica-Bold").text(`Review Bundle: ${plan.resolved_scope.project_id}`, { align: "left" });
|
|
403
509
|
doc.moveDown(0.5);
|
|
404
510
|
doc.fontSize(11).font("Helvetica");
|
|
@@ -406,7 +512,7 @@ export function renderReviewBundlePdf(dbHandle, plan, { generatedAt, syncDir: sy
|
|
|
406
512
|
if (profile === "beta_reader_personalized") {
|
|
407
513
|
doc.text(`Recipient: ${recipientDisplayName}`, { align: "left" });
|
|
408
514
|
}
|
|
409
|
-
doc.text(`Generated: ${
|
|
515
|
+
doc.text(`Generated: ${effectiveGeneratedAt}`, { align: "left" });
|
|
410
516
|
doc.text(`Scenes: ${plan.summary.scene_count}`, { align: "left" });
|
|
411
517
|
doc.moveDown();
|
|
412
518
|
|
|
@@ -470,7 +576,7 @@ export function renderReviewBundlePdf(dbHandle, plan, { generatedAt, syncDir: sy
|
|
|
470
576
|
doc.text(prose, {
|
|
471
577
|
align: "left",
|
|
472
578
|
width: textWidth,
|
|
473
|
-
lineGap: 3,
|
|
579
|
+
lineGap: profile === "beta_reader_personalized" ? 4.5 : 3,
|
|
474
580
|
});
|
|
475
581
|
}
|
|
476
582
|
|
|
@@ -495,4 +601,10 @@ export function renderReviewBundlePdf(dbHandle, plan, { generatedAt, syncDir: sy
|
|
|
495
601
|
});
|
|
496
602
|
}
|
|
497
603
|
|
|
498
|
-
export {
|
|
604
|
+
export {
|
|
605
|
+
renderBetaNoticeMarkdown,
|
|
606
|
+
renderBetaFeedbackFormMarkdown,
|
|
607
|
+
buildPageFingerprintToken,
|
|
608
|
+
buildFingerprintSeed,
|
|
609
|
+
buildFingerprintSeedHash,
|
|
610
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { ReviewBundlePlanError } from "./review-bundles-planner.js";
|
|
4
|
-
import { renderReviewBundleMarkdown,
|
|
4
|
+
import { renderReviewBundleMarkdown, renderReviewBundlePdfWithMetadata, renderBetaNoticeMarkdown, renderBetaFeedbackFormMarkdown } from "./review-bundles-renderer.js";
|
|
5
5
|
|
|
6
6
|
function resolveOutputFilePath(outputDir, fileName) {
|
|
7
7
|
const normalizedOutputDir = path.resolve(outputDir);
|
|
@@ -82,8 +82,11 @@ export async function createReviewBundleArtifacts(dbHandle, {
|
|
|
82
82
|
const markdown = markdownPath ? renderReviewBundleMarkdown(dbHandle, plan, { generatedAt, syncDir }) : null;
|
|
83
83
|
|
|
84
84
|
let pdfBuffer = null;
|
|
85
|
+
let fingerprintMetadata = null;
|
|
85
86
|
if (pdfPath) {
|
|
86
|
-
|
|
87
|
+
const pdfResult = await renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt, syncDir });
|
|
88
|
+
pdfBuffer = pdfResult.pdf_buffer;
|
|
89
|
+
fingerprintMetadata = pdfResult.fingerprint;
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
const recipientName = plan.resolved_scope?.options?.recipient_name;
|
|
@@ -108,6 +111,7 @@ export async function createReviewBundleArtifacts(dbHandle, {
|
|
|
108
111
|
warnings: plan.warnings,
|
|
109
112
|
resolved_scope: plan.resolved_scope,
|
|
110
113
|
scene_ids: plan.ordering.map(row => row.scene_id),
|
|
114
|
+
...(fingerprintMetadata ? { fingerprint: fingerprintMetadata } : {}),
|
|
111
115
|
};
|
|
112
116
|
|
|
113
117
|
for (const outputPath of [markdownPath, pdfPath, manifestPath, noticePath, feedbackPath].filter(Boolean)) {
|
|
@@ -9,6 +9,10 @@ export {
|
|
|
9
9
|
export {
|
|
10
10
|
renderReviewBundleMarkdown,
|
|
11
11
|
renderReviewBundlePdf,
|
|
12
|
+
renderReviewBundlePdfWithMetadata,
|
|
13
|
+
buildPageFingerprintToken,
|
|
14
|
+
buildFingerprintSeed,
|
|
15
|
+
buildFingerprintSeedHash,
|
|
12
16
|
} from "./review-bundles-renderer.js";
|
|
13
17
|
|
|
14
18
|
export { createReviewBundleArtifacts } from "./review-bundles-writer.js";
|
|
@@ -28,6 +28,7 @@ export function registerReviewBundleTools(s, {
|
|
|
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
30
|
chapter: z.number().int().optional().describe("Optional chapter filter."),
|
|
31
|
+
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."),
|
|
31
32
|
tag: z.string().optional().describe("Optional tag filter (exact match)."),
|
|
32
33
|
scene_ids: z.array(z.string()).optional().describe("Optional explicit scene_id allowlist. Intersects with other filters."),
|
|
33
34
|
strictness: z.enum(REVIEW_BUNDLE_STRICTNESS).optional().describe("Strictness mode: warn (default) or fail."),
|
|
@@ -35,6 +36,7 @@ export function registerReviewBundleTools(s, {
|
|
|
35
36
|
include_metadata_sidebar: z.boolean().optional().describe("Rendering option (default false). Echoed in resolved_scope.options for downstream rendering; does not change planning results."),
|
|
36
37
|
include_paragraph_anchors: z.boolean().optional().describe("Rendering option (default false). Echoed in resolved_scope.options for downstream rendering; does not change planning results."),
|
|
37
38
|
recipient_name: z.string().optional().describe("Optional recipient display name for beta_reader_personalized profile."),
|
|
39
|
+
beta_accountability: z.boolean().optional().describe("Enable accountability footer + fingerprint metadata for beta_reader_personalized output (default true for beta profile)."),
|
|
38
40
|
bundle_name: z.string().optional().describe("Optional output bundle base name override (slugified in planned outputs)."),
|
|
39
41
|
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."),
|
|
40
42
|
},
|
|
@@ -43,6 +45,7 @@ export function registerReviewBundleTools(s, {
|
|
|
43
45
|
profile,
|
|
44
46
|
part,
|
|
45
47
|
chapter,
|
|
48
|
+
chapters,
|
|
46
49
|
tag,
|
|
47
50
|
scene_ids,
|
|
48
51
|
strictness = "warn",
|
|
@@ -50,6 +53,7 @@ export function registerReviewBundleTools(s, {
|
|
|
50
53
|
include_metadata_sidebar = false,
|
|
51
54
|
include_paragraph_anchors = false,
|
|
52
55
|
recipient_name,
|
|
56
|
+
beta_accountability,
|
|
53
57
|
bundle_name,
|
|
54
58
|
format = "pdf",
|
|
55
59
|
}) => {
|
|
@@ -64,6 +68,7 @@ export function registerReviewBundleTools(s, {
|
|
|
64
68
|
profile,
|
|
65
69
|
part,
|
|
66
70
|
chapter,
|
|
71
|
+
chapters,
|
|
67
72
|
tag,
|
|
68
73
|
scene_ids,
|
|
69
74
|
strictness,
|
|
@@ -71,6 +76,7 @@ export function registerReviewBundleTools(s, {
|
|
|
71
76
|
include_metadata_sidebar,
|
|
72
77
|
include_paragraph_anchors,
|
|
73
78
|
recipient_name,
|
|
79
|
+
beta_accountability,
|
|
74
80
|
bundle_name,
|
|
75
81
|
format,
|
|
76
82
|
});
|
|
@@ -110,6 +116,7 @@ export function registerReviewBundleTools(s, {
|
|
|
110
116
|
output_dir: z.string().describe("Directory path to write bundle artifacts into."),
|
|
111
117
|
part: z.number().int().optional().describe("Optional part filter."),
|
|
112
118
|
chapter: z.number().int().optional().describe("Optional chapter filter."),
|
|
119
|
+
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."),
|
|
113
120
|
tag: z.string().optional().describe("Optional tag filter (exact match)."),
|
|
114
121
|
scene_ids: z.array(z.string()).optional().describe("Optional explicit scene_id allowlist. Intersects with other filters."),
|
|
115
122
|
strictness: z.enum(REVIEW_BUNDLE_STRICTNESS).optional().describe("Strictness mode: warn (default) or fail."),
|
|
@@ -117,6 +124,7 @@ export function registerReviewBundleTools(s, {
|
|
|
117
124
|
include_metadata_sidebar: z.boolean().optional().describe("Include metadata sidebar in markdown output (default false). Markdown only — no effect on PDF."),
|
|
118
125
|
include_paragraph_anchors: z.boolean().optional().describe("Include paragraph anchors in markdown output (default false). Markdown only — no effect on PDF."),
|
|
119
126
|
recipient_name: z.string().optional().describe("Optional recipient display name for beta_reader_personalized profile."),
|
|
127
|
+
beta_accountability: z.boolean().optional().describe("Enable accountability footer + fingerprint metadata for beta_reader_personalized output (default true for beta profile)."),
|
|
120
128
|
bundle_name: z.string().optional().describe("Optional output bundle base name override (slugified in filenames)."),
|
|
121
129
|
source_commit: z.string().optional().describe("Optional explicit source commit for provenance. Defaults to current HEAD when available."),
|
|
122
130
|
format: z.enum(["pdf", "markdown", "both"]).optional().describe("Output format: pdf (default), markdown, or both."),
|
|
@@ -127,6 +135,7 @@ export function registerReviewBundleTools(s, {
|
|
|
127
135
|
output_dir,
|
|
128
136
|
part,
|
|
129
137
|
chapter,
|
|
138
|
+
chapters,
|
|
130
139
|
tag,
|
|
131
140
|
scene_ids,
|
|
132
141
|
strictness = "warn",
|
|
@@ -134,6 +143,7 @@ export function registerReviewBundleTools(s, {
|
|
|
134
143
|
include_metadata_sidebar = false,
|
|
135
144
|
include_paragraph_anchors = false,
|
|
136
145
|
recipient_name,
|
|
146
|
+
beta_accountability,
|
|
137
147
|
bundle_name,
|
|
138
148
|
source_commit,
|
|
139
149
|
format = "pdf",
|
|
@@ -162,6 +172,7 @@ export function registerReviewBundleTools(s, {
|
|
|
162
172
|
profile,
|
|
163
173
|
part,
|
|
164
174
|
chapter,
|
|
175
|
+
chapters,
|
|
165
176
|
tag,
|
|
166
177
|
scene_ids,
|
|
167
178
|
strictness,
|
|
@@ -169,6 +180,7 @@ export function registerReviewBundleTools(s, {
|
|
|
169
180
|
include_metadata_sidebar,
|
|
170
181
|
include_paragraph_anchors,
|
|
171
182
|
recipient_name,
|
|
183
|
+
beta_accountability,
|
|
172
184
|
bundle_name,
|
|
173
185
|
format,
|
|
174
186
|
});
|