task-summary-extractor 8.3.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) 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/src/config.js +52 -3
  9. package/src/modes/focused-reanalysis.js +2 -1
  10. package/src/modes/progress-updater.js +1 -1
  11. package/src/phases/_shared.js +43 -0
  12. package/src/phases/compile.js +101 -0
  13. package/src/phases/deep-dive.js +118 -0
  14. package/src/phases/discover.js +178 -0
  15. package/src/phases/init.js +192 -0
  16. package/src/phases/output.js +238 -0
  17. package/src/phases/process-media.js +633 -0
  18. package/src/phases/services.js +104 -0
  19. package/src/phases/summary.js +86 -0
  20. package/src/pipeline.js +431 -1463
  21. package/src/renderers/docx.js +531 -0
  22. package/src/renderers/html.js +672 -0
  23. package/src/renderers/markdown.js +15 -183
  24. package/src/renderers/pdf.js +90 -0
  25. package/src/renderers/shared.js +211 -0
  26. package/src/schemas/analysis-compiled.schema.json +381 -0
  27. package/src/schemas/analysis-segment.schema.json +380 -0
  28. package/src/services/doc-parser.js +346 -0
  29. package/src/services/gemini.js +101 -44
  30. package/src/services/video.js +123 -8
  31. package/src/utils/adaptive-budget.js +6 -4
  32. package/src/utils/checkpoint.js +2 -1
  33. package/src/utils/cli.js +131 -110
  34. package/src/utils/colors.js +83 -0
  35. package/src/utils/confidence-filter.js +138 -0
  36. package/src/utils/diff-engine.js +2 -1
  37. package/src/utils/global-config.js +6 -5
  38. package/src/utils/health-dashboard.js +11 -9
  39. package/src/utils/json-parser.js +4 -2
  40. package/src/utils/learning-loop.js +3 -2
  41. package/src/utils/progress-bar.js +286 -0
  42. package/src/utils/quality-gate.js +4 -2
  43. package/src/utils/retry.js +3 -1
  44. package/src/utils/schema-validator.js +314 -0
