task-summary-extractor 8.1.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/ARCHITECTURE.md +605 -0
- package/EXPLORATION.md +451 -0
- package/QUICK_START.md +272 -0
- package/README.md +544 -0
- package/bin/taskex.js +64 -0
- package/package.json +63 -0
- package/process_and_upload.js +107 -0
- package/prompt.json +265 -0
- package/setup.js +505 -0
- package/src/config.js +327 -0
- package/src/logger.js +355 -0
- package/src/pipeline.js +2006 -0
- package/src/renderers/markdown.js +968 -0
- package/src/services/firebase.js +106 -0
- package/src/services/gemini.js +779 -0
- package/src/services/git.js +329 -0
- package/src/services/video.js +305 -0
- package/src/utils/adaptive-budget.js +266 -0
- package/src/utils/change-detector.js +466 -0
- package/src/utils/cli.js +415 -0
- package/src/utils/context-manager.js +499 -0
- package/src/utils/cost-tracker.js +156 -0
- package/src/utils/deep-dive.js +549 -0
- package/src/utils/diff-engine.js +315 -0
- package/src/utils/dynamic-mode.js +567 -0
- package/src/utils/focused-reanalysis.js +317 -0
- package/src/utils/format.js +32 -0
- package/src/utils/fs.js +39 -0
- package/src/utils/global-config.js +315 -0
- package/src/utils/health-dashboard.js +216 -0
- package/src/utils/inject-cli-flags.js +58 -0
- package/src/utils/json-parser.js +245 -0
- package/src/utils/learning-loop.js +301 -0
- package/src/utils/progress-updater.js +451 -0
- package/src/utils/progress.js +166 -0
- package/src/utils/prompt.js +32 -0
- package/src/utils/quality-gate.js +429 -0
- package/src/utils/retry.js +129 -0
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown renderer — generates action-focused Markdown from compiled results.
|
|
3
|
+
*
|
|
4
|
+
* Improvements over v1:
|
|
5
|
+
* - Name clustering: merges case variants, role-suffix variants, partial matches
|
|
6
|
+
* - ID-based dedup: every ticket, CR, blocker, scope-change, action-item appears ONCE
|
|
7
|
+
* - User-first layout: the current user's section is promoted to the top
|
|
8
|
+
* - Cleaner formatting: owned tickets inline, concise tables, no repeated content
|
|
9
|
+
*
|
|
10
|
+
* This renderer expects the FINAL COMPILED analysis (after AI compilation pass),
|
|
11
|
+
* not raw per-segment data. It produces a single coherent task document.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
// ════════════════════════════════════════════════════════════
|
|
17
|
+
// Name Clustering Utilities
|
|
18
|
+
// ════════════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Strip parenthetical suffixes and normalize whitespace.
|
|
22
|
+
* "Mohamed Elhadi (Service Desk)" → "Mohamed Elhadi"
|
|
23
|
+
*/
|
|
24
|
+
function stripParens(name) {
|
|
25
|
+
return (name || '').replace(/\s*\([^)]*\)\s*/g, '').trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Normalize a name to lowercase stripped form for comparison.
|
|
30
|
+
*/
|
|
31
|
+
function normalizeKey(name) {
|
|
32
|
+
return stripParens(name).toLowerCase().replace(/\s+/g, ' ').trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build a Map<canonicalName, Set<rawVariants>> from a list of raw name strings.
|
|
37
|
+
* Clustering rules applied in order:
|
|
38
|
+
* 1. Exact normalized match (case-insensitive, parens stripped)
|
|
39
|
+
* 2. Substring containment after stripping
|
|
40
|
+
*
|
|
41
|
+
* The canonical name chosen is the longest proper-cased variant (no parens).
|
|
42
|
+
*/
|
|
43
|
+
function clusterNames(rawNames) {
|
|
44
|
+
const clusters = new Map(); // normKey → { canonical, variants: Set }
|
|
45
|
+
const normToCluster = new Map(); // normKey → cluster ref
|
|
46
|
+
|
|
47
|
+
for (const raw of rawNames) {
|
|
48
|
+
const stripped = stripParens(raw).trim();
|
|
49
|
+
if (!stripped) continue;
|
|
50
|
+
const nk = normalizeKey(raw);
|
|
51
|
+
|
|
52
|
+
// Check exact match first
|
|
53
|
+
if (normToCluster.has(nk)) {
|
|
54
|
+
const c = normToCluster.get(nk);
|
|
55
|
+
c.variants.add(raw);
|
|
56
|
+
// Upgrade canonical: prefer longest proper-cased form without parens
|
|
57
|
+
if (stripped.length >= c.canonical.length && stripped[0] === stripped[0].toUpperCase()) {
|
|
58
|
+
c.canonical = stripped;
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check substring containment against existing clusters
|
|
64
|
+
let merged = false;
|
|
65
|
+
for (const [existNk, c] of normToCluster) {
|
|
66
|
+
if (existNk.includes(nk) || nk.includes(existNk)) {
|
|
67
|
+
c.variants.add(raw);
|
|
68
|
+
normToCluster.set(nk, c);
|
|
69
|
+
if (stripped.length >= c.canonical.length && stripped[0] === stripped[0].toUpperCase()) {
|
|
70
|
+
c.canonical = stripped;
|
|
71
|
+
}
|
|
72
|
+
merged = true;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (merged) continue;
|
|
77
|
+
|
|
78
|
+
// New cluster
|
|
79
|
+
const cluster = { canonical: stripped[0] === stripped[0].toUpperCase() ? stripped : raw, variants: new Set([raw]) };
|
|
80
|
+
clusters.set(nk, cluster);
|
|
81
|
+
normToCluster.set(nk, cluster);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Build final map: canonical → Set of raw variants
|
|
85
|
+
const result = new Map();
|
|
86
|
+
for (const c of clusters.values()) {
|
|
87
|
+
if (!result.has(c.canonical)) result.set(c.canonical, new Set());
|
|
88
|
+
for (const v of c.variants) {
|
|
89
|
+
result.get(c.canonical).add(v);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Given a raw name and a cluster map, return the canonical form.
|
|
97
|
+
*/
|
|
98
|
+
function resolve(name, clusterMap) {
|
|
99
|
+
if (!name) return name;
|
|
100
|
+
const nk = normalizeKey(name);
|
|
101
|
+
for (const [canonical, variants] of clusterMap) {
|
|
102
|
+
for (const v of variants) {
|
|
103
|
+
if (normalizeKey(v) === nk) return canonical;
|
|
104
|
+
}
|
|
105
|
+
// substring fallback
|
|
106
|
+
const cnk = normalizeKey(canonical);
|
|
107
|
+
if (cnk.includes(nk) || nk.includes(cnk)) return canonical;
|
|
108
|
+
}
|
|
109
|
+
return stripParens(name).trim() || name;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ════════════════════════════════════════════════════════════
|
|
113
|
+
// Dedup Utilities
|
|
114
|
+
// ════════════════════════════════════════════════════════════
|
|
115
|
+
|
|
116
|
+
/** Deduplicate an array by a key function. First occurrence wins (keeps richest data by default). */
|
|
117
|
+
function dedupBy(arr, keyFn) {
|
|
118
|
+
const seen = new Map();
|
|
119
|
+
const result = [];
|
|
120
|
+
for (const item of arr) {
|
|
121
|
+
const k = keyFn(item);
|
|
122
|
+
if (!k) { result.push(item); continue; }
|
|
123
|
+
if (seen.has(k)) {
|
|
124
|
+
// Merge: overwrite sparse fields from later duplicates
|
|
125
|
+
const existing = seen.get(k);
|
|
126
|
+
for (const [field, val] of Object.entries(item)) {
|
|
127
|
+
if (val && !existing[field]) existing[field] = val;
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
seen.set(k, item);
|
|
132
|
+
result.push(item);
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Normalize a description for fuzzy matching — strips file paths, parenthetical details, punctuation. */
|
|
138
|
+
function normalizeDesc(s) {
|
|
139
|
+
return (s || '')
|
|
140
|
+
.toLowerCase()
|
|
141
|
+
// Strip full file paths (keep only the last segment, e.g. "Notifications.cs")
|
|
142
|
+
.replace(/[\w\-./\\]+\/[\w\-./\\]+\.(cs|ts|js|json|html|resx|png|md)/g, m => {
|
|
143
|
+
const parts = m.replace(/\\/g, '/').split('/');
|
|
144
|
+
return parts[parts.length - 1];
|
|
145
|
+
})
|
|
146
|
+
// Strip parenthetical additions like "(likely code/config ...)"
|
|
147
|
+
.replace(/\([^)]*\)/g, '')
|
|
148
|
+
// Strip trailing punctuation and whitespace before punctuation
|
|
149
|
+
.replace(/\s+([.,;:!?])/g, '$1')
|
|
150
|
+
// Collapse whitespace
|
|
151
|
+
.replace(/\s+/g, ' ')
|
|
152
|
+
.trim();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Deduplicate by description text similarity (fallback when IDs are missing). */
|
|
156
|
+
function dedupByDesc(arr, descField = 'description') {
|
|
157
|
+
const seen = new Set();
|
|
158
|
+
return arr.filter(item => {
|
|
159
|
+
const key = normalizeDesc(item[descField]);
|
|
160
|
+
if (!key || seen.has(key)) return false;
|
|
161
|
+
seen.add(key);
|
|
162
|
+
return true;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ════════════════════════════════════════════════════════════
|
|
167
|
+
// Main Renderer
|
|
168
|
+
// ════════════════════════════════════════════════════════════
|
|
169
|
+
|
|
170
|
+
/** Format a timestamp string for display, optionally with segment number. */
|
|
171
|
+
function fmtTs(ts, seg) {
|
|
172
|
+
if (!ts) return '';
|
|
173
|
+
if (seg) return `\`${ts}\` _(Seg ${seg})_`;
|
|
174
|
+
return `\`${ts}\``;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Make a compact priority badge */
|
|
178
|
+
function priBadge(p) {
|
|
179
|
+
if (!p) return '';
|
|
180
|
+
const icons = { high: '🔴', medium: '🟡', low: '🟢', critical: '🔴' };
|
|
181
|
+
return ` ${icons[p] || '⚪'} \`${p}\``;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Make a compact confidence badge */
|
|
185
|
+
function confBadge(c) {
|
|
186
|
+
if (!c) return '';
|
|
187
|
+
const icons = { HIGH: '🟢', MEDIUM: '🟡', LOW: '🔴' };
|
|
188
|
+
return ` ${icons[c] || '⚪'}\`${c}\``;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Make a confidence badge with reason tooltip */
|
|
192
|
+
function confBadgeFull(c, reason) {
|
|
193
|
+
if (!c) return '';
|
|
194
|
+
const icons = { HIGH: '🟢', MEDIUM: '🟡', LOW: '🔴' };
|
|
195
|
+
const badge = `${icons[c] || '⚪'}\`${c}\``;
|
|
196
|
+
if (reason) return ` ${badge} _(${reason})_`;
|
|
197
|
+
return ` ${badge}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Render the final compiled analysis into a comprehensive Markdown report.
|
|
202
|
+
*
|
|
203
|
+
* @param {object} options
|
|
204
|
+
* @param {object} options.compiled - The AI-compiled unified analysis
|
|
205
|
+
* @param {object} options.meta - Call metadata (enriched with compilation stats, segments, settings)
|
|
206
|
+
* @returns {string} Markdown content
|
|
207
|
+
*/
|
|
208
|
+
function renderResultsMarkdown({ compiled, meta }) {
|
|
209
|
+
const lines = [];
|
|
210
|
+
const ln = (...args) => lines.push(args.join(''));
|
|
211
|
+
const hr = () => ln('---');
|
|
212
|
+
|
|
213
|
+
if (!compiled) {
|
|
214
|
+
return '# Call Analysis\n\nNo compiled result available — AI compilation may have failed.\n';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Extract & deduplicate all data ──
|
|
218
|
+
const allTickets = dedupBy(compiled.tickets || [], t => t.ticket_id);
|
|
219
|
+
const allCRs = dedupBy(compiled.change_requests || [], cr => cr.id);
|
|
220
|
+
const allActions = dedupBy(compiled.action_items || [], ai => ai.id);
|
|
221
|
+
const allBlockers = dedupBy(compiled.blockers || [], b => b.id);
|
|
222
|
+
const allScope = dedupBy(compiled.scope_changes || [], sc => sc.id);
|
|
223
|
+
const allFiles = dedupBy(compiled.file_references || [], f => f.resolved_path || f.file_name);
|
|
224
|
+
const summary = compiled.summary || compiled.executive_summary || '';
|
|
225
|
+
const yourTasks = compiled.your_tasks || null;
|
|
226
|
+
|
|
227
|
+
// ── Discover & cluster participant names ──
|
|
228
|
+
const rawNames = new Set();
|
|
229
|
+
const addName = n => { if (n && n.trim()) rawNames.add(n.trim()); };
|
|
230
|
+
|
|
231
|
+
allActions.forEach(ai => addName(ai.assigned_to));
|
|
232
|
+
allCRs.forEach(cr => addName(cr.assigned_to));
|
|
233
|
+
allBlockers.forEach(b => addName(b.owner));
|
|
234
|
+
allScope.forEach(sc => addName(sc.decided_by));
|
|
235
|
+
allTickets.forEach(t => { addName(t.assignee); addName(t.reviewer); });
|
|
236
|
+
if (yourTasks?.user_name) addName(yourTasks.user_name);
|
|
237
|
+
if (yourTasks) {
|
|
238
|
+
(yourTasks.tasks_waiting_on_others || []).forEach(w => addName(w.waiting_on));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const clusterMap = clusterNames([...rawNames]);
|
|
242
|
+
|
|
243
|
+
// Remove generic team references
|
|
244
|
+
const teamKeywords = ['team', 'qa', 'dba', 'devops', 'db team', 'external'];
|
|
245
|
+
const people = [...clusterMap.keys()]
|
|
246
|
+
.filter(n => n && !teamKeywords.some(kw => n.toLowerCase() === kw))
|
|
247
|
+
.sort();
|
|
248
|
+
|
|
249
|
+
// Name matcher using cluster resolution
|
|
250
|
+
const nameMatch = (raw, canonical) => {
|
|
251
|
+
if (!raw || !canonical) return false;
|
|
252
|
+
return resolve(raw, clusterMap) === canonical;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// Detect current user's canonical name
|
|
256
|
+
const currentUserCanonical = meta.userName ? resolve(meta.userName, clusterMap) : null;
|
|
257
|
+
|
|
258
|
+
// Put current user first, others after
|
|
259
|
+
const orderedPeople = [];
|
|
260
|
+
if (currentUserCanonical && people.includes(currentUserCanonical)) {
|
|
261
|
+
orderedPeople.push(currentUserCanonical);
|
|
262
|
+
}
|
|
263
|
+
for (const p of people) {
|
|
264
|
+
if (p !== currentUserCanonical) orderedPeople.push(p);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ══════════════════════════════════════════════════════
|
|
268
|
+
// HEADER
|
|
269
|
+
// ══════════════════════════════════════════════════════
|
|
270
|
+
ln(`# 📋 Call Analysis — ${meta.callName || 'Unknown'}`);
|
|
271
|
+
ln('');
|
|
272
|
+
ln(`> **Date**: ${meta.processedAt ? meta.processedAt.slice(0, 10) : 'N/A'} `);
|
|
273
|
+
ln(`> **Participants**: ${orderedPeople.join(', ') || 'Unknown'} `);
|
|
274
|
+
ln(`> **Segments analyzed**: ${meta.segmentCount || 'N/A'} `);
|
|
275
|
+
ln(`> **Model**: ${meta.geminiModel || 'N/A'} `);
|
|
276
|
+
|
|
277
|
+
// Compilation stats
|
|
278
|
+
const comp = meta.compilation;
|
|
279
|
+
if (comp) {
|
|
280
|
+
const tu = comp.tokenUsage || {};
|
|
281
|
+
const durSec = comp.durationMs ? (comp.durationMs / 1000).toFixed(1) : '?';
|
|
282
|
+
ln(`> **Compilation**: ${durSec}s | ${(tu.inputTokens || 0).toLocaleString()} input → ${(tu.outputTokens || 0).toLocaleString()} output tokens | thinking: ${(tu.thoughtTokens || 0).toLocaleString()} `);
|
|
283
|
+
}
|
|
284
|
+
ln(`> **Compiled**: Yes — AI-merged final result`);
|
|
285
|
+
|
|
286
|
+
// Cost summary
|
|
287
|
+
const cost = meta.costSummary;
|
|
288
|
+
if (cost && cost.totalTokens > 0) {
|
|
289
|
+
ln(`> **Cost estimate**: $${cost.totalCost.toFixed(4)} (${cost.totalTokens.toLocaleString()} tokens | ${(cost.totalDurationMs / 1000).toFixed(1)}s AI time) `);
|
|
290
|
+
}
|
|
291
|
+
ln('');
|
|
292
|
+
|
|
293
|
+
// Segment breakdown
|
|
294
|
+
const segs = meta.segments || [];
|
|
295
|
+
if (segs.length > 0) {
|
|
296
|
+
ln('<details>');
|
|
297
|
+
ln(`<summary>📼 Segment Details (${segs.length} segments)</summary>`);
|
|
298
|
+
ln('');
|
|
299
|
+
ln('| # | File | Duration | Size |');
|
|
300
|
+
ln('| --- | --- | --- | --- |');
|
|
301
|
+
segs.forEach((s, i) => {
|
|
302
|
+
ln(`| ${i + 1} | ${s.file} | ${s.duration || '?'} | ${s.sizeMB || '?'} MB |`);
|
|
303
|
+
});
|
|
304
|
+
const totalDur = segs.reduce((sum, s) => sum + (s.durationSeconds || 0), 0);
|
|
305
|
+
const totalSize = segs.reduce((sum, s) => sum + (parseFloat(s.sizeMB) || 0), 0);
|
|
306
|
+
ln(`| | **Total** | **${Math.floor(totalDur / 60)}:${String(Math.round(totalDur % 60)).padStart(2, '0')}** | **${totalSize.toFixed(2)} MB** |`);
|
|
307
|
+
ln('');
|
|
308
|
+
if (meta.settings) {
|
|
309
|
+
ln(`> Speed: ${meta.settings.speed}x | Preset: ${meta.settings.preset} | Segment time: ${meta.settings.segmentTimeSec}s`);
|
|
310
|
+
}
|
|
311
|
+
ln('</details>');
|
|
312
|
+
ln('');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
hr();
|
|
316
|
+
ln('');
|
|
317
|
+
|
|
318
|
+
// ══════════════════════════════════════════════════════
|
|
319
|
+
// CONFIDENCE DISTRIBUTION
|
|
320
|
+
// ══════════════════════════════════════════════════════
|
|
321
|
+
const allConfItems = [
|
|
322
|
+
...allTickets, ...allCRs, ...allActions, ...allBlockers, ...allScope,
|
|
323
|
+
];
|
|
324
|
+
if (allConfItems.length > 0) {
|
|
325
|
+
const confHigh = allConfItems.filter(i => i.confidence === 'HIGH').length;
|
|
326
|
+
const confMed = allConfItems.filter(i => i.confidence === 'MEDIUM').length;
|
|
327
|
+
const confLow = allConfItems.filter(i => i.confidence === 'LOW').length;
|
|
328
|
+
const confMissing = allConfItems.length - confHigh - confMed - confLow;
|
|
329
|
+
const confTotal = allConfItems.length;
|
|
330
|
+
|
|
331
|
+
if (confHigh + confMed + confLow > 0) {
|
|
332
|
+
ln('### 📊 Confidence Distribution');
|
|
333
|
+
ln('');
|
|
334
|
+
ln(`| Level | Count | % |`);
|
|
335
|
+
ln(`| --- | --- | --- |`);
|
|
336
|
+
if (confHigh > 0) ln(`| 🟢 HIGH | ${confHigh} | ${((confHigh / confTotal) * 100).toFixed(0)}% |`);
|
|
337
|
+
if (confMed > 0) ln(`| 🟡 MEDIUM | ${confMed} | ${((confMed / confTotal) * 100).toFixed(0)}% |`);
|
|
338
|
+
if (confLow > 0) ln(`| 🔴 LOW | ${confLow} | ${((confLow / confTotal) * 100).toFixed(0)}% |`);
|
|
339
|
+
if (confMissing > 0) ln(`| ⚪ UNSET | ${confMissing} | ${((confMissing / confTotal) * 100).toFixed(0)}% |`);
|
|
340
|
+
ln('');
|
|
341
|
+
if (confLow > 0) {
|
|
342
|
+
ln('> ⚠️ **LOW confidence items** need human verification before acting on them.');
|
|
343
|
+
ln('');
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ══════════════════════════════════════════════════════
|
|
349
|
+
// EXECUTIVE SUMMARY
|
|
350
|
+
// ══════════════════════════════════════════════════════
|
|
351
|
+
ln('## 📝 Executive Summary');
|
|
352
|
+
ln('');
|
|
353
|
+
if (summary) ln(summary);
|
|
354
|
+
ln('');
|
|
355
|
+
|
|
356
|
+
// ══════════════════════════════════════════════════════
|
|
357
|
+
// COMPLETED IN CALL
|
|
358
|
+
// ══════════════════════════════════════════════════════
|
|
359
|
+
const completedInCall = yourTasks?.completed_in_call || [];
|
|
360
|
+
if (completedInCall.length > 0) {
|
|
361
|
+
ln('### ✅ Resolved During This Call');
|
|
362
|
+
ln('');
|
|
363
|
+
for (const item of completedInCall) {
|
|
364
|
+
ln(`- ✅ ${item}`);
|
|
365
|
+
}
|
|
366
|
+
ln('');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
hr();
|
|
370
|
+
ln('');
|
|
371
|
+
|
|
372
|
+
// ══════════════════════════════════════════════════════
|
|
373
|
+
// YOUR TASKS (current user — prominent top section)
|
|
374
|
+
// ══════════════════════════════════════════════════════
|
|
375
|
+
if (currentUserCanonical && yourTasks) {
|
|
376
|
+
ln(`## ⭐ Your Tasks — ${currentUserCanonical}`);
|
|
377
|
+
ln('');
|
|
378
|
+
|
|
379
|
+
// Your overall summary
|
|
380
|
+
if (yourTasks.summary) {
|
|
381
|
+
ln(`> ${yourTasks.summary}`);
|
|
382
|
+
ln('');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Owned tickets (compact)
|
|
386
|
+
const myTickets = dedupBy(
|
|
387
|
+
allTickets.filter(t => nameMatch(t.assignee, currentUserCanonical)),
|
|
388
|
+
t => t.ticket_id
|
|
389
|
+
);
|
|
390
|
+
if (myTickets.length > 0) {
|
|
391
|
+
ln(`**🎫 Your Tickets**: ${myTickets.map(t => `${t.ticket_id} (${(t.status || '?').replace(/_/g, ' ')})`).join(' · ')}`);
|
|
392
|
+
ln('');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// To-do items (merged from your_tasks.tasks_todo + action_items assigned to user)
|
|
396
|
+
const todoItems = dedupByDesc(yourTasks.tasks_todo || []);
|
|
397
|
+
const myActions = allActions.filter(ai =>
|
|
398
|
+
nameMatch(ai.assigned_to, currentUserCanonical) &&
|
|
399
|
+
(ai.status === 'todo' || ai.status === 'in_progress')
|
|
400
|
+
);
|
|
401
|
+
const allTodos = [...todoItems];
|
|
402
|
+
// Add action items not already in todo list (fuzzy match to avoid near-duplicates)
|
|
403
|
+
const todoDescKeys = new Set(allTodos.map(t => normalizeDesc(t.description)));
|
|
404
|
+
for (const ai of myActions) {
|
|
405
|
+
const dk = normalizeDesc(ai.description);
|
|
406
|
+
if (!todoDescKeys.has(dk)) {
|
|
407
|
+
allTodos.push(ai);
|
|
408
|
+
todoDescKeys.add(dk);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (allTodos.length > 0) {
|
|
412
|
+
ln('### 📌 To Do');
|
|
413
|
+
ln('');
|
|
414
|
+
for (const item of allTodos) {
|
|
415
|
+
const pri = priBadge(item.priority);
|
|
416
|
+
const conf = confBadge(item.confidence);
|
|
417
|
+
const source = item.source ? ` _(${item.source})_` : '';
|
|
418
|
+
const ts = item.referenced_at ? ` @ ${fmtTs(item.referenced_at, item.source_segment)}` : '';
|
|
419
|
+
const blocker = item.blocked_by ? `\n - ⛔ **Blocked by**: ${item.blocked_by}` : '';
|
|
420
|
+
const relTickets = (item.related_tickets || []).length > 0 ? `\n - Tickets: ${item.related_tickets.join(', ')}` : '';
|
|
421
|
+
const relChanges = (item.related_changes || []).length > 0 ? `\n - Changes: ${item.related_changes.join(', ')}` : '';
|
|
422
|
+
ln(`- [ ] ${item.description}${pri}${conf}${source}${ts}${blocker}${relTickets}${relChanges}`);
|
|
423
|
+
}
|
|
424
|
+
ln('');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// CRs assigned to user
|
|
428
|
+
const myCRs = dedupBy(
|
|
429
|
+
allCRs.filter(cr => nameMatch(cr.assigned_to, currentUserCanonical) && cr.status !== 'completed'),
|
|
430
|
+
cr => cr.id
|
|
431
|
+
);
|
|
432
|
+
if (myCRs.length > 0) {
|
|
433
|
+
ln('### 🔧 Your Change Requests');
|
|
434
|
+
ln('');
|
|
435
|
+
for (const cr of myCRs) {
|
|
436
|
+
const status = cr.status ? ` \`${cr.status}\`` : '';
|
|
437
|
+
const pri = priBadge(cr.priority);
|
|
438
|
+
const where = cr.where?.file_path ? ` → \`${cr.where.file_path}\`` : '';
|
|
439
|
+
const ts = cr.referenced_at ? ` @ ${fmtTs(cr.referenced_at, cr.source_segment)}` : '';
|
|
440
|
+
const type = cr.type ? ` _[${cr.type}]_` : '';
|
|
441
|
+
ln(`- **${cr.id}**: ${cr.title || cr.what}${status}${pri}${type}${where}${ts}`);
|
|
442
|
+
if (cr.what && cr.what !== cr.title) ln(` - **What**: ${cr.what}`);
|
|
443
|
+
if (cr.how) ln(` - **How**: ${cr.how}`);
|
|
444
|
+
if (cr.why) ln(` - **Why**: ${cr.why}`);
|
|
445
|
+
if (cr.blocked_by) ln(` - ⛔ **Blocked by**: ${cr.blocked_by}`);
|
|
446
|
+
if (cr.code_map_match) ln(` - Code map: \`${cr.code_map_match}\``);
|
|
447
|
+
if ((cr.related_tickets || []).length > 0) ln(` - Tickets: ${cr.related_tickets.join(', ')}`);
|
|
448
|
+
}
|
|
449
|
+
ln('');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Waiting on others
|
|
453
|
+
const waitingItems = dedupByDesc(yourTasks.tasks_waiting_on_others || []);
|
|
454
|
+
if (waitingItems.length > 0) {
|
|
455
|
+
ln('### ⏳ Waiting On Others');
|
|
456
|
+
ln('');
|
|
457
|
+
for (const w of waitingItems) {
|
|
458
|
+
const resolvedWho = resolve(w.waiting_on, clusterMap);
|
|
459
|
+
const ts = w.referenced_at ? ` @ ${fmtTs(w.referenced_at, w.source_segment)}` : '';
|
|
460
|
+
ln(`- ⏳ ${w.description} → waiting on **${resolvedWho}**${w.source ? ` _(${w.source})_` : ''}${ts}`);
|
|
461
|
+
}
|
|
462
|
+
ln('');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Decisions needed
|
|
466
|
+
const decisionItems = dedupByDesc(yourTasks.decisions_needed || []);
|
|
467
|
+
if (decisionItems.length > 0) {
|
|
468
|
+
ln('### ❓ Decisions Needed');
|
|
469
|
+
ln('');
|
|
470
|
+
for (const d of decisionItems) {
|
|
471
|
+
const resolvedWho = resolve(d.from_whom, clusterMap);
|
|
472
|
+
const ts = d.referenced_at ? ` @ ${fmtTs(d.referenced_at, d.source_segment)}` : '';
|
|
473
|
+
ln(`- ${d.description} → from **${resolvedWho}**${d.source ? ` _(${d.source})_` : ''}${ts}`);
|
|
474
|
+
}
|
|
475
|
+
ln('');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Blockers owned by user
|
|
479
|
+
const myBlockers = dedupBy(
|
|
480
|
+
allBlockers.filter(b => nameMatch(b.owner, currentUserCanonical)),
|
|
481
|
+
b => b.id
|
|
482
|
+
);
|
|
483
|
+
if (myBlockers.length > 0) {
|
|
484
|
+
ln('### 🚫 Your Blockers');
|
|
485
|
+
ln('');
|
|
486
|
+
for (const b of myBlockers) {
|
|
487
|
+
const env = (b.environments || []).length > 0 ? ` [${b.environments.join(', ')}]` : '';
|
|
488
|
+
const status = b.status ? ` (${b.status})` : '';
|
|
489
|
+
const type = b.type ? ` _[${b.type.replace(/_/g, ' ')}]_` : '';
|
|
490
|
+
const ts = b.referenced_at ? ` @ ${fmtTs(b.referenced_at, b.source_segment)}` : '';
|
|
491
|
+
const bConf = confBadge(b.confidence);
|
|
492
|
+
ln(`- **${b.id}**: ${b.description}${env}${status}${type}${bConf}${ts}`);
|
|
493
|
+
if (b.blocks?.length > 0) ln(` - Blocks: ${b.blocks.join(', ')}`);
|
|
494
|
+
if (b.checklist_match) ln(` - Checklist: ${b.checklist_match}`);
|
|
495
|
+
}
|
|
496
|
+
ln('');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
hr();
|
|
500
|
+
ln('');
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ══════════════════════════════════════════════════════
|
|
504
|
+
// DETAILED TICKET ANALYSIS
|
|
505
|
+
// ══════════════════════════════════════════════════════
|
|
506
|
+
if (allTickets.length > 0) {
|
|
507
|
+
ln('## 🎫 Detailed Ticket Analysis');
|
|
508
|
+
ln('');
|
|
509
|
+
|
|
510
|
+
for (const t of allTickets) {
|
|
511
|
+
const assignee = t.assignee ? resolve(t.assignee, clusterMap) : 'Unassigned';
|
|
512
|
+
const reviewer = t.reviewer ? resolve(t.reviewer, clusterMap) : null;
|
|
513
|
+
const status = (t.status || 'unknown').replace(/_/g, ' ');
|
|
514
|
+
|
|
515
|
+
const tConf = confBadge(t.confidence);
|
|
516
|
+
ln(`### ${t.ticket_id} — ${t.title || 'Untitled'}${tConf}`);
|
|
517
|
+
ln('');
|
|
518
|
+
ln(`> **Status**: ${status} | **Assignee**: ${assignee}${reviewer ? ` | **Reviewer**: ${reviewer}` : ''}`);
|
|
519
|
+
if (t.confidence_reason) ln(`> **Confidence**: ${t.confidence} — ${t.confidence_reason}`);
|
|
520
|
+
ln('');
|
|
521
|
+
|
|
522
|
+
// Documented state
|
|
523
|
+
const ds = t.documented_state;
|
|
524
|
+
if (ds) {
|
|
525
|
+
ln('#### 📄 Documented State');
|
|
526
|
+
ln('');
|
|
527
|
+
if (ds.source) ln(`- **Source**: \`${ds.source}\``);
|
|
528
|
+
if (ds.plan_status) ln(`- **Plan Status**: ${ds.plan_status}`);
|
|
529
|
+
if (ds.checklist_progress) ln(`- **Checklist**: ${ds.checklist_progress}`);
|
|
530
|
+
|
|
531
|
+
// Sub-tickets
|
|
532
|
+
if (ds.sub_tickets && ds.sub_tickets.length > 0) {
|
|
533
|
+
ln('- **Sub-tickets**:');
|
|
534
|
+
for (const st of ds.sub_tickets) {
|
|
535
|
+
ln(` - **${st.id}** ${st.title} — ${st.documented_status || '?'}`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Open blockers from docs
|
|
540
|
+
if (ds.open_blockers && ds.open_blockers.length > 0) {
|
|
541
|
+
ln('- **Documented Blockers**:');
|
|
542
|
+
for (const ob of ds.open_blockers) {
|
|
543
|
+
ln(` - ⚠️ ${ob}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
ln('');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Discussed state (what happened in the call)
|
|
550
|
+
const disc = t.discussed_state;
|
|
551
|
+
if (disc) {
|
|
552
|
+
ln('#### 💬 Discussed in Call');
|
|
553
|
+
ln('');
|
|
554
|
+
if (disc.summary) ln(disc.summary);
|
|
555
|
+
ln('');
|
|
556
|
+
|
|
557
|
+
// Discrepancies between docs and call
|
|
558
|
+
if (disc.discrepancies && disc.discrepancies.length > 0) {
|
|
559
|
+
ln('**⚡ Discrepancies (docs vs. call)**:');
|
|
560
|
+
ln('');
|
|
561
|
+
for (const d of disc.discrepancies) {
|
|
562
|
+
ln(`- ⚡ ${d}`);
|
|
563
|
+
}
|
|
564
|
+
ln('');
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Video segments (timestamps)
|
|
569
|
+
const vs = t.video_segments || [];
|
|
570
|
+
if (vs.length > 0) {
|
|
571
|
+
ln('#### 🎬 Video Segments');
|
|
572
|
+
ln('');
|
|
573
|
+
for (const seg of vs) {
|
|
574
|
+
const start = seg.start_time || '?';
|
|
575
|
+
const end = seg.end_time || '?';
|
|
576
|
+
const segLabel = seg.source_segment ? ` _(Seg ${seg.source_segment})_` : '';
|
|
577
|
+
ln(`- \`${start}\` → \`${end}\`${segLabel}: ${seg.description}`);
|
|
578
|
+
}
|
|
579
|
+
ln('');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Key comments (with timestamps and speakers)
|
|
583
|
+
const comments = t.comments || [];
|
|
584
|
+
if (comments.length > 0) {
|
|
585
|
+
ln('#### 🗣️ Key Quotes');
|
|
586
|
+
ln('');
|
|
587
|
+
for (const c of comments) {
|
|
588
|
+
const speaker = c.speaker ? resolve(c.speaker, clusterMap) : 'Unknown';
|
|
589
|
+
const ts = c.timestamp ? `\`${c.timestamp}\`` : '';
|
|
590
|
+
const segLabel = c.source_segment ? ` _(Seg ${c.source_segment})_` : '';
|
|
591
|
+
ln(`- ${ts}${segLabel} **${speaker}**: "${c.text}"`);
|
|
592
|
+
}
|
|
593
|
+
ln('');
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Code changes
|
|
597
|
+
const codeChanges = t.code_changes || [];
|
|
598
|
+
if (codeChanges.length > 0) {
|
|
599
|
+
ln('#### 💻 Code Changes');
|
|
600
|
+
ln('');
|
|
601
|
+
for (const cc of codeChanges) {
|
|
602
|
+
const type = cc.type ? `[${cc.type}]` : '';
|
|
603
|
+
const pri = priBadge(cc.priority);
|
|
604
|
+
const ts = cc.referenced_at ? ` @ ${fmtTs(cc.referenced_at, cc.source_segment)}` : '';
|
|
605
|
+
ln(`- ${type} \`${cc.file_path || '?'}\`${pri}${ts}`);
|
|
606
|
+
ln(` - ${cc.description}`);
|
|
607
|
+
if (cc.details && cc.details !== cc.description) {
|
|
608
|
+
ln(` - Details: ${cc.details}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
ln('');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Related CRs for this ticket
|
|
615
|
+
const ticketCRs = allCRs.filter(cr => (cr.related_tickets || []).includes(t.ticket_id));
|
|
616
|
+
if (ticketCRs.length > 0) {
|
|
617
|
+
ln(`#### 🔗 Change Requests for ${t.ticket_id}`);
|
|
618
|
+
ln('');
|
|
619
|
+
for (const cr of ticketCRs) {
|
|
620
|
+
const status = cr.status ? ` \`${cr.status}\`` : '';
|
|
621
|
+
const assignee = cr.assigned_to ? resolve(cr.assigned_to, clusterMap) : '?';
|
|
622
|
+
const ts = cr.referenced_at ? ` @ ${fmtTs(cr.referenced_at, cr.source_segment)}` : '';
|
|
623
|
+
ln(`- **${cr.id}**: ${cr.title || cr.what}${status} → ${assignee}${ts}`);
|
|
624
|
+
if (cr.what && cr.what !== cr.title) ln(` - What: ${cr.what}`);
|
|
625
|
+
if (cr.how) ln(` - How: ${cr.how}`);
|
|
626
|
+
if (cr.why) ln(` - Why: ${cr.why}`);
|
|
627
|
+
if (cr.where?.file_path) ln(` - File: \`${cr.where.file_path}\``);
|
|
628
|
+
}
|
|
629
|
+
ln('');
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Related blockers for this ticket
|
|
633
|
+
const ticketBlockers = allBlockers.filter(b => (b.blocks || []).includes(t.ticket_id));
|
|
634
|
+
if (ticketBlockers.length > 0) {
|
|
635
|
+
ln(`#### 🚫 Blockers for ${t.ticket_id}`);
|
|
636
|
+
ln('');
|
|
637
|
+
for (const b of ticketBlockers) {
|
|
638
|
+
const env = (b.environments || []).length > 0 ? ` [${b.environments.join(', ')}]` : '';
|
|
639
|
+
const status = b.status ? ` (${b.status})` : '';
|
|
640
|
+
const owner = b.owner ? ` → ${resolve(b.owner, clusterMap)}` : '';
|
|
641
|
+
const ts = b.referenced_at ? ` @ ${fmtTs(b.referenced_at, b.source_segment)}` : '';
|
|
642
|
+
ln(`- **${b.id}**: ${b.description}${env}${status}${owner}${ts}`);
|
|
643
|
+
}
|
|
644
|
+
ln('');
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
hr();
|
|
648
|
+
ln('');
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ══════════════════════════════════════════════════════
|
|
653
|
+
// ALL ACTION ITEMS (full detail table)
|
|
654
|
+
// ══════════════════════════════════════════════════════
|
|
655
|
+
if (allActions.length > 0) {
|
|
656
|
+
ln('## 📋 All Action Items');
|
|
657
|
+
ln('');
|
|
658
|
+
ln('| ID | Description | Assigned To | Status | Priority | Conf | Ref | Timestamp |');
|
|
659
|
+
ln('| --- | --- | --- | --- | --- | --- | --- | --- |');
|
|
660
|
+
for (const ai of allActions) {
|
|
661
|
+
const assignee = ai.assigned_to ? resolve(ai.assigned_to, clusterMap) : '-';
|
|
662
|
+
const status = (ai.status || '?').replace(/_/g, ' ');
|
|
663
|
+
const pri = ai.priority || '-';
|
|
664
|
+
const conf = ai.confidence || '-';
|
|
665
|
+
const confIcon = { HIGH: '🟢', MEDIUM: '🟡', LOW: '🔴' }[conf] || '';
|
|
666
|
+
const ref = [...(ai.related_tickets || []), ...(ai.related_changes || [])].join(', ') || '-';
|
|
667
|
+
const ts = ai.referenced_at || '-';
|
|
668
|
+
const seg = ai.source_segment ? `Seg ${ai.source_segment}` : '';
|
|
669
|
+
const dep = ai.depends_on ? ` ⛔ ${ai.depends_on}` : '';
|
|
670
|
+
const check = ai.checklist_match ? ` ✓${ai.checklist_match}` : '';
|
|
671
|
+
ln(`| ${ai.id} | ${ai.description}${dep}${check} | ${assignee} | ${status} | ${pri} | ${confIcon}${conf} | ${ref} | ${ts} ${seg} |`);
|
|
672
|
+
}
|
|
673
|
+
ln('');
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// ══════════════════════════════════════════════════════
|
|
677
|
+
// OTHER PARTICIPANTS
|
|
678
|
+
// ══════════════════════════════════════════════════════
|
|
679
|
+
const otherPeople = orderedPeople.filter(p => p !== currentUserCanonical);
|
|
680
|
+
if (otherPeople.length > 0) {
|
|
681
|
+
ln('## 👥 Other Participants');
|
|
682
|
+
ln('');
|
|
683
|
+
|
|
684
|
+
for (const person of otherPeople) {
|
|
685
|
+
const personActions = allActions.filter(ai => nameMatch(ai.assigned_to, person));
|
|
686
|
+
const personCRs = dedupBy(
|
|
687
|
+
allCRs.filter(cr => nameMatch(cr.assigned_to, person) && cr.status !== 'completed'),
|
|
688
|
+
cr => cr.id
|
|
689
|
+
);
|
|
690
|
+
const personBlockersOwned = dedupBy(
|
|
691
|
+
allBlockers.filter(b => nameMatch(b.owner, person)),
|
|
692
|
+
b => b.id
|
|
693
|
+
);
|
|
694
|
+
const personTickets = dedupBy(
|
|
695
|
+
allTickets.filter(t => nameMatch(t.assignee, person)),
|
|
696
|
+
t => t.ticket_id
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
// Check if person has anything
|
|
700
|
+
const hasContent = personActions.length > 0 || personCRs.length > 0 ||
|
|
701
|
+
personBlockersOwned.length > 0 || personTickets.length > 0;
|
|
702
|
+
|
|
703
|
+
if (!hasContent) continue;
|
|
704
|
+
|
|
705
|
+
ln(`### ${person}`);
|
|
706
|
+
ln('');
|
|
707
|
+
|
|
708
|
+
// Owned tickets (inline)
|
|
709
|
+
if (personTickets.length > 0) {
|
|
710
|
+
ln(`**🎫 Tickets**: ${personTickets.map(t => `${t.ticket_id} (${(t.status || '?').replace(/_/g, ' ')})`).join(' · ')}`);
|
|
711
|
+
ln('');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Action items
|
|
715
|
+
const actionableTodos = dedupByDesc(
|
|
716
|
+
personActions.filter(ai => ai.status === 'todo' || ai.status === 'in_progress')
|
|
717
|
+
);
|
|
718
|
+
if (actionableTodos.length > 0) {
|
|
719
|
+
ln('**📌 To Do**');
|
|
720
|
+
ln('');
|
|
721
|
+
for (const item of actionableTodos) {
|
|
722
|
+
const pri = priBadge(item.priority);
|
|
723
|
+
const ts = item.referenced_at ? ` @ ${fmtTs(item.referenced_at, item.source_segment)}` : '';
|
|
724
|
+
const ref = (item.related_tickets || []).length > 0 ? ` _(${item.related_tickets.join(', ')})_` : '';
|
|
725
|
+
const dep = item.depends_on ? ` ⛔ blocked by: ${item.depends_on}` : '';
|
|
726
|
+
ln(`- [ ] ${item.description}${pri}${ref}${ts}${dep}`);
|
|
727
|
+
}
|
|
728
|
+
ln('');
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Change requests
|
|
732
|
+
if (personCRs.length > 0) {
|
|
733
|
+
ln('**🔧 Change Requests**');
|
|
734
|
+
ln('');
|
|
735
|
+
for (const cr of personCRs) {
|
|
736
|
+
const status = cr.status ? ` \`${cr.status}\`` : '';
|
|
737
|
+
const pri = priBadge(cr.priority);
|
|
738
|
+
const where = cr.where?.file_path ? ` → \`${cr.where.file_path}\`` : '';
|
|
739
|
+
const ts = cr.referenced_at ? ` @ ${fmtTs(cr.referenced_at, cr.source_segment)}` : '';
|
|
740
|
+
ln(`- **${cr.id}**: ${cr.title || cr.what}${status}${pri}${where}${ts}`);
|
|
741
|
+
if (cr.what && cr.what !== cr.title) ln(` - What: ${cr.what}`);
|
|
742
|
+
if (cr.how) ln(` - How: ${cr.how}`);
|
|
743
|
+
if (cr.why) ln(` - Why: ${cr.why}`);
|
|
744
|
+
}
|
|
745
|
+
ln('');
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Blockers
|
|
749
|
+
if (personBlockersOwned.length > 0) {
|
|
750
|
+
ln('**🚫 Blockers**');
|
|
751
|
+
ln('');
|
|
752
|
+
for (const b of personBlockersOwned) {
|
|
753
|
+
const env = (b.environments || []).length > 0 ? ` [${b.environments.join(', ')}]` : '';
|
|
754
|
+
const status = b.status ? ` (${b.status})` : '';
|
|
755
|
+
const type = b.type ? ` _[${b.type.replace(/_/g, ' ')}]_` : '';
|
|
756
|
+
const ts = b.referenced_at ? ` @ ${fmtTs(b.referenced_at, b.source_segment)}` : '';
|
|
757
|
+
ln(`- **${b.id}**: ${b.description}${env}${status}${type}${ts}`);
|
|
758
|
+
if (b.blocks?.length > 0) ln(` - Blocks: ${b.blocks.join(', ')}`);
|
|
759
|
+
}
|
|
760
|
+
ln('');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
hr();
|
|
764
|
+
ln('');
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// ══════════════════════════════════════════════════════
|
|
769
|
+
// TEAM / EXTERNAL BLOCKERS
|
|
770
|
+
// ══════════════════════════════════════════════════════
|
|
771
|
+
const teamBlockers = dedupBy(
|
|
772
|
+
allBlockers.filter(b => !orderedPeople.some(p => nameMatch(b.owner, p))),
|
|
773
|
+
b => b.id
|
|
774
|
+
);
|
|
775
|
+
if (teamBlockers.length > 0) {
|
|
776
|
+
ln('## 🚫 Team / External Blockers');
|
|
777
|
+
ln('');
|
|
778
|
+
for (const b of teamBlockers) {
|
|
779
|
+
const env = (b.environments || []).length > 0 ? ` [${b.environments.join(', ')}]` : '';
|
|
780
|
+
const status = b.status ? ` (${b.status})` : '';
|
|
781
|
+
const type = b.type ? ` _[${b.type.replace(/_/g, ' ')}]_` : '';
|
|
782
|
+
const ts = b.referenced_at ? ` @ ${fmtTs(b.referenced_at, b.source_segment)}` : '';
|
|
783
|
+
const check = b.checklist_match ? ` ✓${b.checklist_match}` : '';
|
|
784
|
+
ln(`- **${b.id}** (${b.owner || 'unassigned'}): ${b.description}${env}${status}${type}${ts}${check}`);
|
|
785
|
+
if (b.blocks?.length > 0) ln(` - Blocks: ${b.blocks.join(', ')}`);
|
|
786
|
+
}
|
|
787
|
+
ln('');
|
|
788
|
+
hr();
|
|
789
|
+
ln('');
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// ══════════════════════════════════════════════════════
|
|
793
|
+
// SCOPE CHANGES
|
|
794
|
+
// ══════════════════════════════════════════════════════
|
|
795
|
+
if (allScope.length > 0) {
|
|
796
|
+
ln('## 🔀 Scope Changes');
|
|
797
|
+
ln('');
|
|
798
|
+
for (const sc of allScope) {
|
|
799
|
+
const icon = { added: '➕', removed: '➖', deferred: '⏸️', approach_changed: '🔄', ownership_changed: '👤', requirements_changed: '📋' }[sc.type] || '🔀';
|
|
800
|
+
const decidedBy = sc.decided_by ? resolve(sc.decided_by, clusterMap) : null;
|
|
801
|
+
const ts = sc.referenced_at ? ` @ ${fmtTs(sc.referenced_at, sc.source_segment)}` : '';
|
|
802
|
+
const scConf = confBadge(sc.confidence);
|
|
803
|
+
ln(`- ${icon} **${sc.id}** (${(sc.type || '').replace(/_/g, ' ')}): ${sc.new_scope}${scConf}${ts}`);
|
|
804
|
+
if (sc.original_scope && sc.original_scope !== 'not documented') {
|
|
805
|
+
ln(` - **Was**: ${sc.original_scope}`);
|
|
806
|
+
}
|
|
807
|
+
if (sc.reason) ln(` - **Reason**: ${sc.reason}`);
|
|
808
|
+
if (decidedBy) ln(` - **Decided by**: ${decidedBy}`);
|
|
809
|
+
if (sc.impact) ln(` - **Impact**: \`${sc.impact}\``);
|
|
810
|
+
if ((sc.related_tickets || []).length > 0) ln(` - **Tickets**: ${sc.related_tickets.join(', ')}`);
|
|
811
|
+
}
|
|
812
|
+
ln('');
|
|
813
|
+
hr();
|
|
814
|
+
ln('');
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ══════════════════════════════════════════════════════
|
|
818
|
+
// ALL CHANGE REQUESTS (complete table)
|
|
819
|
+
// ══════════════════════════════════════════════════════
|
|
820
|
+
if (allCRs.length > 0) {
|
|
821
|
+
ln('## 🔧 All Change Requests');
|
|
822
|
+
ln('');
|
|
823
|
+
ln('| ID | Title | Type | Status | Priority | Conf | Assignee | File | Timestamp |');
|
|
824
|
+
ln('| --- | --- | --- | --- | --- | --- | --- | --- | --- |');
|
|
825
|
+
for (const cr of allCRs) {
|
|
826
|
+
const assignee = cr.assigned_to ? resolve(cr.assigned_to, clusterMap) : '-';
|
|
827
|
+
const status = cr.status || '-';
|
|
828
|
+
const pri = cr.priority || '-';
|
|
829
|
+
const conf = cr.confidence || '-';
|
|
830
|
+
const confIcon = { HIGH: '🟢', MEDIUM: '🟡', LOW: '🔴' }[conf] || '';
|
|
831
|
+
const type = cr.type || '-';
|
|
832
|
+
const file = cr.where?.file_path ? `\`${cr.where.file_path}\`` : '-';
|
|
833
|
+
const ts = cr.referenced_at || '-';
|
|
834
|
+
const seg = cr.source_segment ? `Seg ${cr.source_segment}` : '';
|
|
835
|
+
ln(`| ${cr.id} | ${cr.title || cr.what || '-'} | ${type} | ${status} | ${pri} | ${confIcon}${conf} | ${assignee} | ${file} | ${ts} ${seg} |`);
|
|
836
|
+
}
|
|
837
|
+
ln('');
|
|
838
|
+
|
|
839
|
+
// Detailed breakdown in collapsible
|
|
840
|
+
ln('<details>');
|
|
841
|
+
ln('<summary>📖 Change Request Details</summary>');
|
|
842
|
+
ln('');
|
|
843
|
+
for (const cr of allCRs) {
|
|
844
|
+
const assignee = cr.assigned_to ? resolve(cr.assigned_to, clusterMap) : '?';
|
|
845
|
+
const status = cr.status ? ` \`${cr.status}\`` : '';
|
|
846
|
+
const ts = cr.referenced_at ? ` @ ${fmtTs(cr.referenced_at, cr.source_segment)}` : '';
|
|
847
|
+
ln(`**${cr.id}**: ${cr.title || cr.what}${status} → ${assignee}${ts}`);
|
|
848
|
+
if (cr.what && cr.what !== cr.title) ln(`- What: ${cr.what}`);
|
|
849
|
+
if (cr.how) ln(`- How: ${cr.how}`);
|
|
850
|
+
if (cr.why) ln(`- Why: ${cr.why}`);
|
|
851
|
+
if (cr.where?.file_path) ln(`- File: \`${cr.where.file_path}\` (${cr.where.module || '?'}/${cr.where.component || '?'})`);
|
|
852
|
+
if (cr.code_map_match) ln(`- Code map: \`${cr.code_map_match}\``);
|
|
853
|
+
if ((cr.related_tickets || []).length > 0) ln(`- Tickets: ${cr.related_tickets.join(', ')}`);
|
|
854
|
+
ln('');
|
|
855
|
+
}
|
|
856
|
+
ln('</details>');
|
|
857
|
+
ln('');
|
|
858
|
+
hr();
|
|
859
|
+
ln('');
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// ══════════════════════════════════════════════════════
|
|
863
|
+
// FILE REFERENCES (complete — not just actionable)
|
|
864
|
+
// ══════════════════════════════════════════════════════
|
|
865
|
+
if (allFiles.length > 0) {
|
|
866
|
+
// Split into actionable and reference-only
|
|
867
|
+
const actionableFiles = allFiles.filter(f => f.role && !['reference_only', 'source_of_truth'].includes(f.role));
|
|
868
|
+
const referenceFiles = allFiles.filter(f => !f.role || ['reference_only', 'source_of_truth'].includes(f.role));
|
|
869
|
+
|
|
870
|
+
if (actionableFiles.length > 0) {
|
|
871
|
+
ln('## 📂 Files Requiring Action');
|
|
872
|
+
ln('');
|
|
873
|
+
ln('| File | Role | Type | Tickets | Changes | Path |');
|
|
874
|
+
ln('| --- | --- | --- | --- | --- | --- |');
|
|
875
|
+
for (const f of actionableFiles) {
|
|
876
|
+
const role = (f.role || '').replace(/_/g, ' ');
|
|
877
|
+
const type = (f.file_type || '').replace(/_/g, ' ');
|
|
878
|
+
const tickets = (f.mentioned_in_tickets || []).join(', ') || '-';
|
|
879
|
+
const changes = (f.mentioned_in_changes || []).join(', ') || '-';
|
|
880
|
+
const fpath = f.resolved_path || '-';
|
|
881
|
+
ln(`| ${f.file_name} | ${role} | ${type} | ${tickets} | ${changes} | \`${fpath}\` |`);
|
|
882
|
+
}
|
|
883
|
+
ln('');
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (referenceFiles.length > 0) {
|
|
887
|
+
ln('<details>');
|
|
888
|
+
ln(`<summary>📎 Reference Files (${referenceFiles.length} files — not requiring changes)</summary>`);
|
|
889
|
+
ln('');
|
|
890
|
+
ln('| File | Type | Tickets | Context Doc | Notes |');
|
|
891
|
+
ln('| --- | --- | --- | --- | --- |');
|
|
892
|
+
for (const f of referenceFiles) {
|
|
893
|
+
const type = (f.file_type || '').replace(/_/g, ' ');
|
|
894
|
+
const tickets = (f.mentioned_in_tickets || []).join(', ') || '-';
|
|
895
|
+
const ctxDoc = f.context_doc_match || '-';
|
|
896
|
+
const notes = f.notes ? f.notes.slice(0, 80) + (f.notes.length > 80 ? '...' : '') : '-';
|
|
897
|
+
ln(`| ${f.file_name} | ${type} | ${tickets} | ${ctxDoc} | ${notes} |`);
|
|
898
|
+
}
|
|
899
|
+
ln('');
|
|
900
|
+
ln('</details>');
|
|
901
|
+
ln('');
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// ── Footer ──
|
|
906
|
+
hr();
|
|
907
|
+
ln('');
|
|
908
|
+
const genTs = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
909
|
+
const stats = [];
|
|
910
|
+
stats.push(`${allTickets.length} tickets`);
|
|
911
|
+
stats.push(`${allCRs.length} change requests`);
|
|
912
|
+
stats.push(`${allActions.length} action items`);
|
|
913
|
+
stats.push(`${allBlockers.length} blockers`);
|
|
914
|
+
stats.push(`${allScope.length} scope changes`);
|
|
915
|
+
stats.push(`${allFiles.length} file references`);
|
|
916
|
+
ln(`_Generated ${genTs} — AI-compiled final result | ${stats.join(' · ')}_`);
|
|
917
|
+
ln('');
|
|
918
|
+
|
|
919
|
+
return lines.join('\n');
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Legacy renderer — renders from the raw results object (merges all segment analyses).
|
|
924
|
+
* Applies ID-based dedup before rendering to reduce duplicates from naive flat merge.
|
|
925
|
+
* Kept for backward compatibility. Use renderResultsMarkdown() for new code.
|
|
926
|
+
*/
|
|
927
|
+
function renderResultsMarkdownLegacy(results) {
|
|
928
|
+
// Collect all analyses across all files/segments
|
|
929
|
+
const allAnalyses = [];
|
|
930
|
+
for (const file of (results.files || [])) {
|
|
931
|
+
for (const seg of (file.segments || [])) {
|
|
932
|
+
if (seg.analysis && !seg.analysis.error) {
|
|
933
|
+
allAnalyses.push({ seg: seg.segmentFile, ...seg.analysis });
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (allAnalyses.length === 0) {
|
|
939
|
+
return '# Call Analysis\n\nNo segments were successfully analyzed.\n';
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Merge all data — dedup applied at render time via dedupBy
|
|
943
|
+
const merged = {
|
|
944
|
+
tickets: allAnalyses.flatMap(a => a.tickets || []),
|
|
945
|
+
change_requests: allAnalyses.flatMap(a => a.change_requests || []),
|
|
946
|
+
action_items: allAnalyses.flatMap(a => a.action_items || []),
|
|
947
|
+
blockers: allAnalyses.flatMap(a => a.blockers || []),
|
|
948
|
+
scope_changes: allAnalyses.flatMap(a => a.scope_changes || []),
|
|
949
|
+
file_references: allAnalyses.flatMap(a => a.file_references || []),
|
|
950
|
+
summary: allAnalyses.map(a => a.summary).filter(Boolean).pop() || '',
|
|
951
|
+
your_tasks: allAnalyses.map(a => a.your_tasks).filter(Boolean).pop() || null,
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
const segmentCount = allAnalyses.length;
|
|
955
|
+
|
|
956
|
+
return renderResultsMarkdown({
|
|
957
|
+
compiled: merged,
|
|
958
|
+
meta: {
|
|
959
|
+
callName: results.callName,
|
|
960
|
+
processedAt: results.processedAt,
|
|
961
|
+
geminiModel: results.settings?.geminiModel,
|
|
962
|
+
userName: results.userName,
|
|
963
|
+
segmentCount,
|
|
964
|
+
},
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
module.exports = { renderResultsMarkdown, renderResultsMarkdownLegacy };
|