task-summary-extractor 8.3.0 → 9.0.1

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 (46) hide show
  1. package/.env.example +38 -0
  2. package/ARCHITECTURE.md +99 -3
  3. package/EXPLORATION.md +148 -89
  4. package/QUICK_START.md +5 -2
  5. package/README.md +51 -7
  6. package/bin/taskex.js +11 -4
  7. package/package.json +38 -5
  8. package/prompt.json +2 -2
  9. package/src/config.js +52 -3
  10. package/src/logger.js +7 -4
  11. package/src/modes/focused-reanalysis.js +2 -1
  12. package/src/modes/progress-updater.js +1 -1
  13. package/src/phases/_shared.js +43 -0
  14. package/src/phases/compile.js +101 -0
  15. package/src/phases/deep-dive.js +118 -0
  16. package/src/phases/discover.js +178 -0
  17. package/src/phases/init.js +199 -0
  18. package/src/phases/output.js +238 -0
  19. package/src/phases/process-media.js +633 -0
  20. package/src/phases/services.js +104 -0
  21. package/src/phases/summary.js +86 -0
  22. package/src/pipeline.js +432 -1464
  23. package/src/renderers/docx.js +531 -0
  24. package/src/renderers/html.js +672 -0
  25. package/src/renderers/markdown.js +15 -183
  26. package/src/renderers/pdf.js +90 -0
  27. package/src/renderers/shared.js +215 -0
  28. package/src/schemas/analysis-compiled.schema.json +381 -0
  29. package/src/schemas/analysis-segment.schema.json +380 -0
  30. package/src/services/doc-parser.js +346 -0
  31. package/src/services/gemini.js +118 -45
  32. package/src/services/video.js +123 -8
  33. package/src/utils/adaptive-budget.js +6 -4
  34. package/src/utils/checkpoint.js +2 -1
  35. package/src/utils/cli.js +132 -111
  36. package/src/utils/colors.js +83 -0
  37. package/src/utils/confidence-filter.js +139 -0
  38. package/src/utils/diff-engine.js +2 -1
  39. package/src/utils/global-config.js +6 -5
  40. package/src/utils/health-dashboard.js +11 -9
  41. package/src/utils/json-parser.js +4 -2
  42. package/src/utils/learning-loop.js +3 -2
  43. package/src/utils/progress-bar.js +286 -0
  44. package/src/utils/quality-gate.js +10 -8
  45. package/src/utils/retry.js +3 -1
  46. package/src/utils/schema-validator.js +314 -0
