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,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress Updater — AI-powered progress assessment using change detection data.
|
|
3
|
+
*
|
|
4
|
+
* Two assessment modes:
|
|
5
|
+
* 1. Local (deterministic) — works without Gemini, uses correlation scores
|
|
6
|
+
* 2. AI-enhanced — sends items + changes to Gemini for smart assessment
|
|
7
|
+
*
|
|
8
|
+
* Also provides:
|
|
9
|
+
* - Markdown rendering of progress reports
|
|
10
|
+
* - Merge helpers to annotate analysis items with progress data
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const { extractJson } = require('./json-parser');
|
|
16
|
+
const config = require('../config');
|
|
17
|
+
// Access config.GEMINI_MODEL at call time for runtime model changes.
|
|
18
|
+
|
|
19
|
+
// ======================== STATUS CONSTANTS ========================
|
|
20
|
+
|
|
21
|
+
const STATUS = {
|
|
22
|
+
DONE: 'DONE',
|
|
23
|
+
IN_PROGRESS: 'IN_PROGRESS',
|
|
24
|
+
NOT_STARTED: 'NOT_STARTED',
|
|
25
|
+
SUPERSEDED: 'SUPERSEDED',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const STATUS_ICONS = {
|
|
29
|
+
DONE: '✅',
|
|
30
|
+
IN_PROGRESS: '🔄',
|
|
31
|
+
NOT_STARTED: '⏳',
|
|
32
|
+
SUPERSEDED: '🔀',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ======================== LOCAL ASSESSMENT ========================
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Deterministic progress assessment based purely on correlation scores.
|
|
39
|
+
* No API calls — works offline.
|
|
40
|
+
*
|
|
41
|
+
* @param {Array} items - From extractTrackableItems()
|
|
42
|
+
* @param {Map} correlations - From correlateItemsWithChanges()
|
|
43
|
+
* @returns {Array<{item_id, item_type, status, confidence, evidence[], notes}>}
|
|
44
|
+
*/
|
|
45
|
+
function assessProgressLocal(items, correlations) {
|
|
46
|
+
return items.map(item => {
|
|
47
|
+
const corr = correlations.get(item.id);
|
|
48
|
+
|
|
49
|
+
if (!corr || corr.score === 0) {
|
|
50
|
+
return {
|
|
51
|
+
item_id: item.id,
|
|
52
|
+
item_type: item.type,
|
|
53
|
+
title: item.title,
|
|
54
|
+
status: STATUS.NOT_STARTED,
|
|
55
|
+
confidence: 'LOW',
|
|
56
|
+
evidence: [],
|
|
57
|
+
notes: 'No matching changes detected in git history or documents.',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
item_id: item.id,
|
|
63
|
+
item_type: item.type,
|
|
64
|
+
title: item.title,
|
|
65
|
+
status: corr.localAssessment,
|
|
66
|
+
confidence: corr.localConfidence,
|
|
67
|
+
evidence: corr.evidence,
|
|
68
|
+
notes: `Local correlation score: ${corr.score.toFixed(2)}`,
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ======================== AI ASSESSMENT ========================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build a focused Gemini prompt for progress assessment.
|
|
77
|
+
*
|
|
78
|
+
* @param {Array} items - Trackable items
|
|
79
|
+
* @param {object} changeReport - From detectAllChanges()
|
|
80
|
+
* @param {Array} localAssessments - From assessProgressLocal()
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
function buildProgressPrompt(items, changeReport, localAssessments) {
|
|
84
|
+
const lines = [];
|
|
85
|
+
|
|
86
|
+
lines.push('# Progress Assessment Task');
|
|
87
|
+
lines.push('');
|
|
88
|
+
lines.push('You are analyzing whether work items from a meeting/call have been completed.');
|
|
89
|
+
lines.push('Based on the git changes and document updates below, assess each item\'s progress.');
|
|
90
|
+
lines.push('');
|
|
91
|
+
|
|
92
|
+
// Items section
|
|
93
|
+
lines.push('## Items To Assess');
|
|
94
|
+
lines.push('');
|
|
95
|
+
for (const item of items) {
|
|
96
|
+
lines.push(`### [${item.type}] ${item.id}: ${item.title}`);
|
|
97
|
+
if (item.description && item.description !== item.title) {
|
|
98
|
+
lines.push(` Description: ${item.description}`);
|
|
99
|
+
}
|
|
100
|
+
if (item.fileRefs.length > 0) {
|
|
101
|
+
lines.push(` Referenced files: ${item.fileRefs.join(', ')}`);
|
|
102
|
+
}
|
|
103
|
+
if (item.assignee) {
|
|
104
|
+
lines.push(` Assigned to: ${item.assignee}`);
|
|
105
|
+
}
|
|
106
|
+
lines.push('');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Git changes section
|
|
110
|
+
if (changeReport.git.available) {
|
|
111
|
+
lines.push('## Git Changes Since Last Analysis');
|
|
112
|
+
lines.push(`Branch: ${changeReport.git.branch || 'unknown'}`);
|
|
113
|
+
lines.push(`Commits: ${changeReport.git.commits.length}`);
|
|
114
|
+
lines.push(`Files changed: ${changeReport.git.changedFiles.length}`);
|
|
115
|
+
lines.push('');
|
|
116
|
+
|
|
117
|
+
if (changeReport.git.commits.length > 0) {
|
|
118
|
+
lines.push('### Recent Commits');
|
|
119
|
+
for (const c of changeReport.git.commits.slice(0, 50)) {
|
|
120
|
+
lines.push(`- **${c.hash}** (${c.date}): ${c.message}`);
|
|
121
|
+
if (c.files && c.files.length > 0) {
|
|
122
|
+
for (const f of c.files.slice(0, 10)) {
|
|
123
|
+
lines.push(` - ${f}`);
|
|
124
|
+
}
|
|
125
|
+
if (c.files.length > 10) lines.push(` - ... and ${c.files.length - 10} more`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
lines.push('');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (changeReport.git.changedFiles.length > 0) {
|
|
132
|
+
lines.push('### Changed Files');
|
|
133
|
+
for (const f of changeReport.git.changedFiles.slice(0, 100)) {
|
|
134
|
+
lines.push(`- [${f.status}] ${f.path} (${f.changes} change(s))`);
|
|
135
|
+
}
|
|
136
|
+
lines.push('');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Document changes
|
|
141
|
+
if (changeReport.documents.changes.length > 0) {
|
|
142
|
+
lines.push('## Document Updates (in call folder)');
|
|
143
|
+
for (const d of changeReport.documents.changes) {
|
|
144
|
+
lines.push(`- ${d.relPath} (modified: ${d.modified})`);
|
|
145
|
+
}
|
|
146
|
+
lines.push('');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Local correlations as hints
|
|
150
|
+
lines.push('## Pre-Computed Correlations (hints — verify and improve)');
|
|
151
|
+
for (const la of localAssessments) {
|
|
152
|
+
lines.push(`- ${la.item_id} (${la.item_type}): local says ${la.status} (${la.confidence})`);
|
|
153
|
+
if (la.evidence.length > 0) {
|
|
154
|
+
for (const e of la.evidence.slice(0, 5)) {
|
|
155
|
+
lines.push(` • [${e.type}] ${e.detail}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
lines.push('');
|
|
160
|
+
|
|
161
|
+
// Instructions
|
|
162
|
+
lines.push('## Instructions');
|
|
163
|
+
lines.push('');
|
|
164
|
+
lines.push('For EACH item, provide a JSON response with this structure:');
|
|
165
|
+
lines.push('```json');
|
|
166
|
+
lines.push('{');
|
|
167
|
+
lines.push(' "assessments": [');
|
|
168
|
+
lines.push(' {');
|
|
169
|
+
lines.push(' "item_id": "string — the exact item ID",');
|
|
170
|
+
lines.push(' "item_type": "string — ticket/change_request/action_item/blocker/scope_change",');
|
|
171
|
+
lines.push(' "status": "DONE | IN_PROGRESS | NOT_STARTED | SUPERSEDED",');
|
|
172
|
+
lines.push(' "confidence": "HIGH | MEDIUM | LOW",');
|
|
173
|
+
lines.push(' "evidence": ["array of specific evidence strings"],');
|
|
174
|
+
lines.push(' "notes": "string — brief explanation of your assessment"');
|
|
175
|
+
lines.push(' }');
|
|
176
|
+
lines.push(' ],');
|
|
177
|
+
lines.push(' "overall_summary": "Brief summary of overall project progress",');
|
|
178
|
+
lines.push(' "recommendations": ["Array of actionable recommendations"]');
|
|
179
|
+
lines.push('}');
|
|
180
|
+
lines.push('```');
|
|
181
|
+
lines.push('');
|
|
182
|
+
lines.push('Assessment rules:');
|
|
183
|
+
lines.push('- DONE = Clear evidence that the item\'s requirements are fully addressed');
|
|
184
|
+
lines.push('- IN_PROGRESS = Partial changes exist that relate to this item');
|
|
185
|
+
lines.push('- NOT_STARTED = No relevant changes found');
|
|
186
|
+
lines.push('- SUPERSEDED = Item is no longer relevant (replaced/cancelled by later changes)');
|
|
187
|
+
lines.push('- Be conservative — prefer IN_PROGRESS over DONE unless evidence is strong');
|
|
188
|
+
lines.push('- Use commit messages, file changes, and file names as evidence');
|
|
189
|
+
lines.push('- If a commit message explicitly references an item ID, that\'s strong evidence');
|
|
190
|
+
|
|
191
|
+
return lines.join('\n');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Run AI-powered progress assessment via Gemini.
|
|
196
|
+
*
|
|
197
|
+
* @param {object} ai - Initialized Gemini AI instance
|
|
198
|
+
* @param {Array} items - Trackable items
|
|
199
|
+
* @param {object} changeReport - From detectAllChanges()
|
|
200
|
+
* @param {Array} localAssessments - From assessProgressLocal()
|
|
201
|
+
* @param {object} [opts] - { thinkingBudget }
|
|
202
|
+
* @returns {object} { assessments[], overall_summary, recommendations[], model, tokenUsage }
|
|
203
|
+
*/
|
|
204
|
+
async function assessProgressWithAI(ai, items, changeReport, localAssessments, opts = {}) {
|
|
205
|
+
const prompt = buildProgressPrompt(items, changeReport, localAssessments);
|
|
206
|
+
const thinkingBudget = opts.thinkingBudget || 16384;
|
|
207
|
+
|
|
208
|
+
const result = await ai.models.generateContent({
|
|
209
|
+
model: config.GEMINI_MODEL,
|
|
210
|
+
contents: [{ role: 'user', parts: [{ text: prompt }] }],
|
|
211
|
+
config: {
|
|
212
|
+
temperature: 0,
|
|
213
|
+
thinkingConfig: { thinkingBudget },
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const rawText = result.text || '';
|
|
218
|
+
const tokenUsage = result.usageMetadata || {};
|
|
219
|
+
|
|
220
|
+
const parsed = extractJson(rawText);
|
|
221
|
+
if (!parsed) {
|
|
222
|
+
throw new Error('Failed to parse AI progress assessment response as JSON');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
assessments: parsed.assessments || [],
|
|
227
|
+
overall_summary: parsed.overall_summary || 'No summary provided',
|
|
228
|
+
recommendations: parsed.recommendations || [],
|
|
229
|
+
model: config.GEMINI_MODEL,
|
|
230
|
+
tokenUsage,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ======================== MERGE & SUMMARY ========================
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Merge AI or local assessments back into the analysis items,
|
|
238
|
+
* adding a `_progress` annotation to each matched item.
|
|
239
|
+
*
|
|
240
|
+
* @param {object} analysis - The compiled analysis object
|
|
241
|
+
* @param {Array} assessments - Array of assessment objects
|
|
242
|
+
* @returns {object} The annotated analysis (mutated)
|
|
243
|
+
*/
|
|
244
|
+
function mergeProgressIntoAnalysis(analysis, assessments) {
|
|
245
|
+
if (!analysis || !assessments) return analysis;
|
|
246
|
+
|
|
247
|
+
const assessMap = new Map();
|
|
248
|
+
for (const a of assessments) {
|
|
249
|
+
assessMap.set(a.item_id, a);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const sections = [
|
|
253
|
+
{ key: 'tickets', idField: 'ticket_id' },
|
|
254
|
+
{ key: 'change_requests', idField: 'id' },
|
|
255
|
+
{ key: 'action_items', idField: 'id' },
|
|
256
|
+
{ key: 'blockers', idField: 'id' },
|
|
257
|
+
{ key: 'scope_changes', idField: 'id' },
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
for (const { key, idField } of sections) {
|
|
261
|
+
for (const item of (analysis[key] || [])) {
|
|
262
|
+
const itemId = item[idField];
|
|
263
|
+
const assessment = assessMap.get(itemId);
|
|
264
|
+
if (assessment) {
|
|
265
|
+
item._progress = {
|
|
266
|
+
status: assessment.status,
|
|
267
|
+
confidence: assessment.confidence,
|
|
268
|
+
evidence: assessment.evidence,
|
|
269
|
+
notes: assessment.notes,
|
|
270
|
+
assessedAt: new Date().toISOString(),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return analysis;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Build summary counts from assessments.
|
|
281
|
+
*
|
|
282
|
+
* @param {Array} assessments
|
|
283
|
+
* @returns {object} { done, inProgress, notStarted, superseded, total }
|
|
284
|
+
*/
|
|
285
|
+
function buildProgressSummary(assessments) {
|
|
286
|
+
const summary = { done: 0, inProgress: 0, notStarted: 0, superseded: 0, total: assessments.length };
|
|
287
|
+
for (const a of assessments) {
|
|
288
|
+
switch (a.status) {
|
|
289
|
+
case STATUS.DONE: summary.done++; break;
|
|
290
|
+
case STATUS.IN_PROGRESS: summary.inProgress++; break;
|
|
291
|
+
case STATUS.NOT_STARTED: summary.notStarted++; break;
|
|
292
|
+
case STATUS.SUPERSEDED: summary.superseded++; break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return summary;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ======================== MARKDOWN RENDERING ========================
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Render a full progress report as Markdown.
|
|
302
|
+
*
|
|
303
|
+
* @param {object} params
|
|
304
|
+
* @param {Array} params.assessments - Per-item assessments
|
|
305
|
+
* @param {object} params.changeReport - From detectAllChanges()
|
|
306
|
+
* @param {string} [params.overallSummary] - AI-generated summary
|
|
307
|
+
* @param {string[]} [params.recommendations] - AI-generated recommendations
|
|
308
|
+
* @param {object} [params.meta] - { callName, timestamp, mode }
|
|
309
|
+
* @returns {string} Markdown content
|
|
310
|
+
*/
|
|
311
|
+
function renderProgressMarkdown({ assessments, changeReport, overallSummary, recommendations, meta = {} }) {
|
|
312
|
+
const lines = [];
|
|
313
|
+
const summary = buildProgressSummary(assessments);
|
|
314
|
+
const ts = meta.timestamp || new Date().toISOString();
|
|
315
|
+
|
|
316
|
+
lines.push(`# Progress Report${meta.callName ? ` — ${meta.callName}` : ''}`);
|
|
317
|
+
lines.push('');
|
|
318
|
+
lines.push(`> Generated: ${ts}`);
|
|
319
|
+
lines.push(`> Mode: ${meta.mode || 'local'}`);
|
|
320
|
+
if (changeReport.git.available) {
|
|
321
|
+
lines.push(`> Branch: ${changeReport.git.branch || 'unknown'}`);
|
|
322
|
+
lines.push(`> Commits since last analysis: ${changeReport.totals.commits}`);
|
|
323
|
+
lines.push(`> Files changed: ${changeReport.totals.filesChanged}`);
|
|
324
|
+
}
|
|
325
|
+
lines.push(`> Documents updated: ${changeReport.totals.docsChanged}`);
|
|
326
|
+
lines.push('');
|
|
327
|
+
|
|
328
|
+
// Overview bar
|
|
329
|
+
lines.push('## Overview');
|
|
330
|
+
lines.push('');
|
|
331
|
+
lines.push(`| Status | Count |`);
|
|
332
|
+
lines.push(`|--------|-------|`);
|
|
333
|
+
lines.push(`| ${STATUS_ICONS.DONE} Completed | ${summary.done} |`);
|
|
334
|
+
lines.push(`| ${STATUS_ICONS.IN_PROGRESS} In Progress | ${summary.inProgress} |`);
|
|
335
|
+
lines.push(`| ${STATUS_ICONS.NOT_STARTED} Not Started | ${summary.notStarted} |`);
|
|
336
|
+
lines.push(`| ${STATUS_ICONS.SUPERSEDED} Superseded | ${summary.superseded} |`);
|
|
337
|
+
lines.push(`| **Total** | **${summary.total}** |`);
|
|
338
|
+
lines.push('');
|
|
339
|
+
|
|
340
|
+
// Progress percentage
|
|
341
|
+
const pct = summary.total > 0 ? ((summary.done / summary.total) * 100).toFixed(0) : 0;
|
|
342
|
+
lines.push(`**Overall completion: ${pct}%** (${summary.done}/${summary.total} items done)`);
|
|
343
|
+
lines.push('');
|
|
344
|
+
|
|
345
|
+
// Overall summary
|
|
346
|
+
if (overallSummary) {
|
|
347
|
+
lines.push('## Summary');
|
|
348
|
+
lines.push('');
|
|
349
|
+
lines.push(overallSummary);
|
|
350
|
+
lines.push('');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Per-status sections
|
|
354
|
+
const statusOrder = [STATUS.DONE, STATUS.IN_PROGRESS, STATUS.NOT_STARTED, STATUS.SUPERSEDED];
|
|
355
|
+
const statusLabels = {
|
|
356
|
+
DONE: 'Completed Items',
|
|
357
|
+
IN_PROGRESS: 'In Progress',
|
|
358
|
+
NOT_STARTED: 'Not Started',
|
|
359
|
+
SUPERSEDED: 'Superseded',
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
for (const status of statusOrder) {
|
|
363
|
+
const items = assessments.filter(a => a.status === status);
|
|
364
|
+
if (items.length === 0) continue;
|
|
365
|
+
|
|
366
|
+
lines.push(`## ${STATUS_ICONS[status]} ${statusLabels[status]}`);
|
|
367
|
+
lines.push('');
|
|
368
|
+
|
|
369
|
+
for (const item of items) {
|
|
370
|
+
lines.push(`### ${item.item_id} (${item.item_type})`);
|
|
371
|
+
if (item.title) lines.push(`**${item.title}**`);
|
|
372
|
+
lines.push('');
|
|
373
|
+
lines.push(`- **Confidence:** ${item.confidence}`);
|
|
374
|
+
if (item.notes) lines.push(`- **Notes:** ${item.notes}`);
|
|
375
|
+
if (item.evidence && item.evidence.length > 0) {
|
|
376
|
+
lines.push('- **Evidence:**');
|
|
377
|
+
for (const e of item.evidence) {
|
|
378
|
+
if (typeof e === 'string') {
|
|
379
|
+
lines.push(` - ${e}`);
|
|
380
|
+
} else {
|
|
381
|
+
lines.push(` - [${e.type}] ${e.detail}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
lines.push('');
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Recommendations
|
|
390
|
+
if (recommendations && recommendations.length > 0) {
|
|
391
|
+
lines.push('## Recommendations');
|
|
392
|
+
lines.push('');
|
|
393
|
+
for (const r of recommendations) {
|
|
394
|
+
lines.push(`- ${r}`);
|
|
395
|
+
}
|
|
396
|
+
lines.push('');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Git details
|
|
400
|
+
if (changeReport.git.available && changeReport.git.commits.length > 0) {
|
|
401
|
+
lines.push('## Git Activity Summary');
|
|
402
|
+
lines.push('');
|
|
403
|
+
lines.push(`- **${changeReport.git.commits.length}** commit(s) since last analysis`);
|
|
404
|
+
lines.push(`- **${changeReport.git.changedFiles.length}** file(s) changed`);
|
|
405
|
+
if (changeReport.git.summary) {
|
|
406
|
+
lines.push(`- Diff summary: ${changeReport.git.summary}`);
|
|
407
|
+
}
|
|
408
|
+
lines.push('');
|
|
409
|
+
|
|
410
|
+
// Top changed files
|
|
411
|
+
const topFiles = changeReport.git.changedFiles
|
|
412
|
+
.sort((a, b) => b.changes - a.changes)
|
|
413
|
+
.slice(0, 15);
|
|
414
|
+
if (topFiles.length > 0) {
|
|
415
|
+
lines.push('### Top Changed Files');
|
|
416
|
+
lines.push('');
|
|
417
|
+
lines.push('| File | Status | Changes |');
|
|
418
|
+
lines.push('|------|--------|---------|');
|
|
419
|
+
for (const f of topFiles) {
|
|
420
|
+
lines.push(`| ${f.path} | ${f.status} | ${f.changes} |`);
|
|
421
|
+
}
|
|
422
|
+
lines.push('');
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Document changes
|
|
427
|
+
if (changeReport.documents.changes.length > 0) {
|
|
428
|
+
lines.push('## Updated Documents');
|
|
429
|
+
lines.push('');
|
|
430
|
+
for (const d of changeReport.documents.changes) {
|
|
431
|
+
lines.push(`- ${d.relPath} (updated: ${d.modified})`);
|
|
432
|
+
}
|
|
433
|
+
lines.push('');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
lines.push('---');
|
|
437
|
+
lines.push('*Generated by Smart Change Detection (v8.0.0)*');
|
|
438
|
+
|
|
439
|
+
return lines.join('\n');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
module.exports = {
|
|
443
|
+
STATUS,
|
|
444
|
+
STATUS_ICONS,
|
|
445
|
+
assessProgressLocal,
|
|
446
|
+
assessProgressWithAI,
|
|
447
|
+
buildProgressPrompt,
|
|
448
|
+
mergeProgressIntoAnalysis,
|
|
449
|
+
buildProgressSummary,
|
|
450
|
+
renderProgressMarkdown,
|
|
451
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress persistence — checkpoint/resume for long-running pipelines.
|
|
3
|
+
*
|
|
4
|
+
* Saves pipeline state to a JSON file so that if the process crashes,
|
|
5
|
+
* it can resume from where it left off instead of re-doing everything.
|
|
6
|
+
*
|
|
7
|
+
* State file: <targetDir>/.pipeline-state.json
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const STATE_FILE = '.pipeline-state.json';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {object} PipelineState
|
|
19
|
+
* @property {string} startedAt - ISO timestamp
|
|
20
|
+
* @property {string} callName - Name of the call
|
|
21
|
+
* @property {string} userName - User's name
|
|
22
|
+
* @property {string} phase - Current phase: compress|upload|analyze|compile|output
|
|
23
|
+
* @property {object} compression - Per-video compression status
|
|
24
|
+
* @property {object} uploads - Per-segment upload status (storagePath → url)
|
|
25
|
+
* @property {object} analyses - Per-segment analysis status (index → runFile)
|
|
26
|
+
* @property {boolean} compilationDone - Whether compilation is complete
|
|
27
|
+
* @property {string} updatedAt - Last update timestamp
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
class Progress {
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} targetDir - Directory for the state file
|
|
33
|
+
*/
|
|
34
|
+
constructor(targetDir) {
|
|
35
|
+
this.filePath = path.join(targetDir, STATE_FILE);
|
|
36
|
+
this.state = this._load();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Load existing state or create a fresh one.
|
|
41
|
+
* @returns {PipelineState}
|
|
42
|
+
*/
|
|
43
|
+
_load() {
|
|
44
|
+
try {
|
|
45
|
+
if (fs.existsSync(this.filePath)) {
|
|
46
|
+
const raw = fs.readFileSync(this.filePath, 'utf8');
|
|
47
|
+
return JSON.parse(raw);
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// Corrupt state file — start fresh
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
startedAt: new Date().toISOString(),
|
|
54
|
+
callName: null,
|
|
55
|
+
userName: null,
|
|
56
|
+
phase: 'init',
|
|
57
|
+
compression: {}, // videoName → { done: boolean, segmentCount: number }
|
|
58
|
+
uploads: {}, // storagePath → { url, done }
|
|
59
|
+
analyses: {}, // segKey → { runFile, done }
|
|
60
|
+
compilationDone: false,
|
|
61
|
+
updatedAt: new Date().toISOString(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Save current state to disk. */
|
|
66
|
+
save() {
|
|
67
|
+
this.state.updatedAt = new Date().toISOString();
|
|
68
|
+
try {
|
|
69
|
+
fs.writeFileSync(this.filePath, JSON.stringify(this.state, null, 2), 'utf8');
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.warn(` ⚠ Could not save progress: ${err.message}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Initialize state for a new run. */
|
|
76
|
+
init(callName, userName) {
|
|
77
|
+
this.state.callName = callName;
|
|
78
|
+
this.state.userName = userName;
|
|
79
|
+
this.state.phase = 'init';
|
|
80
|
+
this.save();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Set current phase. */
|
|
84
|
+
setPhase(phase) {
|
|
85
|
+
this.state.phase = phase;
|
|
86
|
+
this.save();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Mark a video's compression as done. */
|
|
90
|
+
markCompressed(videoName, segmentCount) {
|
|
91
|
+
this.state.compression[videoName] = { done: true, segmentCount };
|
|
92
|
+
this.save();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Check if a video has been compressed. */
|
|
96
|
+
isCompressed(videoName) {
|
|
97
|
+
return this.state.compression[videoName]?.done === true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Mark a segment upload as done. */
|
|
101
|
+
markUploaded(storagePath, url) {
|
|
102
|
+
this.state.uploads[storagePath] = { url, done: true };
|
|
103
|
+
this.save();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Check if a segment has been uploaded. Returns URL or null. */
|
|
107
|
+
getUploadUrl(storagePath) {
|
|
108
|
+
const entry = this.state.uploads[storagePath];
|
|
109
|
+
return entry?.done ? entry.url : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Mark a segment analysis as done. */
|
|
113
|
+
markAnalyzed(segKey, runFile) {
|
|
114
|
+
this.state.analyses[segKey] = { runFile, done: true };
|
|
115
|
+
this.save();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Check if a segment has been analyzed. */
|
|
119
|
+
isAnalyzed(segKey) {
|
|
120
|
+
return this.state.analyses[segKey]?.done === true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Mark compilation as done. */
|
|
124
|
+
markCompilationDone() {
|
|
125
|
+
this.state.compilationDone = true;
|
|
126
|
+
this.save();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Check if compilation is done. */
|
|
130
|
+
isCompilationDone() {
|
|
131
|
+
return this.state.compilationDone === true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Remove the state file (on successful completion). */
|
|
135
|
+
cleanup() {
|
|
136
|
+
try {
|
|
137
|
+
if (fs.existsSync(this.filePath)) {
|
|
138
|
+
fs.unlinkSync(this.filePath);
|
|
139
|
+
}
|
|
140
|
+
} catch { /* ignore */ }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Check if there is an existing resume state. */
|
|
144
|
+
hasResumableState() {
|
|
145
|
+
return this.state.phase !== 'init' && (
|
|
146
|
+
Object.keys(this.state.compression).length > 0 ||
|
|
147
|
+
Object.keys(this.state.uploads).length > 0 ||
|
|
148
|
+
Object.keys(this.state.analyses).length > 0
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Print a summary of what can be resumed. */
|
|
153
|
+
printResumeSummary() {
|
|
154
|
+
const comp = Object.values(this.state.compression).filter(c => c.done).length;
|
|
155
|
+
const uploads = Object.values(this.state.uploads).filter(u => u.done).length;
|
|
156
|
+
const analyses = Object.values(this.state.analyses).filter(a => a.done).length;
|
|
157
|
+
console.log(` Resume state found (${this.state.updatedAt}):`);
|
|
158
|
+
console.log(` Phase: ${this.state.phase}`);
|
|
159
|
+
console.log(` Compressions done: ${comp}`);
|
|
160
|
+
console.log(` Uploads done: ${uploads}`);
|
|
161
|
+
console.log(` Analyses done: ${analyses}`);
|
|
162
|
+
console.log(` Compilation: ${this.state.compilationDone ? 'done' : 'pending'}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = Progress;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive CLI prompts (stdin/stdout).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
/** Prompt user for a yes/no question on stdin. Returns true for yes. */
|
|
8
|
+
function promptUser(question) {
|
|
9
|
+
const readline = require('readline');
|
|
10
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
11
|
+
return new Promise(resolve => {
|
|
12
|
+
rl.question(question, answer => {
|
|
13
|
+
rl.close();
|
|
14
|
+
const a = (answer || '').trim().toLowerCase();
|
|
15
|
+
resolve(a === 'y' || a === 'yes');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Prompt user for free text input. Returns trimmed string. */
|
|
21
|
+
function promptUserText(question) {
|
|
22
|
+
const readline = require('readline');
|
|
23
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
24
|
+
return new Promise(resolve => {
|
|
25
|
+
rl.question(question, answer => {
|
|
26
|
+
rl.close();
|
|
27
|
+
resolve((answer || '').trim());
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { promptUser, promptUserText };
|