@bendyline/docblocks-cli 1.1.1 → 2.0.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.
Files changed (114) hide show
  1. package/README.md +60 -76
  2. package/dist/commands/build.d.ts +11 -0
  3. package/dist/commands/build.d.ts.map +1 -1
  4. package/dist/commands/build.js +61 -45
  5. package/dist/commands/build.js.map +1 -1
  6. package/dist/commands/convert.d.ts +12 -2
  7. package/dist/commands/convert.d.ts.map +1 -1
  8. package/dist/commands/convert.js +272 -101
  9. package/dist/commands/convert.js.map +1 -1
  10. package/dist/commands/mcp.d.ts +1 -1
  11. package/dist/commands/mcp.d.ts.map +1 -1
  12. package/dist/commands/mcp.js +90 -4
  13. package/dist/commands/mcp.js.map +1 -1
  14. package/dist/commands/parse.js +1 -1
  15. package/dist/commands/parse.js.map +1 -1
  16. package/dist/commands/serve.d.ts +33 -0
  17. package/dist/commands/serve.d.ts.map +1 -1
  18. package/dist/commands/serve.js +340 -3
  19. package/dist/commands/serve.js.map +1 -1
  20. package/dist/commands/themes.js +1 -1
  21. package/dist/commands/themes.js.map +1 -1
  22. package/dist/commands/transforms.js +1 -1
  23. package/dist/commands/transforms.js.map +1 -1
  24. package/dist/commands/video.d.ts +8 -1
  25. package/dist/commands/video.d.ts.map +1 -1
  26. package/dist/commands/video.js +50 -30
  27. package/dist/commands/video.js.map +1 -1
  28. package/dist/contained-file.d.ts +8 -0
  29. package/dist/contained-file.d.ts.map +1 -0
  30. package/dist/contained-file.js +77 -0
  31. package/dist/contained-file.js.map +1 -0
  32. package/dist/index.js +3 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/mcp/agentic-tools.d.ts +16 -0
  35. package/dist/mcp/agentic-tools.d.ts.map +1 -0
  36. package/dist/mcp/agentic-tools.js +967 -0
  37. package/dist/mcp/agentic-tools.js.map +1 -0
  38. package/dist/mcp/artifact-store.d.ts +98 -0
  39. package/dist/mcp/artifact-store.d.ts.map +1 -0
  40. package/dist/mcp/artifact-store.js +542 -0
  41. package/dist/mcp/artifact-store.js.map +1 -0
  42. package/dist/mcp/authority.d.ts +53 -0
  43. package/dist/mcp/authority.d.ts.map +1 -0
  44. package/dist/mcp/authority.js +433 -0
  45. package/dist/mcp/authority.js.map +1 -0
  46. package/dist/mcp/conversion-service.d.ts +111 -0
  47. package/dist/mcp/conversion-service.d.ts.map +1 -0
  48. package/dist/mcp/conversion-service.js +512 -0
  49. package/dist/mcp/conversion-service.js.map +1 -0
  50. package/dist/mcp/discovery-tools.d.ts +4 -0
  51. package/dist/mcp/discovery-tools.d.ts.map +1 -0
  52. package/dist/mcp/discovery-tools.js +114 -0
  53. package/dist/mcp/discovery-tools.js.map +1 -0
  54. package/dist/mcp/document-service.d.ts +47 -0
  55. package/dist/mcp/document-service.d.ts.map +1 -0
  56. package/dist/mcp/document-service.js +424 -0
  57. package/dist/mcp/document-service.js.map +1 -0
  58. package/dist/mcp/error-result.d.ts +17 -0
  59. package/dist/mcp/error-result.d.ts.map +1 -0
  60. package/dist/mcp/error-result.js +82 -0
  61. package/dist/mcp/error-result.js.map +1 -0
  62. package/dist/mcp/intelligence.d.ts +15 -0
  63. package/dist/mcp/intelligence.d.ts.map +1 -0
  64. package/dist/mcp/intelligence.js +855 -0
  65. package/dist/mcp/intelligence.js.map +1 -0
  66. package/dist/mcp/output-bounds.d.ts +13 -0
  67. package/dist/mcp/output-bounds.d.ts.map +1 -0
  68. package/dist/mcp/output-bounds.js +61 -0
  69. package/dist/mcp/output-bounds.js.map +1 -0
  70. package/dist/mcp/preview-service.d.ts +54 -0
  71. package/dist/mcp/preview-service.d.ts.map +1 -0
  72. package/dist/mcp/preview-service.js +454 -0
  73. package/dist/mcp/preview-service.js.map +1 -0
  74. package/dist/mcp/progress.d.ts +18 -0
  75. package/dist/mcp/progress.d.ts.map +1 -0
  76. package/dist/mcp/progress.js +28 -0
  77. package/dist/mcp/progress.js.map +1 -0
  78. package/dist/mcp/prompts.d.ts +4 -0
  79. package/dist/mcp/prompts.d.ts.map +1 -0
  80. package/dist/mcp/prompts.js +124 -0
  81. package/dist/mcp/prompts.js.map +1 -0
  82. package/dist/mcp/rendered-conversion.d.ts +34 -0
  83. package/dist/mcp/rendered-conversion.d.ts.map +1 -0
  84. package/dist/mcp/rendered-conversion.js +364 -0
  85. package/dist/mcp/rendered-conversion.js.map +1 -0
  86. package/dist/mcp/server.d.ts +67 -10
  87. package/dist/mcp/server.d.ts.map +1 -1
  88. package/dist/mcp/server.js +228 -496
  89. package/dist/mcp/server.js.map +1 -1
  90. package/dist/preview-policy.d.ts +8 -0
  91. package/dist/preview-policy.d.ts.map +1 -0
  92. package/dist/preview-policy.js +85 -0
  93. package/dist/preview-policy.js.map +1 -0
  94. package/dist/render-html.d.ts +17 -0
  95. package/dist/render-html.d.ts.map +1 -0
  96. package/dist/render-html.js +85 -0
  97. package/dist/render-html.js.map +1 -0
  98. package/dist/version.d.ts +6 -0
  99. package/dist/version.d.ts.map +1 -0
  100. package/dist/version.js +104 -0
  101. package/dist/version.js.map +1 -0
  102. package/package.json +11 -11
  103. package/dist/converters/docx-to-md.d.ts +0 -10
  104. package/dist/converters/docx-to-md.d.ts.map +0 -1
  105. package/dist/converters/docx-to-md.js +0 -17
  106. package/dist/converters/docx-to-md.js.map +0 -1
  107. package/dist/converters/pdf-to-md.d.ts +0 -14
  108. package/dist/converters/pdf-to-md.d.ts.map +0 -1
  109. package/dist/converters/pdf-to-md.js +0 -51
  110. package/dist/converters/pdf-to-md.js.map +0 -1
  111. package/dist/converters/pptx-to-md.d.ts +0 -13
  112. package/dist/converters/pptx-to-md.d.ts.map +0 -1
  113. package/dist/converters/pptx-to-md.js +0 -67
  114. package/dist/converters/pptx-to-md.js.map +0 -1