@@ -0,0 +1,238 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ // --- Config ---
7
+ const config = require('../config');
8
+
9
+ // --- Services ---
10
+ const { uploadToStorage } = require('../services/firebase');
11
+
12
+ // --- Renderers ---
13
+ const { renderResultsMarkdown } = require('../renderers/markdown');
14
+ const { renderResultsHtml } = require('../renderers/html');
15
+ const { renderResultsPdf } = require('../renderers/pdf');
16
+ const { renderResultsDocx } = require('../renderers/docx');
17
+
18
+ // --- Utils ---
19
+ const { loadPreviousCompilation, generateDiff, renderDiffMarkdown } = require('../utils/diff-engine');
20
+ const { filterByConfidence } = require('../utils/confidence-filter');
21
+ const { c } = require('../utils/colors');
22
+
23
+ // --- Shared state ---
24
+ const { getLog, phaseTimer, PROJECT_ROOT } = require('./_shared');
25
+
26
+ /** Check whether a given output type should be rendered. */
27
+ function shouldRender(opts, type) {
28
+ if (opts.format === 'all') return true;
29
+ return opts.format === type;
30
+ }
31
+
32
+ // ======================== PHASE: OUTPUT ========================
33
+
34
+ /**
35
+ * Write results JSON, generate Markdown, upload final artifacts.
36
+ * Returns { runDir, jsonPath, mdPath }.
37
+ */
38
+ async function phaseOutput(ctx, results, compiledAnalysis, compilationRun, compilationPayload) {
39
+ const log = getLog();
40
+ const timer = phaseTimer('output');
41
+ const { opts, targetDir, storage, firebaseReady, callName, progress, costTracker, userName } = ctx;
42
+
43
+ progress.setPhase('output');
44
+
45
+ // Determine output directory
46
+ const runTs = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
47
+ const runDir = opts.outputDir
48
+ ? path.resolve(opts.outputDir)
49
+ : path.join(targetDir, 'runs', runTs);
50
+ fs.mkdirSync(runDir, { recursive: true });
51
+ log.step(`Run folder created → ${runDir}`);
52
+
53
+ // Copy compilation JSON into run folder
54
+ if (compilationPayload) {
55
+ const runCompFile = path.join(runDir, 'compilation.json');
56
+ fs.writeFileSync(runCompFile, JSON.stringify(compilationPayload, null, 2), 'utf8');
57
+ }
58
+
59
+ // Attach cost summary to results
60
+ results.costSummary = costTracker.getSummary();
61
+
62
+ // Write results JSON (always written unless --format excludes it)
63
+ const jsonPath = path.join(runDir, 'results.json');
64
+ if (shouldRender(opts, 'json') || opts.format === 'all') {
65
+ fs.writeFileSync(jsonPath, JSON.stringify(results, null, 2), 'utf8');
66
+ log.step(`Results JSON saved → ${jsonPath}`);
67
+ } else {
68
+ // Still write JSON internally (needed for uploads/diffs) but don't advertise
69
+ fs.writeFileSync(jsonPath, JSON.stringify(results, null, 2), 'utf8');
70
+ }
71
+
72
+ // Generate Markdown
73
+ const mdPath = path.join(runDir, 'results.md');
74
+ const totalSegs = results.files.reduce((s, f) => s + f.segmentCount, 0);
75
+
76
+ // Apply confidence filter for rendered output (MD/HTML) — results.json keeps full data
77
+ let renderData = compiledAnalysis;
78
+ if (compiledAnalysis && opts.minConfidence && opts.minConfidence !== 'low') {
79
+ renderData = filterByConfidence(compiledAnalysis, opts.minConfidence);
80
+ const meta = renderData._filterMeta;
81
+ if (meta && meta.removed > 0) {
82
+ console.log(` Confidence filter: ${meta.minConfidence} → kept ${meta.filteredCounts.total}/${meta.originalCounts.total} items (${meta.removed} removed)`);
83
+ log.step(`Confidence filter applied: ${meta.minConfidence}, removed ${meta.removed} items`);
84
+ }
85
+ }
86
+
87
+ // Build shared meta for renderers
88
+ const renderMeta = {
89
+ callName: results.callName,
90
+ processedAt: results.processedAt,
91
+ geminiModel: config.GEMINI_MODEL,
92
+ userName,
93
+ segmentCount: totalSegs,
94
+ compilation: compilationRun || null,
95
+ costSummary: results.costSummary,
96
+ segments: results.files.flatMap(f => {
97
+ const speed = results.settings?.speed || 1;
98
+ let cum = 0;
99
+ return (f.segments || []).map(s => {
100
+ const startSec = cum;
101
+ cum += (s.durationSeconds || 0) * speed;
102
+ return {
103
+ file: s.segmentFile,
104
+ duration: s.duration,
105
+ durationSeconds: s.durationSeconds,
106
+ sizeMB: s.fileSizeMB,
107
+ video: f.originalFile,
108
+ startTimeSec: startSec,
109
+ endTimeSec: cum,
110
+ segmentNumber: (s.segmentIndex || 0) + 1,
111
+ };
112
+ });
113
+ }),
114
+ settings: results.settings,
115
+ };
116
+
117
+ if (renderData && !renderData._incomplete) {
118
+ // --- Markdown report ---
119
+ if (shouldRender(opts, 'md')) {
120
+ const mdContent = renderResultsMarkdown({ compiled: renderData, meta: renderMeta });
121
+ fs.writeFileSync(mdPath, mdContent, 'utf8');
122
+ log.step(`Results MD saved (compiled) → ${mdPath}`);
123
+ console.log(` ${c.success('Markdown report (AI-compiled)')} → ${c.cyan(path.basename(mdPath))}`);
124
+ }
125
+
126
+ // --- HTML report ---
127
+ if (shouldRender(opts, 'html') && !opts.noHtml) {
128
+ const htmlPath = path.join(runDir, 'results.html');
129
+ const htmlContent = renderResultsHtml({ compiled: renderData, meta: renderMeta });
130
+ fs.writeFileSync(htmlPath, htmlContent, 'utf8');
131
+ log.step(`Results HTML saved → ${htmlPath}`);
132
+ console.log(` ${c.success('HTML report')} → ${c.cyan(path.basename(htmlPath))}`);
133
+
134
+ // --- PDF report (requires HTML first) ---
135
+ if (shouldRender(opts, 'pdf')) {
136
+ try {
137
+ const pdfPath = path.join(runDir, 'results.pdf');
138
+ const pdfInfo = await renderResultsPdf(htmlContent, pdfPath);
139
+ log.step(`Results PDF saved → ${pdfPath}`);
140
+ console.log(` ${c.success('PDF report')} → ${c.cyan(path.basename(pdfPath))} ${c.dim(`(${(pdfInfo.bytes / 1024).toFixed(0)} KB)`)}`);
141
+ } catch (pdfErr) {
142
+ console.warn(` ${c.warn('PDF generation failed:')} ${pdfErr.message}`);
143
+ log.warn(`PDF generation error: ${pdfErr.message}`);
144
+ }
145
+ }
146
+ } else if (shouldRender(opts, 'pdf')) {
147
+ // PDF requested without HTML — generate HTML in memory
148
+ try {
149
+ const htmlContent = renderResultsHtml({ compiled: renderData, meta: renderMeta });
150
+ const pdfPath = path.join(runDir, 'results.pdf');
151
+ const pdfInfo = await renderResultsPdf(htmlContent, pdfPath);
152
+ log.step(`Results PDF saved → ${pdfPath}`);
153
+ console.log(` ${c.success('PDF report')} → ${c.cyan(path.basename(pdfPath))} ${c.dim(`(${(pdfInfo.bytes / 1024).toFixed(0)} KB)`)}`);
154
+ } catch (pdfErr) {
155
+ console.warn(` ${c.warn('PDF generation failed:')} ${pdfErr.message}`);
156
+ log.warn(`PDF generation error: ${pdfErr.message}`);
157
+ }
158
+ }
159
+
160
+ // --- DOCX report ---
161
+ if (shouldRender(opts, 'docx')) {
162
+ try {
163
+ const docxPath = path.join(runDir, 'results.docx');
164
+ const docxBuffer = await renderResultsDocx({ compiled: renderData, meta: renderMeta });
165
+ fs.writeFileSync(docxPath, docxBuffer);
166
+ log.step(`Results DOCX saved → ${docxPath}`);
167
+ console.log(` ${c.success('DOCX report')} → ${c.cyan(path.basename(docxPath))} ${c.dim(`(${(docxBuffer.length / 1024).toFixed(0)} KB)`)}`);
168
+ } catch (docxErr) {
169
+ console.warn(` ${c.warn('DOCX generation failed:')} ${docxErr.message}`);
170
+ log.warn(`DOCX generation error: ${docxErr.message}`);
171
+ }
172
+ }
173
+ } else if (shouldRender(opts, 'md')) {
174
+ const { renderResultsMarkdownLegacy } = require('../renderers/markdown');
175
+ const mdContent = renderResultsMarkdownLegacy(results);
176
+ fs.writeFileSync(mdPath, mdContent, 'utf8');
177
+ log.step(`Results MD saved (legacy merge) → ${mdPath}`);
178
+ console.log(` ${c.success('Markdown report (legacy merge)')} → ${c.cyan(path.basename(mdPath))}`);
179
+ }
180
+
181
+ // === DIFF ENGINE (v6) ===
182
+ let diffResult = null;
183
+ if (!opts.disableDiff && compiledAnalysis) {
184
+ try {
185
+ const prevComp = loadPreviousCompilation(targetDir, runTs);
186
+ if (prevComp && prevComp.compiled) {
187
+ diffResult = generateDiff(compiledAnalysis, prevComp.compiled);
188
+ // Inject the previous run timestamp into the diff
189
+ if (diffResult.hasDiff) {
190
+ diffResult.previousTimestamp = prevComp.timestamp;
191
+ const diffMd = renderDiffMarkdown(diffResult);
192
+ if (shouldRender(opts, 'md')) {
193
+ fs.appendFileSync(mdPath, '\n\n' + diffMd, 'utf8');
194
+ }
195
+ fs.writeFileSync(path.join(runDir, 'diff.json'), JSON.stringify(diffResult, null, 2), 'utf8');
196
+ log.step(`Diff report: ${diffResult.totals.newItems} new, ${diffResult.totals.removedItems} removed, ${diffResult.totals.changedItems} changed`);
197
+ console.log(` ${c.success('Diff report appended')} (vs ${c.dim(prevComp.timestamp)})`);
198
+ } else {
199
+ console.log(` ${c.info('No differences vs previous run')} (${c.dim(prevComp.timestamp)})`);
200
+ }
201
+ } else {
202
+ console.log(` ${c.info('No previous compilation found for diff comparison')}`);
203
+ }
204
+ } catch (diffErr) {
205
+ console.warn(` ${c.warn('Diff generation failed:')} ${diffErr.message}`);
206
+ log.warn(`Diff generation error: ${diffErr.message}`);
207
+ }
208
+ }
209
+
210
+ // Upload results to Firebase
211
+ if (firebaseReady && !opts.skipUpload && !opts.dryRun) {
212
+ try {
213
+ const resultsStoragePath = `calls/${callName}/runs/${runTs}/results.json`;
214
+ // Results always upload fresh (never skip-existing) — they change every run
215
+ const url = await uploadToStorage(storage, jsonPath, resultsStoragePath);
216
+ results.storageUrl = url;
217
+ fs.writeFileSync(jsonPath, JSON.stringify(results, null, 2), 'utf8');
218
+ console.log(` ${c.success('Results JSON uploaded')} → ${c.dim(resultsStoragePath)}`);
219
+
220
+ if (shouldRender(opts, 'md') && fs.existsSync(mdPath)) {
221
+ const mdStoragePath = `calls/${callName}/runs/${runTs}/results.md`;
222
+ await uploadToStorage(storage, mdPath, mdStoragePath);
223
+ console.log(` ${c.success('Results MD uploaded')} → ${c.dim(mdStoragePath)}`);
224
+ }
225
+ } catch (err) {
226
+ console.warn(` ${c.warn('Results upload failed:')} ${err.message}`);
227
+ }
228
+ } else if (opts.skipUpload) {
229
+ console.log(` ${c.warn('Skipping results upload')} ${c.dim('(--skip-upload)')}`);
230
+ } else {
231
+ console.log(` ${c.warn('Skipping results upload')} ${c.dim('(Firebase auth not configured)')}`);
232
+ }
233
+
234
+ timer.end();
235
+ return { runDir, jsonPath, mdPath, runTs };
236
+ }
237
+
238
+ module.exports = phaseOutput;