@@ -0,0 +1,531 @@
1
+ /**
2
+ * DOCX renderer — generates a Word document from compiled analysis results.
3
+ *
4
+ * Uses the `docx` package to programmatically build a professional DOCX file
5
+ * that mirrors the structure of the Markdown/HTML reports.
6
+ *
7
+ * Usage:
8
+ * const { renderResultsDocx } = require('./docx');
9
+ * const buffer = renderResultsDocx({ compiled, meta });
10
+ * fs.writeFileSync('results.docx', buffer);
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const {
16
+ stripParens, clusterNames, resolve,
17
+ dedupBy,
18
+ } = require('./shared');
19
+
20
+ // ════════════════════════════════════════════════════════════
21
+ // Lazy-load docx package
22
+ // ════════════════════════════════════════════════════════════
23
+
24
+ let docx;
25
+ function loadDocx() {
26
+ if (!docx) {
27
+ try {
28
+ docx = require('docx');
29
+ } catch {
30
+ throw new Error(
31
+ 'DOCX generation requires the "docx" package. Install it with: npm install docx'
32
+ );
33
+ }
34
+ }
35
+ return docx;
36
+ }
37
+
38
+ // ════════════════════════════════════════════════════════════
39
+ // Color / style constants
40
+ // ════════════════════════════════════════════════════════════
41
+
42
+ const BRAND_BLUE = '4361EE';
43
+ const MUTED_GRAY = '6C757D';
44
+ const SUCCESS_GREEN = '2ECC71';
45
+ const WARNING_AMBER = 'F39C12';
46
+ const DANGER_RED = 'E74C3C';
47
+
48
+ // Priority → color map
49
+ const PRI_COLOR = { critical: DANGER_RED, high: DANGER_RED, medium: WARNING_AMBER, low: SUCCESS_GREEN };
50
+ // Confidence → color map
51
+ const CONF_COLOR = { HIGH: SUCCESS_GREEN, MEDIUM: WARNING_AMBER, LOW: DANGER_RED };
52
+
53
+ // ════════════════════════════════════════════════════════════
54
+ // Shorthand constructors
55
+ // ════════════════════════════════════════════════════════════
56
+
57
+ function heading(text, level = 1) {
58
+ const { Paragraph, HeadingLevel, TextRun } = loadDocx();
59
+ const map = {
60
+ 1: HeadingLevel.HEADING_1,
61
+ 2: HeadingLevel.HEADING_2,
62
+ 3: HeadingLevel.HEADING_3,
63
+ 4: HeadingLevel.HEADING_4,
64
+ };
65
+ return new Paragraph({
66
+ heading: map[level] || HeadingLevel.HEADING_1,
67
+ children: [new TextRun({ text: String(text) })],
68
+ spacing: { before: level <= 2 ? 300 : 200, after: 100 },
69
+ });
70
+ }
71
+
72
+ function para(text, opts = {}) {
73
+ const { Paragraph, TextRun } = loadDocx();
74
+ return new Paragraph({
75
+ children: [new TextRun({
76
+ text: String(text || ''),
77
+ bold: opts.bold || false,
78
+ italics: opts.italic || false,
79
+ color: opts.color || undefined,
80
+ size: opts.size || undefined,
81
+ })],
82
+ spacing: { after: opts.spacingAfter ?? 80 },
83
+ });
84
+ }
85
+
86
+ function bulletItem(text, level = 0) {
87
+ const { Paragraph, TextRun } = loadDocx();
88
+ return new Paragraph({
89
+ children: [new TextRun({ text: String(text || '') })],
90
+ bullet: { level },
91
+ spacing: { after: 40 },
92
+ });
93
+ }
94
+
95
+ function metaLine(label, value) {
96
+ const { Paragraph, TextRun } = loadDocx();
97
+ return new Paragraph({
98
+ children: [
99
+ new TextRun({ text: `${label}: `, bold: true, color: MUTED_GRAY }),
100
+ new TextRun({ text: String(value || 'N/A') }),
101
+ ],
102
+ spacing: { after: 40 },
103
+ });
104
+ }
105
+
106
+ function hrule() {
107
+ const { Paragraph, BorderStyle } = loadDocx();
108
+ return new Paragraph({
109
+ children: [],
110
+ border: { bottom: { style: BorderStyle.SINGLE, size: 1, color: 'CCCCCC' } },
111
+ spacing: { before: 200, after: 200 },
112
+ });
113
+ }
114
+
115
+ function badge(text, color) {
116
+ const { TextRun } = loadDocx();
117
+ return new TextRun({ text: ` [${text}]`, bold: true, color: color || MUTED_GRAY });
118
+ }
119
+
120
+ // ════════════════════════════════════════════════════════════
121
+ // Table builder
122
+ // ════════════════════════════════════════════════════════════
123
+
124
+ function buildTable(headers, rows) {
125
+ const { Table, TableRow, TableCell, Paragraph, TextRun, WidthType, BorderStyle, ShadingType } = loadDocx();
126
+
127
+ const cellBorder = {
128
+ top: { style: BorderStyle.SINGLE, size: 1, color: 'DEE2E6' },
129
+ bottom: { style: BorderStyle.SINGLE, size: 1, color: 'DEE2E6' },
130
+ left: { style: BorderStyle.SINGLE, size: 1, color: 'DEE2E6' },
131
+ right: { style: BorderStyle.SINGLE, size: 1, color: 'DEE2E6' },
132
+ };
133
+
134
+ const headerRow = new TableRow({
135
+ tableHeader: true,
136
+ children: headers.map(h =>
137
+ new TableCell({
138
+ children: [new Paragraph({ children: [new TextRun({ text: h, bold: true, color: 'FFFFFF', size: 20 })] })],
139
+ shading: { type: ShadingType.SOLID, color: BRAND_BLUE },
140
+ borders: cellBorder,
141
+ })
142
+ ),
143
+ });
144
+
145
+ const dataRows = rows.map((cells, rowIdx) =>
146
+ new TableRow({
147
+ children: cells.map(cellText =>
148
+ new TableCell({
149
+ children: [new Paragraph({ children: [new TextRun({ text: String(cellText || ''), size: 20 })] })],
150
+ borders: cellBorder,
151
+ shading: rowIdx % 2 === 1 ? { type: ShadingType.SOLID, color: 'F2F4F8' } : undefined,
152
+ })
153
+ ),
154
+ })
155
+ );
156
+
157
+ return new Table({
158
+ rows: [headerRow, ...dataRows],
159
+ width: { size: 100, type: WidthType.PERCENTAGE },
160
+ });
161
+ }
162
+
163
+ // ════════════════════════════════════════════════════════════
164
+ // Section renderers
165
+ // ════════════════════════════════════════════════════════════
166
+
167
+ function renderYourTasks(yourTasks, clusterMap, allTickets) {
168
+ if (!yourTasks) return [];
169
+ const elements = [];
170
+ elements.push(heading('⭐ Your Tasks', 2));
171
+ if (yourTasks.user_name) {
172
+ elements.push(para(`Assigned to: ${resolve(yourTasks.user_name, clusterMap)}`, { bold: true, color: BRAND_BLUE }));
173
+ }
174
+ // owned_tickets are plain ticket-ID strings (e.g. ["CR31296872"])
175
+ if (yourTasks.owned_tickets?.length) {
176
+ elements.push(heading('Owned Tickets', 3));
177
+ const ticketMap = new Map((allTickets || []).map(t => [t.ticket_id, t]));
178
+ for (const ticketId of yourTasks.owned_tickets) {
179
+ const t = ticketMap.get(ticketId);
180
+ const title = t?.title || t?.summary || '';
181
+ const status = t?.status ? ` [${t.status.replace(/_/g, ' ')}]` : '';
182
+ elements.push(bulletItem(`${ticketId}${title ? ` — ${title}` : ''}${status}`));
183
+ }
184
+ }
185
+ // tasks_todo — the main todo list
186
+ if (yourTasks.tasks_todo?.length) {
187
+ elements.push(heading('Tasks To-Do', 3));
188
+ for (const task of yourTasks.tasks_todo) {
189
+ const pri = task.priority ? ` [${task.priority}]` : '';
190
+ const src = task.source ? ` (from ${task.source})` : '';
191
+ elements.push(bulletItem(`${task.description || ''}${pri}${src}`));
192
+ }
193
+ }
194
+ if (yourTasks.action_items?.length) {
195
+ elements.push(heading('Action Items', 3));
196
+ for (const ai of yourTasks.action_items) {
197
+ elements.push(bulletItem(`${ai.description || ai.action || ''}${ai.deadline ? ` (by ${ai.deadline})` : ''}`));
198
+ }
199
+ }
200
+ // decisions_needed — decisions awaiting the user
201
+ if (yourTasks.decisions_needed?.length) {
202
+ elements.push(heading('Decisions Needed', 3));
203
+ for (const d of yourTasks.decisions_needed) {
204
+ const opts = (d.options || []).length ? ` — Options: ${d.options.join(', ')}` : '';
205
+ elements.push(bulletItem(`${d.description || d.question || ''}${opts}`));
206
+ }
207
+ }
208
+ // completed_in_call — items finished during this call
209
+ if (yourTasks.completed_in_call?.length) {
210
+ elements.push(heading('Completed In Call', 3));
211
+ for (const c of yourTasks.completed_in_call) {
212
+ elements.push(bulletItem(c.description || c.action || String(c)));
213
+ }
214
+ }
215
+ if (yourTasks.tasks_waiting_on_others?.length) {
216
+ elements.push(heading('Waiting On Others', 3));
217
+ for (const w of yourTasks.tasks_waiting_on_others) {
218
+ const who = w.waiting_on ? resolve(w.waiting_on, clusterMap) : 'Unknown';
219
+ elements.push(bulletItem(`${w.description || ''} — waiting on ${who}`));
220
+ }
221
+ }
222
+ // summary
223
+ if (yourTasks.summary) {
224
+ elements.push(para(yourTasks.summary, { italic: true, color: MUTED_GRAY }));
225
+ }
226
+ return elements;
227
+ }
228
+
229
+ function renderTickets(tickets, clusterMap) {
230
+ if (!tickets.length) return [];
231
+ const elements = [heading('🎫 Tickets', 2)];
232
+ const rows = tickets.map(t => [
233
+ t.ticket_id || '—',
234
+ t.title || t.summary || '',
235
+ t.status ? t.status.replace(/_/g, ' ') : '—',
236
+ t.priority || '—',
237
+ t.assignee ? resolve(t.assignee, clusterMap) : '—',
238
+ t.confidence || '—',
239
+ ]);
240
+ elements.push(buildTable(['ID', 'Title', 'Status', 'Priority', 'Assignee', 'Conf.'], rows));
241
+ return elements;
242
+ }
243
+
244
+ function renderActions(actions, clusterMap) {
245
+ if (!actions.length) return [];
246
+ const elements = [heading('📋 Action Items', 2)];
247
+ const rows = actions.map(ai => [
248
+ ai.description || ai.action || '',
249
+ ai.assigned_to ? resolve(ai.assigned_to, clusterMap) : '—',
250
+ ai.deadline || '—',
251
+ ai.priority || '—',
252
+ ai.confidence || '—',
253
+ ]);
254
+ elements.push(buildTable(['Action', 'Assigned To', 'Deadline', 'Priority', 'Conf.'], rows));
255
+ return elements;
256
+ }
257
+
258
+ function renderChangeRequests(crs, clusterMap) {
259
+ if (!crs.length) return [];
260
+ const elements = [heading('🔄 Change Requests', 2)];
261
+ for (const cr of crs) {
262
+ const { Paragraph, TextRun } = loadDocx();
263
+ const typeLabel = cr.type ? ` (${cr.type.replace(/_/g, ' ')})` : '';
264
+ const statusLabel = cr.status ? ` [${cr.status.replace(/_/g, ' ')}]` : '';
265
+ elements.push(new Paragraph({
266
+ children: [
267
+ new TextRun({ text: `${cr.id}: ${cr.title || cr.what || 'Untitled'}${typeLabel}${statusLabel}`, bold: true }),
268
+ badge(cr.priority || '—', PRI_COLOR[cr.priority] || MUTED_GRAY),
269
+ badge(cr.confidence || '—', CONF_COLOR[cr.confidence] || MUTED_GRAY),
270
+ ],
271
+ spacing: { before: 120, after: 40 },
272
+ }));
273
+ if (cr.what && cr.what !== cr.title) elements.push(bulletItem(`What: ${cr.what}`));
274
+ if (cr.how) elements.push(bulletItem(`How: ${cr.how}`));
275
+ if (cr.why) elements.push(bulletItem(`Why: ${cr.why}`));
276
+ if (cr.where?.file_path) elements.push(bulletItem(`File: ${cr.where.file_path}`));
277
+ if (cr.assigned_to) elements.push(bulletItem(`Owner: ${resolve(cr.assigned_to, clusterMap)}`));
278
+ if ((cr.related_tickets || []).length > 0) elements.push(bulletItem(`Tickets: ${cr.related_tickets.join(', ')}`));
279
+ }
280
+ return elements;
281
+ }
282
+
283
+ function renderBlockers(blockers, clusterMap) {
284
+ if (!blockers.length) return [];
285
+ const elements = [heading('🚧 Blockers', 2)];
286
+ const rows = blockers.map(b => [
287
+ b.description || '',
288
+ b.owner ? resolve(b.owner, clusterMap) : '—',
289
+ b.severity || b.priority || '—',
290
+ b.resolution || '—',
291
+ b.confidence || '—',
292
+ ]);
293
+ elements.push(buildTable(['Blocker', 'Owner', 'Severity', 'Resolution', 'Conf.'], rows));
294
+ return elements;
295
+ }
296
+
297
+ function renderScopeChanges(scopes, clusterMap) {
298
+ if (!scopes.length) return [];
299
+ const elements = [heading('📐 Scope Changes', 2)];
300
+ for (const sc of scopes) {
301
+ const icon = { added: '➕', removed: '➖', deferred: '⏸️', approach_changed: '🔄', ownership_changed: '👤', requirements_changed: '📋' }[sc.type] || '🔀';
302
+ const decidedBy = sc.decided_by ? resolve(sc.decided_by, clusterMap) : null;
303
+ const typeLabel = (sc.type || '').replace(/_/g, ' ');
304
+ const mainText = sc.new_scope || sc.reason || sc.id || '';
305
+ elements.push(bulletItem(`${icon} ${sc.id} (${typeLabel}): ${mainText} [${sc.impact || '?'}]`));
306
+ if (sc.original_scope && sc.original_scope !== 'not documented') {
307
+ elements.push(bulletItem(`Was: ${sc.original_scope}`, 1));
308
+ }
309
+ if (sc.reason && sc.new_scope) elements.push(bulletItem(`Reason: ${sc.reason}`, 1));
310
+ if (decidedBy) elements.push(bulletItem(`Decided by: ${decidedBy}`, 1));
311
+ if ((sc.related_tickets || []).length > 0) elements.push(bulletItem(`Tickets: ${sc.related_tickets.join(', ')}`, 1));
312
+ }
313
+ return elements;
314
+ }
315
+
316
+ function renderFileReferences(files) {
317
+ if (!files.length) return [];
318
+ const elements = [heading('📁 File References', 2)];
319
+ // Split into actionable and reference-only (matches HTML/MD renderers)
320
+ const actionable = files.filter(f => f.role && !['reference_only', 'source_of_truth'].includes(f.role));
321
+ const reference = files.filter(f => !f.role || ['reference_only', 'source_of_truth'].includes(f.role));
322
+
323
+ if (actionable.length > 0) {
324
+ elements.push(heading('Files Requiring Action', 3));
325
+ const rows = actionable.map(f => [
326
+ f.file_name || '—',
327
+ (f.role || '').replace(/_/g, ' '),
328
+ (f.file_type || '').replace(/_/g, ' '),
329
+ (f.mentioned_in_tickets || []).join(', ') || '—',
330
+ (f.mentioned_in_changes || []).join(', ') || '—',
331
+ f.resolved_path || '—',
332
+ ]);
333
+ elements.push(buildTable(['File', 'Role', 'Type', 'Tickets', 'Changes', 'Path'], rows));
334
+ }
335
+
336
+ if (reference.length > 0) {
337
+ elements.push(heading('Reference Files', 3));
338
+ const rows = reference.map(f => [
339
+ f.file_name || '—',
340
+ (f.file_type || '').replace(/_/g, ' '),
341
+ (f.mentioned_in_tickets || []).join(', ') || '—',
342
+ f.notes ? f.notes.slice(0, 80) + (f.notes.length > 80 ? '...' : '') : '—',
343
+ ]);
344
+ elements.push(buildTable(['File', 'Type', 'Tickets', 'Notes'], rows));
345
+ }
346
+ return elements;
347
+ }
348
+
349
+ // ════════════════════════════════════════════════════════════
350
+ // Main export
351
+ // ════════════════════════════════════════════════════════════
352
+
353
+ /**
354
+ * Generate a DOCX buffer from compiled analysis results.
355
+ *
356
+ * @param {object} options
357
+ * @param {object} options.compiled - The AI-compiled unified analysis
358
+ * @param {object} options.meta - Call metadata
359
+ * @returns {Promise<Buffer>} DOCX file buffer
360
+ */
361
+ async function renderResultsDocx({ compiled, meta }) {
362
+ const { Document, Packer, Paragraph, TextRun, Header, Footer, AlignmentType } = loadDocx();
363
+
364
+ if (!compiled) {
365
+ const fallback = new Document({
366
+ sections: [{
367
+ children: [
368
+ heading('Call Analysis', 1),
369
+ para('No compiled result available — AI compilation may have failed.'),
370
+ ],
371
+ }],
372
+ });
373
+ return Packer.toBuffer(fallback);
374
+ }
375
+
376
+ // Deduplicate data
377
+ const allTickets = dedupBy(compiled.tickets || [], t => t.ticket_id);
378
+ const allCRs = dedupBy(compiled.change_requests || [], cr => cr.id);
379
+ const allActions = dedupBy(compiled.action_items || [], ai => ai.id);
380
+ const allBlockers = dedupBy(compiled.blockers || [], b => b.id);
381
+ const allScope = dedupBy(compiled.scope_changes || [], sc => sc.id);
382
+ const allFiles = dedupBy(compiled.file_references || [], f => f.resolved_path || f.file_name);
383
+ const summary = compiled.summary || compiled.executive_summary || '';
384
+ const yourTasks = compiled.your_tasks || null;
385
+
386
+ // Build cluster map
387
+ const rawNames = new Set();
388
+ const addName = n => { if (n?.trim()) rawNames.add(n.trim()); };
389
+ allActions.forEach(ai => addName(ai.assigned_to));
390
+ allCRs.forEach(cr => addName(cr.assigned_to));
391
+ allBlockers.forEach(b => addName(b.owner));
392
+ allScope.forEach(sc => addName(sc.decided_by));
393
+ allTickets.forEach(t => { addName(t.assignee); addName(t.reviewer); });
394
+ if (yourTasks?.user_name) addName(yourTasks.user_name);
395
+ const clusterMap = clusterNames([...rawNames]);
396
+
397
+ const teamKeywords = ['team', 'qa', 'dba', 'devops', 'db team', 'external'];
398
+ const people = [...clusterMap.keys()]
399
+ .filter(n => n && !teamKeywords.some(kw => n.toLowerCase() === kw))
400
+ .sort();
401
+
402
+ // ── Build document sections ──
403
+ const children = [];
404
+
405
+ // Title
406
+ children.push(heading(`📋 Call Analysis — ${meta.callName || 'Unknown'}`, 1));
407
+
408
+ // Metadata block
409
+ children.push(metaLine('Date', meta.processedAt ? meta.processedAt.slice(0, 10) : 'N/A'));
410
+ children.push(metaLine('Participants', people.join(', ') || 'Unknown'));
411
+ children.push(metaLine('Segments', String(meta.segmentCount || 'N/A')));
412
+ children.push(metaLine('Model', meta.geminiModel || 'N/A'));
413
+
414
+ const comp = meta.compilation;
415
+ if (comp) {
416
+ const tu = comp.tokenUsage || {};
417
+ const durSec = comp.durationMs ? (comp.durationMs / 1000).toFixed(1) : '?';
418
+ children.push(metaLine('Compilation',
419
+ `${durSec}s | ${(tu.inputTokens || 0).toLocaleString()} input → ${(tu.outputTokens || 0).toLocaleString()} output tokens`
420
+ ));
421
+ }
422
+
423
+ const cost = meta.costSummary;
424
+ if (cost && cost.totalTokens > 0) {
425
+ children.push(metaLine('Cost',
426
+ `$${cost.totalCost.toFixed(4)} (${cost.totalTokens.toLocaleString()} tokens | ${(cost.totalDurationMs / 1000).toFixed(1)}s)`
427
+ ));
428
+ }
429
+
430
+ // Confidence filter notice
431
+ if (compiled._filterMeta && compiled._filterMeta.minConfidence !== 'LOW') {
432
+ const fm = compiled._filterMeta;
433
+ const label = fm.minConfidence === 'HIGH' ? 'HIGH' : 'MEDIUM+';
434
+ children.push(para(
435
+ `⚠ Confidence filter: showing only ${label} items. Kept ${fm.filteredCounts.total}/${fm.originalCounts.total} (${fm.removed} removed).`,
436
+ { italic: true, color: WARNING_AMBER }
437
+ ));
438
+ }
439
+
440
+ children.push(hrule());
441
+
442
+ // Summary
443
+ if (summary) {
444
+ children.push(heading('Executive Summary', 2));
445
+ // Split summary into paragraphs
446
+ const parts = String(summary).split(/\n\n+/);
447
+ for (const p of parts) {
448
+ if (p.trim()) children.push(para(p.trim()));
449
+ }
450
+ children.push(hrule());
451
+ }
452
+
453
+ // Confidence distribution
454
+ const allConf = [...allTickets, ...allCRs, ...allActions, ...allBlockers, ...allScope];
455
+ if (allConf.length > 0) {
456
+ const hi = allConf.filter(i => i.confidence === 'HIGH').length;
457
+ const md = allConf.filter(i => i.confidence === 'MEDIUM').length;
458
+ const lo = allConf.filter(i => i.confidence === 'LOW').length;
459
+ children.push(para(
460
+ `Confidence: 🟢 HIGH ${hi} | 🟡 MEDIUM ${md} | 🔴 LOW ${lo} — Total ${allConf.length} items`,
461
+ { italic: true, color: MUTED_GRAY }
462
+ ));
463
+ }
464
+
465
+ // Your Tasks
466
+ children.push(...renderYourTasks(yourTasks, clusterMap, allTickets));
467
+
468
+ // Tickets
469
+ children.push(...renderTickets(allTickets, clusterMap));
470
+
471
+ // Action Items
472
+ children.push(...renderActions(allActions, clusterMap));
473
+
474
+ // Change Requests
475
+ children.push(...renderChangeRequests(allCRs, clusterMap));
476
+
477
+ // Blockers
478
+ children.push(...renderBlockers(allBlockers, clusterMap));
479
+
480
+ // Scope Changes
481
+ children.push(...renderScopeChanges(allScope, clusterMap));
482
+
483
+ // File References
484
+ children.push(...renderFileReferences(allFiles));
485
+
486
+ // ── Assemble document ──
487
+ const doc = new Document({
488
+ title: `Call Analysis — ${meta.callName || 'Unknown'}`,
489
+ creator: 'taskex',
490
+ description: 'Auto-generated call analysis report',
491
+ styles: {
492
+ default: {
493
+ document: {
494
+ run: { font: 'Calibri', size: 22 }, // 11pt
495
+ },
496
+ heading1: {
497
+ run: { size: 32, bold: true, color: BRAND_BLUE, font: 'Calibri' },
498
+ },
499
+ heading2: {
500
+ run: { size: 28, bold: true, color: BRAND_BLUE, font: 'Calibri' },
501
+ },
502
+ heading3: {
503
+ run: { size: 24, bold: true, font: 'Calibri' },
504
+ },
505
+ },
506
+ },
507
+ sections: [{
508
+ headers: {
509
+ default: new Header({
510
+ children: [new Paragraph({
511
+ alignment: AlignmentType.RIGHT,
512
+ children: [new TextRun({ text: 'taskex — Call Analysis Report', color: MUTED_GRAY, size: 16, italics: true })],
513
+ })],
514
+ }),
515
+ },
516
+ footers: {
517
+ default: new Footer({
518
+ children: [new Paragraph({
519
+ alignment: AlignmentType.CENTER,
520
+ children: [new TextRun({ text: `Generated by taskex • ${new Date().toISOString().slice(0, 10)}`, color: MUTED_GRAY, size: 16 })],
521
+ })],
522
+ }),
523
+ },
524
+ children,
525
+ }],
526
+ });
527
+
528
+ return Packer.toBuffer(doc);
529
+ }
530
+
531
+ module.exports = { renderResultsDocx };