@@ -0,0 +1,855 @@
1
+ import { DOCBLOCKS_MCP_WIRE_VERSION, MCP_WIRE_LIMITS } from '@bendyline/docblocks/mcp';
2
+ import { throwIfAborted, warningDiagnostic } from './document-service.js';
3
+ import { boundWireText, toWireIdentifier } from './output-bounds.js';
4
+ const DEFAULT_MAX_INSPECTION_BLOCKS = 200;
5
+ const MAX_INSPECTION_BLOCKS = 2000;
6
+ /** Producer budget keeps worst-case diagnostics comfortably within the default report quota. */
7
+ export const MCP_MAX_PUBLISHED_DIAGNOSTICS = 500;
8
+ const MAX_DIAGNOSTICS = MCP_MAX_PUBLISHED_DIAGNOSTICS;
9
+ const FORMAT_PATTERN = /^[a-z0-9][a-z0-9.+_-]{0,63}$/u;
10
+ const CANCELLATION_YIELD_INTERVAL = 256;
11
+ export async function inspectPreparedDocument(_documents, prepared, maximumBlocks = DEFAULT_MAX_INSPECTION_BLOCKS, cursor = null, signal) {
12
+ throwIfAborted(signal);
13
+ if (!Number.isSafeInteger(maximumBlocks) ||
14
+ maximumBlocks < 1 ||
15
+ maximumBlocks > MAX_INSPECTION_BLOCKS) {
16
+ throw new Error(`Inspection block limit must be between 1 and ${MAX_INSPECTION_BLOCKS}`);
17
+ }
18
+ const { analyzeBlocks } = await import('@bendyline/squisq/transform');
19
+ const { flattenBlocks, validateMarkdownSource } = await import('@bendyline/squisq/doc');
20
+ await yieldForCancellation(signal);
21
+ const assets = prepared.assets;
22
+ const assetPaths = new Set(assets.map((asset) => asset.path));
23
+ const validation = validateMarkdownSource(prepared.markdown, { assets: assetPaths });
24
+ const analyzed = analyzeBlocks(prepared.doc.blocks);
25
+ const flatBlocks = flattenBlocks(prepared.doc.blocks);
26
+ throwIfAborted(signal);
27
+ const blockOffset = parseInspectionCursor(cursor, analyzed.length);
28
+ const blockEnd = Math.min(analyzed.length, blockOffset + maximumBlocks);
29
+ const analyzedBlocks = analyzed.map((entry) => entry.block);
30
+ const sourceRanges = await computeBlockSourceRanges(analyzedBlocks, prepared.markdown.length, signal);
31
+ const blocks = analyzed.slice(blockOffset, blockEnd).map((entry, index) => ({
32
+ id: toWireIdentifier(entry.block.id, 'block'),
33
+ type: toWireIdentifier(entry.block.template ?? 'section', 'section'),
34
+ text: boundWireText(entry.plainText, MCP_WIRE_LIMITS.excerptCharacters),
35
+ wordCount: entry.bodyWordCount,
36
+ templateId: entry.block.template === undefined
37
+ ? null
38
+ : toWireIdentifier(entry.block.template, 'template'),
39
+ sourceRange: sourceRanges[blockOffset + index] ?? null,
40
+ }));
41
+ const outline = (await collectAnalyzedOutline(prepared.doc.blocks, analyzedBlocks, signal)).filter((entry) => entry.blockIndex >= blockOffset && entry.blockIndex < blockEnd);
42
+ const details = summarizeMarkdownNodes(prepared.markdownDoc);
43
+ await yieldForCancellation(signal);
44
+ const counts = { tables: details.tableCount, links: details.linkCount };
45
+ const itemSummary = summarizeItems(prepared, flatBlocks, details.tables);
46
+ const items = itemSummary.items;
47
+ const metadata = metadataFromFrontmatter(prepared.markdownDoc.frontmatter);
48
+ const diagnostics = [
49
+ ...prepared.diagnostics,
50
+ ...validation.diagnostics.map((diagnostic) => mapDocDiagnostic(diagnostic, prepared.sourceFormat)),
51
+ ...missingAltTextDiagnostics(prepared.markdownDoc, prepared.sourceFormat),
52
+ ];
53
+ let wordCount = 0;
54
+ for (let index = 0; index < analyzed.length; index += 1) {
55
+ wordCount += analyzed[index]?.bodyWordCount ?? 0;
56
+ const checkpoint = cancellationCheckpoint(signal, index);
57
+ if (checkpoint)
58
+ await checkpoint;
59
+ }
60
+ return {
61
+ version: DOCBLOCKS_MCP_WIRE_VERSION,
62
+ kind: 'inspection',
63
+ sourceFormat: prepared.sourceFormat,
64
+ metadata,
65
+ statistics: {
66
+ blockCount: analyzed.length,
67
+ wordCount,
68
+ characterCount: prepared.markdown.length,
69
+ tableCount: counts.tables,
70
+ linkCount: counts.links,
71
+ assetCount: assets.length,
72
+ pageCount: prepared.sourceFormat === 'pdf' ? items.length : null,
73
+ slideCount: prepared.sourceFormat === 'pptx' ? items.length : null,
74
+ sheetCount: prepared.sourceFormat === 'xlsx' || prepared.sourceFormat === 'csv' ? items.length : null,
75
+ },
76
+ outline,
77
+ blocks,
78
+ blockOffset,
79
+ nextCursor: blockEnd < analyzed.length ? `blocks:${blockEnd}` : null,
80
+ tables: details.tables,
81
+ links: details.links,
82
+ items,
83
+ assets,
84
+ theme: await themeSummary(prepared, signal),
85
+ truncated: blockEnd < analyzed.length,
86
+ detailsTruncated: details.truncated || itemSummary.truncated,
87
+ diagnostics: boundDiagnostics(diagnostics, 'inspect'),
88
+ };
89
+ }
90
+ export async function validatePreparedDocument(_documents, prepared, targetFormat, signal) {
91
+ throwIfAborted(signal);
92
+ const normalizedTargetFormat = normalizeTargetFormat(targetFormat);
93
+ const { validateMarkdownSource } = await import('@bendyline/squisq/doc');
94
+ await yieldForCancellation(signal);
95
+ const assets = prepared.assets;
96
+ const validation = validateMarkdownSource(prepared.markdown, {
97
+ assets: new Set(assets.map((asset) => asset.path)),
98
+ });
99
+ throwIfAborted(signal);
100
+ const diagnostics = [
101
+ ...prepared.diagnostics,
102
+ ...validation.diagnostics.map((diagnostic) => mapDocDiagnostic(diagnostic, prepared.sourceFormat)),
103
+ ...missingAltTextDiagnostics(prepared.markdownDoc, normalizedTargetFormat ?? prepared.sourceFormat),
104
+ ...(normalizedTargetFormat
105
+ ? await targetPreflight(prepared, normalizedTargetFormat, signal)
106
+ : []),
107
+ ];
108
+ await yieldForCancellation(signal);
109
+ const unique = boundDiagnostics(diagnostics, 'validate');
110
+ throwIfAborted(signal);
111
+ const summary = summarizeDiagnostics(unique);
112
+ return {
113
+ version: DOCBLOCKS_MCP_WIRE_VERSION,
114
+ kind: 'validation',
115
+ sourceFormat: prepared.sourceFormat,
116
+ targetFormat: normalizedTargetFormat,
117
+ valid: summary.errorCount === 0,
118
+ summary,
119
+ diagnostics: unique,
120
+ };
121
+ }
122
+ export async function comparePreparedDocuments(documents, left, right, signal) {
123
+ throwIfAborted(signal);
124
+ await yieldForCancellation(signal);
125
+ const [leftInspection, rightInspection] = await Promise.all([
126
+ inspectPreparedDocument(documents, left, MAX_INSPECTION_BLOCKS, null, signal),
127
+ inspectPreparedDocument(documents, right, MAX_INSPECTION_BLOCKS, null, signal),
128
+ ]);
129
+ throwIfAborted(signal);
130
+ const { stripMarkdown } = await import('@bendyline/squisq/generate');
131
+ const leftWords = words(stripMarkdown(left.markdown));
132
+ const rightWords = words(stripMarkdown(right.markdown));
133
+ await yieldForCancellation(signal);
134
+ const [textSimilarity, leftLayouts, rightLayouts, leftAccessibility, rightAccessibility] = await Promise.all([
135
+ multisetSimilarity(leftWords, rightWords, signal),
136
+ layoutTokens(left, signal),
137
+ layoutTokens(right, signal),
138
+ accessibilityTokens(left, signal),
139
+ accessibilityTokens(right, signal),
140
+ ]);
141
+ const structureSimilarity = ratioSimilarity(leftInspection.statistics.blockCount, rightInspection.statistics.blockCount);
142
+ const tableSimilarity = ratioSimilarity(leftInspection.statistics.tableCount, rightInspection.statistics.tableCount);
143
+ const mediaSimilarity = assetSimilarity(leftInspection.assets, rightInspection.assets);
144
+ const metadataSimilarity = objectSimilarity(leftInspection.metadata, rightInspection.metadata);
145
+ const themeSimilarity = leftInspection.theme?.id === rightInspection.theme?.id
146
+ ? 1
147
+ : leftInspection.theme === null && rightInspection.theme === null
148
+ ? 1
149
+ : 0;
150
+ const layoutSimilarity = await multisetSimilarity(leftLayouts, rightLayouts, signal);
151
+ const leftTiming = timingSummary(left);
152
+ const rightTiming = timingSummary(right);
153
+ const timingSimilarity = (ratioSimilarity(leftTiming.duration, rightTiming.duration) +
154
+ ratioSimilarity(leftTiming.segmentCount, rightTiming.segmentCount) +
155
+ ratioSimilarity(leftTiming.captionCount, rightTiming.captionCount)) /
156
+ 3;
157
+ const accessibilitySimilarity = await multisetSimilarity(leftAccessibility, rightAccessibility, signal);
158
+ throwIfAborted(signal);
159
+ const score = clamp01(textSimilarity * 0.4 +
160
+ structureSimilarity * 0.12 +
161
+ tableSimilarity * 0.06 +
162
+ mediaSimilarity * 0.12 +
163
+ layoutSimilarity * 0.08 +
164
+ metadataSimilarity * 0.05 +
165
+ themeSimilarity * 0.06 +
166
+ timingSimilarity * 0.05 +
167
+ accessibilitySimilarity * 0.06);
168
+ const metrics = [
169
+ metric('words', leftInspection.statistics.wordCount, rightInspection.statistics.wordCount, textSimilarity),
170
+ metric('blocks', leftInspection.statistics.blockCount, rightInspection.statistics.blockCount, structureSimilarity),
171
+ metric('tables', leftInspection.statistics.tableCount, rightInspection.statistics.tableCount, tableSimilarity),
172
+ metric('assets', leftInspection.statistics.assetCount, rightInspection.statistics.assetCount, mediaSimilarity),
173
+ metric('layouts', leftLayouts.length, rightLayouts.length, layoutSimilarity),
174
+ metric('durationSeconds', leftTiming.duration, rightTiming.duration, timingSimilarity),
175
+ metric('accessibilityFeatures', leftAccessibility.length, rightAccessibility.length, accessibilitySimilarity),
176
+ ];
177
+ const changes = [
178
+ change('text', textSimilarity, 'Text content', leftInspection.statistics.wordCount, rightInspection.statistics.wordCount),
179
+ change('structure', structureSimilarity, 'Document structure', leftInspection.statistics.blockCount, rightInspection.statistics.blockCount),
180
+ change('table', tableSimilarity, 'Tables', leftInspection.statistics.tableCount, rightInspection.statistics.tableCount),
181
+ change('media', mediaSimilarity, 'Media assets', leftInspection.statistics.assetCount, rightInspection.statistics.assetCount),
182
+ change('theme', themeSimilarity, 'Theme', leftInspection.theme ? 1 : 0, rightInspection.theme ? 1 : 0),
183
+ change('layout', layoutSimilarity, 'Layouts and templates', leftLayouts.length, rightLayouts.length),
184
+ change('metadata', metadataSimilarity, 'Document metadata', metadataFieldCount(leftInspection), metadataFieldCount(rightInspection)),
185
+ change('timing', timingSimilarity, 'Audio and caption timing', leftTiming.segmentCount, rightTiming.segmentCount),
186
+ change('accessibility', accessibilitySimilarity, 'Accessibility features', leftAccessibility.length, rightAccessibility.length),
187
+ ];
188
+ return {
189
+ version: DOCBLOCKS_MCP_WIRE_VERSION,
190
+ kind: 'comparison',
191
+ leftFormat: left.sourceFormat,
192
+ rightFormat: right.sourceFormat,
193
+ equivalent: score === 1 && changes.every((entry) => entry.status === 'retained'),
194
+ score,
195
+ changes,
196
+ metrics,
197
+ diagnostics: boundDiagnostics([...leftInspection.diagnostics, ...rightInspection.diagnostics], 'compare'),
198
+ };
199
+ }
200
+ async function collectAnalyzedOutline(blocks, analyzedBlocks, signal) {
201
+ const levels = new Map();
202
+ const pending = [...blocks].reverse().map((block) => ({ block, level: 1 }));
203
+ let visited = 0;
204
+ while (pending.length > 0) {
205
+ const current = pending.pop();
206
+ if (!current)
207
+ break;
208
+ levels.set(current.block, current.level);
209
+ const children = current.block.children ?? [];
210
+ for (let index = children.length - 1; index >= 0; index -= 1) {
211
+ const block = children[index];
212
+ if (block)
213
+ pending.push({ block, level: current.level + 1 });
214
+ }
215
+ const checkpoint = cancellationCheckpoint(signal, visited);
216
+ if (checkpoint)
217
+ await checkpoint;
218
+ visited += 1;
219
+ }
220
+ const outline = [];
221
+ for (let blockIndex = 0; blockIndex < analyzedBlocks.length; blockIndex += 1) {
222
+ const block = analyzedBlocks[blockIndex];
223
+ if (!block)
224
+ continue;
225
+ outline.push({
226
+ id: toWireIdentifier(block.id, 'block'),
227
+ title: boundWireText(block.title ?? block.sourceHeading?.children?.map(textFromNode).join('') ?? block.id, MCP_WIRE_LIMITS.labelCharacters, 'Untitled block'),
228
+ level: levels.get(block) ?? 1,
229
+ blockIndex,
230
+ });
231
+ const checkpoint = cancellationCheckpoint(signal, blockIndex);
232
+ if (checkpoint)
233
+ await checkpoint;
234
+ }
235
+ return outline;
236
+ }
237
+ function parseInspectionCursor(cursor, blockCount) {
238
+ if (cursor === null)
239
+ return 0;
240
+ const match = /^blocks:(0|[1-9][0-9]{0,9})$/u.exec(cursor);
241
+ if (!match)
242
+ throw new Error('Invalid inspection cursor');
243
+ const offset = Number(match[1]);
244
+ if (!Number.isSafeInteger(offset) || offset > blockCount) {
245
+ throw new Error('Inspection cursor is outside the document');
246
+ }
247
+ return offset;
248
+ }
249
+ /** Compute heading ranges in one reverse pass while allowing protocol cancellation to run. */
250
+ export async function computeBlockSourceRanges(blocks, markdownLength, signal) {
251
+ const starts = new Array(blocks.length);
252
+ for (let index = 0; index < blocks.length; index += 1) {
253
+ const block = blocks[index];
254
+ const position = block.sourceHeading?.position;
255
+ const offset = position?.start?.offset;
256
+ starts[index] =
257
+ typeof offset === 'number' && Number.isSafeInteger(offset) && offset >= 0
258
+ ? Math.min(offset, markdownLength)
259
+ : null;
260
+ const checkpoint = cancellationCheckpoint(signal, index);
261
+ if (checkpoint)
262
+ await checkpoint;
263
+ }
264
+ const ranges = new Array(starts.length);
265
+ let nextStart = markdownLength;
266
+ for (let index = starts.length - 1; index >= 0; index -= 1) {
267
+ const start = starts[index];
268
+ if (start === null || start === undefined) {
269
+ ranges[index] = null;
270
+ }
271
+ else {
272
+ ranges[index] = { start, end: nextStart };
273
+ nextStart = start;
274
+ }
275
+ const checkpoint = cancellationCheckpoint(signal, starts.length - index);
276
+ if (checkpoint)
277
+ await checkpoint;
278
+ }
279
+ return ranges;
280
+ }
281
+ function summarizeItems(prepared, blocks, tables) {
282
+ if (prepared.sourceFormat === 'pptx' || prepared.sourceFormat === 'pdf') {
283
+ const kind = prepared.sourceFormat === 'pptx' ? 'slide' : 'page';
284
+ const all = blocks.length > 0 ? blocks : [null];
285
+ return {
286
+ items: all.slice(0, 1000).map((block, index) => ({
287
+ kind,
288
+ index,
289
+ label: block?.title === undefined
290
+ ? `${kind} ${index + 1}`
291
+ : boundWireText(block.title, MCP_WIRE_LIMITS.labelCharacters),
292
+ blockIds: block ? [toWireIdentifier(block.id, 'block')] : [],
293
+ })),
294
+ truncated: all.length > 1000,
295
+ };
296
+ }
297
+ if (prepared.sourceFormat === 'xlsx' || prepared.sourceFormat === 'csv') {
298
+ const total = Math.max(1, tables.length);
299
+ return {
300
+ items: Array.from({ length: Math.min(total, 1000) }, (_unused, index) => ({
301
+ kind: 'sheet',
302
+ index,
303
+ label: tables[index]?.headers[0] === undefined
304
+ ? `Sheet ${index + 1}`
305
+ : boundWireText(tables[index].headers[0], MCP_WIRE_LIMITS.labelCharacters),
306
+ blockIds: [],
307
+ })),
308
+ truncated: total > 1000,
309
+ };
310
+ }
311
+ return { items: [], truncated: false };
312
+ }
313
+ function textFromNode(node) {
314
+ if (!node || typeof node !== 'object')
315
+ return '';
316
+ const candidate = node;
317
+ if (typeof candidate.value === 'string')
318
+ return candidate.value;
319
+ return Array.isArray(candidate.children) ? candidate.children.map(textFromNode).join('') : '';
320
+ }
321
+ function summarizeMarkdownNodes(markdownDoc) {
322
+ let tableCount = 0;
323
+ let linkCount = 0;
324
+ const tables = [];
325
+ const links = [];
326
+ const visit = (node) => {
327
+ if (!node || typeof node !== 'object')
328
+ return;
329
+ const candidate = node;
330
+ if (candidate.type === 'table') {
331
+ const index = tableCount;
332
+ tableCount += 1;
333
+ if (tables.length < 1000) {
334
+ const rows = Array.isArray(candidate.children) ? candidate.children : [];
335
+ const rowCells = rows.map((row) => {
336
+ if (!row || typeof row !== 'object')
337
+ return [];
338
+ const children = row.children;
339
+ return Array.isArray(children) ? children : [];
340
+ });
341
+ tables.push({
342
+ index,
343
+ rowCount: rowCells.length,
344
+ columnCount: rowCells.reduce((maximum, cells) => Math.max(maximum, cells.length), 0),
345
+ headers: (rowCells[0] ?? [])
346
+ .slice(0, MCP_WIRE_LIMITS.arrayEntries)
347
+ .map((cell) => boundWireText(textFromNode(cell), MCP_WIRE_LIMITS.labelCharacters)),
348
+ });
349
+ }
350
+ }
351
+ if (candidate.type === 'link' || candidate.type === 'linkReference') {
352
+ const index = linkCount;
353
+ linkCount += 1;
354
+ if (links.length < 1000) {
355
+ const target = typeof candidate.url === 'string'
356
+ ? candidate.url
357
+ : typeof candidate.identifier === 'string'
358
+ ? `reference:${candidate.identifier}`
359
+ : 'unknown:';
360
+ links.push({
361
+ index,
362
+ text: boundWireText(textFromNode(candidate), MCP_WIRE_LIMITS.labelCharacters),
363
+ target: boundWireText(target, MCP_WIRE_LIMITS.uriCharacters, 'unknown:'),
364
+ });
365
+ }
366
+ }
367
+ if (Array.isArray(candidate.children))
368
+ candidate.children.forEach(visit);
369
+ };
370
+ visit(markdownDoc);
371
+ return {
372
+ tableCount,
373
+ linkCount,
374
+ tables,
375
+ links,
376
+ truncated: tableCount > tables.length || linkCount > links.length,
377
+ };
378
+ }
379
+ function metadataFromFrontmatter(frontmatter) {
380
+ return {
381
+ title: stringField(frontmatter, 'title', MCP_WIRE_LIMITS.labelCharacters),
382
+ author: stringField(frontmatter, 'author', MCP_WIRE_LIMITS.labelCharacters),
383
+ description: stringField(frontmatter, 'description', MCP_WIRE_LIMITS.messageCharacters),
384
+ };
385
+ }
386
+ function stringField(frontmatter, key, maximumCharacters) {
387
+ const value = frontmatter?.[key];
388
+ if (typeof value !== 'string' || !value)
389
+ return null;
390
+ const bounded = boundWireText(value, maximumCharacters);
391
+ return bounded || null;
392
+ }
393
+ async function themeSummary(prepared, signal) {
394
+ throwIfAborted(signal);
395
+ const themeId = prepared.doc.themeId;
396
+ if (!themeId)
397
+ return null;
398
+ const documentTheme = prepared.doc.customThemes?.find((theme) => theme.id === themeId);
399
+ if (documentTheme) {
400
+ return {
401
+ id: toWireIdentifier(themeId, 'theme'),
402
+ name: boundWireText(documentTheme.name ?? themeId, MCP_WIRE_LIMITS.labelCharacters, themeId),
403
+ source: prepared.sourceFormat === 'pptx' ? 'inferred' : 'document',
404
+ layouts: [
405
+ ...new Set(prepared.doc.customTemplates
406
+ ?.slice(0, MCP_WIRE_LIMITS.arrayEntries)
407
+ .map((template) => boundWireText(template.name, MCP_WIRE_LIMITS.labelCharacters)) ?? []),
408
+ ],
409
+ };
410
+ }
411
+ const { getThemeSummaries } = await import('@bendyline/squisq/schemas');
412
+ throwIfAborted(signal);
413
+ const builtIn = getThemeSummaries().find((theme) => theme.id === themeId);
414
+ return {
415
+ id: toWireIdentifier(themeId, 'theme'),
416
+ name: boundWireText(builtIn?.name ?? themeId, MCP_WIRE_LIMITS.labelCharacters, themeId),
417
+ source: 'built-in',
418
+ layouts: [
419
+ ...new Set(prepared.doc.customTemplates
420
+ ?.slice(0, MCP_WIRE_LIMITS.arrayEntries)
421
+ .map((template) => boundWireText(template.name, MCP_WIRE_LIMITS.labelCharacters)) ?? []),
422
+ ],
423
+ };
424
+ }
425
+ function mapDocDiagnostic(diagnostic, format) {
426
+ return {
427
+ code: diagnostic.code,
428
+ severity: diagnostic.severity,
429
+ stage: 'validate',
430
+ format,
431
+ count: 1,
432
+ message: diagnostic.message,
433
+ remediation: null,
434
+ retryable: false,
435
+ location: diagnostic.blockId
436
+ ? {
437
+ kind: 'block',
438
+ blockId: toWireIdentifier(diagnostic.blockId, 'block'),
439
+ nodeType: 'block',
440
+ }
441
+ : diagnostic.line
442
+ ? { kind: 'source', line: diagnostic.line, column: 1 }
443
+ : null,
444
+ };
445
+ }
446
+ function missingAltTextDiagnostics(markdownDoc, format) {
447
+ let missingCount = 0;
448
+ const visit = (node) => {
449
+ if (!node || typeof node !== 'object')
450
+ return;
451
+ const candidate = node;
452
+ if (candidate.type === 'image' &&
453
+ (typeof candidate.alt !== 'string' || !candidate.alt.trim())) {
454
+ missingCount += 1;
455
+ }
456
+ if (Array.isArray(candidate.children))
457
+ candidate.children.forEach(visit);
458
+ };
459
+ visit(markdownDoc);
460
+ if (missingCount === 0)
461
+ return [];
462
+ return [
463
+ {
464
+ code: 'missing-alt-text',
465
+ severity: 'warning',
466
+ stage: 'validate',
467
+ format,
468
+ count: missingCount,
469
+ message: missingCount === 1
470
+ ? 'One image has no alternative text.'
471
+ : `${missingCount} images have no alternative text.`,
472
+ remediation: 'Add concise, meaningful alternative text to every Markdown image.',
473
+ retryable: false,
474
+ location: null,
475
+ },
476
+ ];
477
+ }
478
+ async function targetPreflight(prepared, targetFormat, signal) {
479
+ throwIfAborted(signal);
480
+ const normalized = targetFormat.toLowerCase();
481
+ const { createCliRegistry } = await import('@bendyline/squisq-cli/api');
482
+ throwIfAborted(signal);
483
+ const target = createCliRegistry().get(normalized);
484
+ if (!target?.exportDoc) {
485
+ return [
486
+ {
487
+ code: target ? 'unsupported-output' : 'unknown-format',
488
+ severity: 'error',
489
+ stage: 'validate',
490
+ format: normalized,
491
+ count: 1,
492
+ message: target
493
+ ? `${target.label} cannot be used as a conversion target.`
494
+ : `Unknown target format "${normalized}".`,
495
+ remediation: 'Choose an export-capable format from list_formats.',
496
+ retryable: false,
497
+ location: null,
498
+ },
499
+ ];
500
+ }
501
+ const diagnostics = [];
502
+ const details = summarizeMarkdownNodes(prepared.markdownDoc);
503
+ await yieldForCancellation(signal);
504
+ if (['pptx', 'pdf', 'mp4', 'gif'].includes(normalized)) {
505
+ const { analyzeBlocks } = await import('@bendyline/squisq/transform');
506
+ let denseBlockCount = 0;
507
+ let firstDenseBlock = null;
508
+ let analyzedIndex = 0;
509
+ for (const entry of analyzeBlocks(prepared.doc.blocks)) {
510
+ const checkpoint = cancellationCheckpoint(signal, analyzedIndex);
511
+ if (checkpoint)
512
+ await checkpoint;
513
+ analyzedIndex += 1;
514
+ if (entry.bodyWordCount <= 120 && entry.plainText.length <= 900)
515
+ continue;
516
+ denseBlockCount += 1;
517
+ firstDenseBlock ?? (firstDenseBlock = entry.block);
518
+ }
519
+ if (denseBlockCount > 0) {
520
+ diagnostics.push({
521
+ code: 'content-density-high',
522
+ severity: 'warning',
523
+ stage: 'validate',
524
+ format: normalized,
525
+ count: denseBlockCount,
526
+ message: denseBlockCount === 1
527
+ ? `Block "${firstDenseBlock?.title ?? firstDenseBlock?.id ?? 'unknown'}" may be too dense for a single rendered item.`
528
+ : `${denseBlockCount} blocks may be too dense for a single rendered item.`,
529
+ remediation: 'Split the block, shorten prose, or confirm the result with preview_document overflow diagnostics.',
530
+ retryable: false,
531
+ location: denseBlockCount === 1 && firstDenseBlock
532
+ ? {
533
+ kind: 'block',
534
+ blockId: toWireIdentifier(firstDenseBlock.id, 'block'),
535
+ nodeType: 'block',
536
+ }
537
+ : null,
538
+ });
539
+ }
540
+ const denseTables = details.tables.filter((table) => table.columnCount > 8 || table.rowCount > 30);
541
+ if (denseTables.length > 0) {
542
+ const firstDenseTable = denseTables[0];
543
+ diagnostics.push({
544
+ code: 'table-density-high',
545
+ severity: 'warning',
546
+ stage: 'validate',
547
+ format: normalized,
548
+ count: denseTables.length,
549
+ message: denseTables.length === 1 && firstDenseTable
550
+ ? `Table ${firstDenseTable.index + 1} has ${firstDenseTable.rowCount} rows and ${firstDenseTable.columnCount} columns and may overflow.`
551
+ : `${denseTables.length} tables may overflow the rendered output.`,
552
+ remediation: 'Split the table or use a spreadsheet target; verify visual clipping with preview_document.',
553
+ retryable: false,
554
+ location: null,
555
+ });
556
+ }
557
+ }
558
+ if (normalized === 'pptx' && prepared.doc.blocks.some((block) => block.template)) {
559
+ diagnostics.push(warningDiagnostic('Editable-native PPTX export preserves semantic content and theme but does not reproduce arbitrary Squisq template geometry.', 'validate', normalized));
560
+ }
561
+ if (normalized === 'pdf' && (await hasImages(prepared.markdownDoc, signal))) {
562
+ diagnostics.push(warningDiagnostic('Native PDF export currently represents Markdown images as labeled placeholders.', 'validate', normalized));
563
+ }
564
+ if ((normalized === 'csv' || normalized === 'xlsx') && details.tableCount === 0) {
565
+ diagnostics.push(warningDiagnostic(`${normalized.toUpperCase()} export is table-oriented; this document contains no Markdown tables.`, 'validate', normalized));
566
+ }
567
+ if (normalized === 'gif' && (prepared.doc.audio?.segments?.length ?? 0) > 0) {
568
+ diagnostics.push(warningDiagnostic('Animated GIF does not support audio; audio will be omitted.', 'validate', normalized));
569
+ }
570
+ return diagnostics;
571
+ }
572
+ async function hasImages(markdownDoc, signal) {
573
+ throwIfAborted(signal);
574
+ let found = false;
575
+ const visit = (node) => {
576
+ if (found || !node || typeof node !== 'object')
577
+ return;
578
+ const candidate = node;
579
+ if (candidate.type === 'image' || candidate.type === 'imageReference')
580
+ found = true;
581
+ if (Array.isArray(candidate.children))
582
+ candidate.children.forEach(visit);
583
+ };
584
+ visit(markdownDoc);
585
+ await yieldForCancellation(signal);
586
+ return found;
587
+ }
588
+ export function boundDiagnostics(diagnostics, stage) {
589
+ const aggregated = aggregateDiagnostics(diagnostics);
590
+ if (aggregated.length <= MAX_DIAGNOSTICS)
591
+ return aggregated;
592
+ // Reserve room for one aggregate per severity. This preserves validation
593
+ // occurrence counts and error status even when individual entries are omitted.
594
+ const retained = aggregated.slice(0, MAX_DIAGNOSTICS - 3);
595
+ const omitted = aggregated.slice(MAX_DIAGNOSTICS - 3);
596
+ const severities = ['error', 'warning', 'info'];
597
+ const truncationDiagnostics = severities.flatMap((severity) => {
598
+ const entries = omitted.filter((diagnostic) => diagnostic.severity === severity);
599
+ if (entries.length === 0)
600
+ return [];
601
+ const occurrenceCount = entries.reduce((sum, diagnostic) => safeDiagnosticCountSum(sum, diagnostic.count), 0);
602
+ return [
603
+ {
604
+ code: 'diagnostics-truncated',
605
+ severity,
606
+ stage,
607
+ format: null,
608
+ count: occurrenceCount,
609
+ message: `${entries.length} additional ${severity} diagnostic entries were aggregated to keep the MCP result bounded.`,
610
+ remediation: 'Inspect a narrower document range or repair the reported issue class before validating again.',
611
+ retryable: false,
612
+ location: null,
613
+ },
614
+ ];
615
+ });
616
+ return [...retained, ...truncationDiagnostics];
617
+ }
618
+ function aggregateDiagnostics(diagnostics) {
619
+ const aggregated = new Map();
620
+ for (const input of diagnostics) {
621
+ const diagnostic = boundDiagnosticText(input);
622
+ const key = [
623
+ diagnostic.code,
624
+ diagnostic.severity,
625
+ diagnostic.stage,
626
+ diagnostic.format ?? '',
627
+ diagnostic.message,
628
+ diagnostic.remediation ?? '',
629
+ String(diagnostic.retryable),
630
+ ].join('\0');
631
+ const current = aggregated.get(key);
632
+ if (!current) {
633
+ aggregated.set(key, diagnostic);
634
+ continue;
635
+ }
636
+ aggregated.set(key, {
637
+ ...current,
638
+ count: safeDiagnosticCountSum(current.count, diagnostic.count),
639
+ location: sameLocation(current.location, diagnostic.location) ? current.location : null,
640
+ });
641
+ }
642
+ return [...aggregated.values()];
643
+ }
644
+ function boundDiagnosticText(diagnostic) {
645
+ const normalizedFormat = diagnostic.format?.toLowerCase() ?? null;
646
+ return {
647
+ ...diagnostic,
648
+ code: toWireIdentifier(diagnostic.code, 'diagnostic'),
649
+ format: normalizedFormat !== null && FORMAT_PATTERN.test(normalizedFormat) ? normalizedFormat : null,
650
+ message: boundWireText(diagnostic.message, MCP_WIRE_LIMITS.messageCharacters, 'Document diagnostic'),
651
+ remediation: diagnostic.remediation === null
652
+ ? null
653
+ : boundWireText(diagnostic.remediation, MCP_WIRE_LIMITS.messageCharacters),
654
+ location: diagnostic.location?.kind === 'block'
655
+ ? {
656
+ ...diagnostic.location,
657
+ blockId: toWireIdentifier(diagnostic.location.blockId, 'block'),
658
+ nodeType: toWireIdentifier(diagnostic.location.nodeType, 'node'),
659
+ }
660
+ : diagnostic.location,
661
+ };
662
+ }
663
+ function sameLocation(left, right) {
664
+ return JSON.stringify(left) === JSON.stringify(right);
665
+ }
666
+ function safeDiagnosticCountSum(left, right) {
667
+ return Math.min(Number.MAX_SAFE_INTEGER, left + right);
668
+ }
669
+ function summarizeDiagnostics(diagnostics) {
670
+ let errorCount = 0;
671
+ let warningCount = 0;
672
+ let infoCount = 0;
673
+ for (const diagnostic of diagnostics) {
674
+ if (diagnostic.severity === 'error') {
675
+ errorCount = safeDiagnosticCountSum(errorCount, diagnostic.count);
676
+ }
677
+ else if (diagnostic.severity === 'warning') {
678
+ warningCount = safeDiagnosticCountSum(warningCount, diagnostic.count);
679
+ }
680
+ else {
681
+ infoCount = safeDiagnosticCountSum(infoCount, diagnostic.count);
682
+ }
683
+ }
684
+ return { errorCount, warningCount, infoCount };
685
+ }
686
+ function normalizeTargetFormat(targetFormat) {
687
+ if (targetFormat === null)
688
+ return null;
689
+ const normalized = targetFormat.toLowerCase();
690
+ if (!FORMAT_PATTERN.test(normalized)) {
691
+ throw new Error(`Invalid target format "${targetFormat.slice(0, 64)}"`);
692
+ }
693
+ return normalized;
694
+ }
695
+ function words(text) {
696
+ return text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
697
+ }
698
+ async function multisetSimilarity(left, right, signal) {
699
+ if (left.length === 0 && right.length === 0)
700
+ return 1;
701
+ const [leftCounts, rightCounts] = await Promise.all([
702
+ counts(left, signal),
703
+ counts(right, signal),
704
+ ]);
705
+ let intersection = 0;
706
+ let union = 0;
707
+ let index = 0;
708
+ for (const word of new Set([...leftCounts.keys(), ...rightCounts.keys()])) {
709
+ intersection += Math.min(leftCounts.get(word) ?? 0, rightCounts.get(word) ?? 0);
710
+ union += Math.max(leftCounts.get(word) ?? 0, rightCounts.get(word) ?? 0);
711
+ const checkpoint = cancellationCheckpoint(signal, index);
712
+ if (checkpoint)
713
+ await checkpoint;
714
+ index += 1;
715
+ }
716
+ return union === 0 ? 1 : intersection / union;
717
+ }
718
+ async function counts(values, signal) {
719
+ const result = new Map();
720
+ for (let index = 0; index < values.length; index += 1) {
721
+ const value = values[index];
722
+ if (value !== undefined)
723
+ result.set(value, (result.get(value) ?? 0) + 1);
724
+ const checkpoint = cancellationCheckpoint(signal, index);
725
+ if (checkpoint)
726
+ await checkpoint;
727
+ }
728
+ return result;
729
+ }
730
+ function ratioSimilarity(left, right) {
731
+ return left === 0 && right === 0 ? 1 : Math.min(left, right) / Math.max(left, right);
732
+ }
733
+ function assetSimilarity(left, right) {
734
+ if (left.length === 0 && right.length === 0)
735
+ return 1;
736
+ const leftKeys = new Set(left.map((asset) => `${asset.path}:${asset.sha256 ?? asset.size}`));
737
+ const rightKeys = new Set(right.map((asset) => `${asset.path}:${asset.sha256 ?? asset.size}`));
738
+ const intersection = [...leftKeys].filter((key) => rightKeys.has(key)).length;
739
+ const union = new Set([...leftKeys, ...rightKeys]).size;
740
+ return union === 0 ? 1 : intersection / union;
741
+ }
742
+ function objectSimilarity(left, right) {
743
+ const keys = ['title', 'author', 'description'];
744
+ return keys.filter((key) => left[key] === right[key]).length / keys.length;
745
+ }
746
+ function metadataFieldCount(inspection) {
747
+ return Object.values(inspection.metadata).filter((value) => value !== null).length;
748
+ }
749
+ async function layoutTokens(prepared, signal) {
750
+ const tokens = [];
751
+ const pending = [...prepared.doc.blocks].reverse();
752
+ let index = 0;
753
+ while (pending.length > 0) {
754
+ const block = pending.pop();
755
+ if (!block)
756
+ break;
757
+ tokens.push(`block:${block.template ?? 'default'}`);
758
+ const children = block.children ?? [];
759
+ for (let childIndex = children.length - 1; childIndex >= 0; childIndex -= 1) {
760
+ const child = children[childIndex];
761
+ if (child)
762
+ pending.push(child);
763
+ }
764
+ const checkpoint = cancellationCheckpoint(signal, index);
765
+ if (checkpoint)
766
+ await checkpoint;
767
+ index += 1;
768
+ }
769
+ const customTemplates = prepared.doc.customTemplates ?? [];
770
+ for (let templateIndex = 0; templateIndex < customTemplates.length; templateIndex += 1) {
771
+ const template = customTemplates[templateIndex];
772
+ if (!template)
773
+ continue;
774
+ tokens.push(`custom:${template.name}:${template.label}`);
775
+ const checkpoint = cancellationCheckpoint(signal, index + templateIndex);
776
+ if (checkpoint)
777
+ await checkpoint;
778
+ }
779
+ return tokens;
780
+ }
781
+ function timingSummary(prepared) {
782
+ return {
783
+ duration: prepared.doc.duration,
784
+ segmentCount: prepared.doc.audio.segments.length,
785
+ captionCount: prepared.doc.captions?.phrases.length ?? 0,
786
+ };
787
+ }
788
+ async function accessibilityTokens(prepared, signal) {
789
+ const tokens = [];
790
+ const pending = [prepared.markdownDoc];
791
+ let index = 0;
792
+ while (pending.length > 0) {
793
+ const node = pending.pop();
794
+ if (!node || typeof node !== 'object')
795
+ continue;
796
+ const candidate = node;
797
+ if (candidate.type === 'image' || candidate.type === 'imageReference') {
798
+ const alt = typeof candidate.alt === 'string' ? candidate.alt.trim() : '';
799
+ tokens.push(`image-alt:${alt}`);
800
+ }
801
+ if (Array.isArray(candidate.children)) {
802
+ for (let childIndex = candidate.children.length - 1; childIndex >= 0; childIndex -= 1) {
803
+ pending.push(candidate.children[childIndex]);
804
+ }
805
+ }
806
+ const checkpoint = cancellationCheckpoint(signal, index);
807
+ if (checkpoint)
808
+ await checkpoint;
809
+ index += 1;
810
+ }
811
+ const phrases = prepared.doc.captions?.phrases ?? [];
812
+ for (let phraseIndex = 0; phraseIndex < phrases.length; phraseIndex += 1) {
813
+ const phrase = phrases[phraseIndex];
814
+ if (!phrase)
815
+ continue;
816
+ tokens.push(`caption:${phrase.text}`);
817
+ const checkpoint = cancellationCheckpoint(signal, index + phraseIndex);
818
+ if (checkpoint)
819
+ await checkpoint;
820
+ }
821
+ return tokens;
822
+ }
823
+ function cancellationCheckpoint(signal, index) {
824
+ throwIfAborted(signal);
825
+ return index % CANCELLATION_YIELD_INTERVAL === 0 ? yieldForCancellation(signal) : null;
826
+ }
827
+ async function yieldForCancellation(signal) {
828
+ throwIfAborted(signal);
829
+ await new Promise((resolve) => setImmediate(resolve));
830
+ throwIfAborted(signal);
831
+ }
832
+ function metric(name, leftValue, rightValue, similarity) {
833
+ return { name, leftValue, rightValue, similarity: clamp01(similarity) };
834
+ }
835
+ function change(category, similarity, label, leftCount, rightCount) {
836
+ const status = similarity === 1
837
+ ? 'retained'
838
+ : leftCount === 0 && rightCount > 0
839
+ ? 'added'
840
+ : leftCount > 0 && rightCount === 0
841
+ ? 'omitted'
842
+ : similarity === 0
843
+ ? 'changed'
844
+ : 'degraded';
845
+ return {
846
+ category,
847
+ status,
848
+ path: null,
849
+ message: `${label} similarity is ${Math.round(similarity * 100)}%.`,
850
+ };
851
+ }
852
+ function clamp01(value) {
853
+ return Math.max(0, Math.min(1, Number(value.toFixed(6))));
854
+ }
855
+ //# sourceMappingURL=intelligence.js.map