@hanna84/mcp-writing 3.4.4 → 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 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.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
+
7
11
  #### [v3.4.4](https://github.com/hannasdev/mcp-writing/compare/v3.4.3...v3.4.4)
8
12
 
13
+ > 8 May 2026
14
+
9
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)
10
17
 
11
18
  #### [v3.4.3](https://github.com/hannasdev/mcp-writing/compare/v3.4.2...v3.4.3)
12
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanna84/mcp-writing",
3
- "version": "3.4.4",
3
+ "version": "3.5.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,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
- size: "Letter",
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(Buffer.concat(chunks));
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: ${generatedAt ?? new Date().toISOString()}`, { align: "left" });
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 { renderBetaNoticeMarkdown, renderBetaFeedbackFormMarkdown };
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, renderReviewBundlePdf, renderBetaNoticeMarkdown, renderBetaFeedbackFormMarkdown } from "./review-bundles-renderer.js";
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
- pdfBuffer = await renderReviewBundlePdf(dbHandle, plan, { generatedAt, syncDir });
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
  });