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.
- package/.env.example +38 -0
- package/ARCHITECTURE.md +99 -3
- package/EXPLORATION.md +148 -89
- package/QUICK_START.md +5 -2
- package/README.md +51 -7
- package/bin/taskex.js +11 -4
- package/package.json +38 -5
- package/prompt.json +2 -2
- package/src/config.js +52 -3
- package/src/logger.js +7 -4
- package/src/modes/focused-reanalysis.js +2 -1
- package/src/modes/progress-updater.js +1 -1
- package/src/phases/_shared.js +43 -0
- package/src/phases/compile.js +101 -0
- package/src/phases/deep-dive.js +118 -0
- package/src/phases/discover.js +178 -0
- package/src/phases/init.js +199 -0
- package/src/phases/output.js +238 -0
- package/src/phases/process-media.js +633 -0
- package/src/phases/services.js +104 -0
- package/src/phases/summary.js +86 -0
- package/src/pipeline.js +432 -1464
- package/src/renderers/docx.js +531 -0
- package/src/renderers/html.js +672 -0
- package/src/renderers/markdown.js +15 -183
- package/src/renderers/pdf.js +90 -0
- package/src/renderers/shared.js +215 -0
- package/src/schemas/analysis-compiled.schema.json +381 -0
- package/src/schemas/analysis-segment.schema.json +380 -0
- package/src/services/doc-parser.js +346 -0
- package/src/services/gemini.js +118 -45
- package/src/services/video.js +123 -8
- package/src/utils/adaptive-budget.js +6 -4
- package/src/utils/checkpoint.js +2 -1
- package/src/utils/cli.js +132 -111
- package/src/utils/colors.js +83 -0
- package/src/utils/confidence-filter.js +139 -0
- package/src/utils/diff-engine.js +2 -1
- package/src/utils/global-config.js +6 -5
- package/src/utils/health-dashboard.js +11 -9
- package/src/utils/json-parser.js +4 -2
- package/src/utils/learning-loop.js +3 -2
- package/src/utils/progress-bar.js +286 -0
- package/src/utils/quality-gate.js +10 -8
- package/src/utils/retry.js +3 -1
- 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 };
|