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,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global configuration manager — persistent config stored in ~/.taskexrc
|
|
3
|
+
*
|
|
4
|
+
* Resolution priority (highest wins):
|
|
5
|
+
* 1. CLI flags (--gemini-key, --firebase-key, etc.)
|
|
6
|
+
* 2. process.env (set by user shell or CI)
|
|
7
|
+
* 3. CWD .env file (project-specific)
|
|
8
|
+
* 4. ~/.taskexrc (global persistent config)
|
|
9
|
+
* 5. Package root .env (development fallback)
|
|
10
|
+
*
|
|
11
|
+
* The global config is a JSON file at ~/.taskexrc containing:
|
|
12
|
+
* { "GEMINI_API_KEY": "...", "FIREBASE_API_KEY": "...", ... }
|
|
13
|
+
*
|
|
14
|
+
* Use `taskex config` to interactively set/update keys.
|
|
15
|
+
* Use `taskex config --show` to display current saved config.
|
|
16
|
+
* Use `taskex config --clear` to remove the global config file.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const os = require('os');
|
|
24
|
+
|
|
25
|
+
// ── Config file path ──────────────────────────────────────────────────────
|
|
26
|
+
const CONFIG_FILE = path.join(os.homedir(), '.taskexrc');
|
|
27
|
+
|
|
28
|
+
// ── Known config keys ─────────────────────────────────────────────────────
|
|
29
|
+
// Maps config key name → description (for interactive prompts)
|
|
30
|
+
const CONFIG_KEYS = {
|
|
31
|
+
GEMINI_API_KEY: { label: 'Gemini API Key', required: true, hint: 'Get one at https://aistudio.google.com/apikey' },
|
|
32
|
+
GEMINI_MODEL: { label: 'Default Gemini Model', required: false, hint: 'e.g. gemini-2.5-flash, gemini-2.5-pro' },
|
|
33
|
+
FIREBASE_API_KEY: { label: 'Firebase API Key', required: false, hint: 'From Firebase Console → Project Settings' },
|
|
34
|
+
FIREBASE_PROJECT_ID: { label: 'Firebase Project ID', required: false, hint: 'e.g. my-project-12345' },
|
|
35
|
+
FIREBASE_STORAGE_BUCKET: { label: 'Firebase Storage Bucket', required: false, hint: 'e.g. my-project-12345.appspot.com' },
|
|
36
|
+
FIREBASE_AUTH_DOMAIN: { label: 'Firebase Auth Domain', required: false, hint: 'e.g. my-project-12345.firebaseapp.com' },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ── Read / Write ──────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Load the global config from ~/.taskexrc.
|
|
43
|
+
* Returns an object of key-value pairs, or {} if file doesn't exist.
|
|
44
|
+
* @returns {Record<string, string>}
|
|
45
|
+
*/
|
|
46
|
+
function loadGlobalConfig() {
|
|
47
|
+
try {
|
|
48
|
+
if (!fs.existsSync(CONFIG_FILE)) return {};
|
|
49
|
+
const raw = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return {};
|
|
52
|
+
return parsed;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
// Warn if file exists but is corrupt (not just missing)
|
|
55
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
56
|
+
process.stderr.write(` ⚠ Could not parse ${CONFIG_FILE}: ${err.message}\n`);
|
|
57
|
+
process.stderr.write(` Run \`taskex config\` to reconfigure, or delete the file.\n`);
|
|
58
|
+
}
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Save the global config to ~/.taskexrc.
|
|
65
|
+
* Merges new values into existing config (doesn't overwrite unrelated keys).
|
|
66
|
+
* @param {Record<string, string>} newValues
|
|
67
|
+
*/
|
|
68
|
+
function saveGlobalConfig(newValues) {
|
|
69
|
+
const existing = loadGlobalConfig();
|
|
70
|
+
const merged = { ...existing, ...newValues };
|
|
71
|
+
|
|
72
|
+
// Remove empty/null values
|
|
73
|
+
for (const key of Object.keys(merged)) {
|
|
74
|
+
if (!merged[key]) delete merged[key];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + '\n', 'utf8');
|
|
78
|
+
// Restrict permissions on config file (contains secrets)
|
|
79
|
+
try {
|
|
80
|
+
if (process.platform !== 'win32') {
|
|
81
|
+
fs.chmodSync(CONFIG_FILE, 0o600);
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// Best-effort — Windows doesn't support chmod
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Delete the global config file.
|
|
90
|
+
* @returns {boolean} true if file was deleted
|
|
91
|
+
*/
|
|
92
|
+
function clearGlobalConfig() {
|
|
93
|
+
try {
|
|
94
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
95
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Inject into process.env ───────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Load global config and inject any values into process.env that aren't
|
|
108
|
+
* already set. This respects the priority chain: env vars / CLI flags
|
|
109
|
+
* that are already set take precedence over global config.
|
|
110
|
+
*
|
|
111
|
+
* Call this BEFORE dotenv loads (so it's lower priority than .env too —
|
|
112
|
+
* actually call it AFTER dotenv, but only inject if still missing).
|
|
113
|
+
*
|
|
114
|
+
* @returns {string[]} List of keys that were injected from global config
|
|
115
|
+
*/
|
|
116
|
+
function injectGlobalConfig() {
|
|
117
|
+
const config = loadGlobalConfig();
|
|
118
|
+
const injected = [];
|
|
119
|
+
|
|
120
|
+
for (const [key, value] of Object.entries(config)) {
|
|
121
|
+
if (value && (!process.env[key] || process.env[key] === '')) {
|
|
122
|
+
process.env[key] = value;
|
|
123
|
+
injected.push(key);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return injected;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Interactive setup ─────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Mask a secret for display — shows first 4 + last 4 chars.
|
|
134
|
+
* @param {string} value
|
|
135
|
+
* @returns {string}
|
|
136
|
+
*/
|
|
137
|
+
function maskSecret(value) {
|
|
138
|
+
if (!value || value.length < 12) return '****';
|
|
139
|
+
return value.slice(0, 4) + '...' + value.slice(-4);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Interactive config setup — prompts for each key and saves to ~/.taskexrc.
|
|
144
|
+
* Shows current values (masked) and lets user update or keep existing.
|
|
145
|
+
*
|
|
146
|
+
* @param {object} [options]
|
|
147
|
+
* @param {boolean} [options.showOnly=false] Just display, don't prompt
|
|
148
|
+
* @param {boolean} [options.clear=false] Delete config file
|
|
149
|
+
* @param {boolean} [options.onlyMissing=false] Only prompt for keys not yet set anywhere
|
|
150
|
+
* @returns {Promise<void>}
|
|
151
|
+
*/
|
|
152
|
+
async function interactiveSetup({ showOnly = false, clear = false, onlyMissing = false } = {}) {
|
|
153
|
+
const readline = require('readline');
|
|
154
|
+
|
|
155
|
+
if (clear) {
|
|
156
|
+
const removed = clearGlobalConfig();
|
|
157
|
+
if (removed) {
|
|
158
|
+
console.log('\n ✓ Global config cleared (~/.taskexrc deleted)\n');
|
|
159
|
+
} else {
|
|
160
|
+
console.log('\n No global config to clear.\n');
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const existing = loadGlobalConfig();
|
|
166
|
+
|
|
167
|
+
if (showOnly) {
|
|
168
|
+
console.log('');
|
|
169
|
+
console.log(' Global Config (~/.taskexrc)');
|
|
170
|
+
console.log(' ─────────────────────────────');
|
|
171
|
+
|
|
172
|
+
const keys = Object.keys(CONFIG_KEYS);
|
|
173
|
+
let hasAny = false;
|
|
174
|
+
for (const key of keys) {
|
|
175
|
+
const val = existing[key] || process.env[key];
|
|
176
|
+
const source = existing[key] ? 'saved' : process.env[key] ? 'env' : '';
|
|
177
|
+
if (val) {
|
|
178
|
+
const display = key.includes('KEY') || key.includes('SECRET') ? maskSecret(val) : val;
|
|
179
|
+
console.log(` ${CONFIG_KEYS[key].label}: ${display} (${source})`);
|
|
180
|
+
hasAny = true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!hasAny) {
|
|
184
|
+
console.log(' (empty — run `taskex config` to set up)');
|
|
185
|
+
}
|
|
186
|
+
console.log(`\n Config file: ${CONFIG_FILE}\n`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Interactive prompts
|
|
191
|
+
console.log('');
|
|
192
|
+
console.log(' ┌──────────────────────────────────────────────────────────┐');
|
|
193
|
+
console.log(' │ 🔧 taskex — Global Configuration │');
|
|
194
|
+
console.log(' └──────────────────────────────────────────────────────────┘');
|
|
195
|
+
console.log('');
|
|
196
|
+
console.log(` Config file: ${CONFIG_FILE}`);
|
|
197
|
+
console.log(' Values saved here are used whenever .env or CLI flags don\'t set them.');
|
|
198
|
+
console.log(' Press Enter to keep the current value, or type a new one.');
|
|
199
|
+
console.log('');
|
|
200
|
+
|
|
201
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
202
|
+
const ask = (question) => new Promise(resolve => {
|
|
203
|
+
rl.question(question, answer => resolve((answer || '').trim()));
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const updates = {};
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
for (const [key, meta] of Object.entries(CONFIG_KEYS)) {
|
|
210
|
+
const current = existing[key] || process.env[key] || '';
|
|
211
|
+
|
|
212
|
+
// If onlyMissing mode, skip keys that are already set
|
|
213
|
+
if (onlyMissing && current) continue;
|
|
214
|
+
|
|
215
|
+
const displayCurrent = current
|
|
216
|
+
? (key.includes('KEY') || key.includes('SECRET') ? maskSecret(current) : current)
|
|
217
|
+
: '(not set)';
|
|
218
|
+
|
|
219
|
+
const reqTag = meta.required ? ' *required*' : '';
|
|
220
|
+
console.log(` ${meta.label}${reqTag}`);
|
|
221
|
+
if (meta.hint) console.log(` ${meta.hint}`);
|
|
222
|
+
console.log(` Current: ${displayCurrent}`);
|
|
223
|
+
|
|
224
|
+
const answer = await ask(' New value (Enter to keep): ');
|
|
225
|
+
if (answer) {
|
|
226
|
+
updates[key] = answer;
|
|
227
|
+
}
|
|
228
|
+
console.log('');
|
|
229
|
+
}
|
|
230
|
+
} finally {
|
|
231
|
+
rl.close();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (Object.keys(updates).length > 0) {
|
|
235
|
+
saveGlobalConfig(updates);
|
|
236
|
+
console.log(` ✓ Saved ${Object.keys(updates).length} value(s) to ${CONFIG_FILE}`);
|
|
237
|
+
console.log('');
|
|
238
|
+
|
|
239
|
+
// Also inject into current process so the pipeline can proceed
|
|
240
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
241
|
+
process.env[k] = v;
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
console.log(' No changes made.');
|
|
245
|
+
console.log('');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Quick prompt for a single missing required key.
|
|
251
|
+
* Used during first-run when GEMINI_API_KEY is missing after all resolution.
|
|
252
|
+
*
|
|
253
|
+
* @param {string} key - The config key (e.g. 'GEMINI_API_KEY')
|
|
254
|
+
* @returns {Promise<string|null>} The value entered, or null if skipped
|
|
255
|
+
*/
|
|
256
|
+
async function promptForKey(key) {
|
|
257
|
+
const meta = CONFIG_KEYS[key];
|
|
258
|
+
if (!meta) return null;
|
|
259
|
+
|
|
260
|
+
const readline = require('readline');
|
|
261
|
+
|
|
262
|
+
console.log('');
|
|
263
|
+
console.log(` ⚠ ${meta.label} is not configured.`);
|
|
264
|
+
if (meta.hint) console.log(` ${meta.hint}`);
|
|
265
|
+
console.log('');
|
|
266
|
+
|
|
267
|
+
let value;
|
|
268
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
269
|
+
try {
|
|
270
|
+
value = await new Promise(resolve => {
|
|
271
|
+
rl.question(` Enter ${meta.label}: `, answer => resolve((answer || '').trim()));
|
|
272
|
+
});
|
|
273
|
+
} finally {
|
|
274
|
+
rl.close();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!value) return null;
|
|
278
|
+
|
|
279
|
+
// Ask if they want to save globally
|
|
280
|
+
let save;
|
|
281
|
+
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
282
|
+
try {
|
|
283
|
+
save = await new Promise(resolve => {
|
|
284
|
+
rl2.question(' Save to global config for future use? (Y/n): ', answer => {
|
|
285
|
+
const a = (answer || '').trim().toLowerCase();
|
|
286
|
+
resolve(a === '' || a === 'y' || a === 'yes');
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
} finally {
|
|
290
|
+
rl2.close();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (save) {
|
|
294
|
+
saveGlobalConfig({ [key]: value });
|
|
295
|
+
console.log(` ✓ Saved to ${CONFIG_FILE}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Inject into current process
|
|
299
|
+
process.env[key] = value;
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── Exports ───────────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
module.exports = {
|
|
306
|
+
CONFIG_FILE,
|
|
307
|
+
CONFIG_KEYS,
|
|
308
|
+
loadGlobalConfig,
|
|
309
|
+
saveGlobalConfig,
|
|
310
|
+
clearGlobalConfig,
|
|
311
|
+
injectGlobalConfig,
|
|
312
|
+
interactiveSetup,
|
|
313
|
+
promptForKey,
|
|
314
|
+
maskSecret,
|
|
315
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline Health Dashboard — generates a comprehensive quality report
|
|
3
|
+
* after all processing is complete.
|
|
4
|
+
*
|
|
5
|
+
* Reports on:
|
|
6
|
+
* - Parse success rates
|
|
7
|
+
* - Quality scores per segment
|
|
8
|
+
* - Extraction density (items per segment)
|
|
9
|
+
* - Data coverage analysis
|
|
10
|
+
* - Retry statistics
|
|
11
|
+
* - Token efficiency metrics
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
// ======================== HEALTH REPORT ========================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build a comprehensive health report from pipeline execution data.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} params
|
|
22
|
+
* @param {Array} params.segmentReports - Array of { segmentName, qualityReport, retried, retryImproved }
|
|
23
|
+
* @param {Array} params.allSegmentAnalyses - All final segment analyses
|
|
24
|
+
* @param {object} params.costSummary - From CostTracker.getSummary()
|
|
25
|
+
* @param {object} [params.compilationQuality] - Quality report for the compilation step
|
|
26
|
+
* @param {number} params.totalDurationMs - Wall-clock duration of pipeline
|
|
27
|
+
* @returns {object} Health report
|
|
28
|
+
*/
|
|
29
|
+
function buildHealthReport(params) {
|
|
30
|
+
const {
|
|
31
|
+
segmentReports = [],
|
|
32
|
+
allSegmentAnalyses = [],
|
|
33
|
+
costSummary = {},
|
|
34
|
+
compilationQuality = null,
|
|
35
|
+
totalDurationMs = 0,
|
|
36
|
+
} = params;
|
|
37
|
+
|
|
38
|
+
// Parse success rate
|
|
39
|
+
const totalSegments = segmentReports.length;
|
|
40
|
+
const parsed = segmentReports.filter(r => r.qualityReport?.grade !== 'FAIL').length;
|
|
41
|
+
const parseRate = totalSegments > 0 ? (parsed / totalSegments * 100).toFixed(1) : 0;
|
|
42
|
+
|
|
43
|
+
// Quality score distribution
|
|
44
|
+
const scores = segmentReports.map(r => r.qualityReport?.score || 0);
|
|
45
|
+
const avgScore = scores.length > 0 ? (scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(1) : 0;
|
|
46
|
+
const minScore = scores.length > 0 ? Math.min(...scores) : 0;
|
|
47
|
+
const maxScore = scores.length > 0 ? Math.max(...scores) : 0;
|
|
48
|
+
|
|
49
|
+
// Grade distribution
|
|
50
|
+
const grades = { PASS: 0, WARN: 0, FAIL: 0 };
|
|
51
|
+
for (const r of segmentReports) {
|
|
52
|
+
const g = r.qualityReport?.grade || 'FAIL';
|
|
53
|
+
grades[g] = (grades[g] || 0) + 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Extraction density
|
|
57
|
+
let totalTickets = 0, totalCrs = 0, totalActions = 0, totalBlockers = 0, totalScopes = 0;
|
|
58
|
+
const perSegment = [];
|
|
59
|
+
|
|
60
|
+
for (const analysis of allSegmentAnalyses) {
|
|
61
|
+
const tickets = analysis.tickets?.length || 0;
|
|
62
|
+
const crs = analysis.change_requests?.length || 0;
|
|
63
|
+
const actions = analysis.action_items?.length || 0;
|
|
64
|
+
const blockers = analysis.blockers?.length || 0;
|
|
65
|
+
const scopes = analysis.scope_changes?.length || 0;
|
|
66
|
+
|
|
67
|
+
totalTickets += tickets;
|
|
68
|
+
totalCrs += crs;
|
|
69
|
+
totalActions += actions;
|
|
70
|
+
totalBlockers += blockers;
|
|
71
|
+
totalScopes += scopes;
|
|
72
|
+
|
|
73
|
+
perSegment.push({ tickets, crs, actions, blockers, scopes });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Retry stats
|
|
77
|
+
const retriedCount = segmentReports.filter(r => r.retried).length;
|
|
78
|
+
const retryImprovedCount = segmentReports.filter(r => r.retryImproved).length;
|
|
79
|
+
|
|
80
|
+
// Token efficiency
|
|
81
|
+
const tokensPerItem = costSummary.totalTokens && (totalTickets + totalCrs + totalActions) > 0
|
|
82
|
+
? Math.round(costSummary.totalTokens / (totalTickets + totalCrs + totalActions))
|
|
83
|
+
: 0;
|
|
84
|
+
|
|
85
|
+
// All issues across segments
|
|
86
|
+
const allIssues = [];
|
|
87
|
+
for (const r of segmentReports) {
|
|
88
|
+
for (const issue of (r.qualityReport?.issues || [])) {
|
|
89
|
+
allIssues.push({ segment: r.segmentName, issue });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
summary: {
|
|
95
|
+
totalSegments,
|
|
96
|
+
parseSuccessRate: parseFloat(parseRate),
|
|
97
|
+
avgQualityScore: parseFloat(avgScore),
|
|
98
|
+
minQualityScore: minScore,
|
|
99
|
+
maxQualityScore: maxScore,
|
|
100
|
+
grades,
|
|
101
|
+
},
|
|
102
|
+
extraction: {
|
|
103
|
+
totalTickets,
|
|
104
|
+
totalChangeRequests: totalCrs,
|
|
105
|
+
totalActionItems: totalActions,
|
|
106
|
+
totalBlockers,
|
|
107
|
+
totalScopeChanges: totalScopes,
|
|
108
|
+
totalItems: totalTickets + totalCrs + totalActions + totalBlockers + totalScopes,
|
|
109
|
+
perSegment,
|
|
110
|
+
},
|
|
111
|
+
retry: {
|
|
112
|
+
segmentsRetried: retriedCount,
|
|
113
|
+
retriesImproved: retryImprovedCount,
|
|
114
|
+
},
|
|
115
|
+
efficiency: {
|
|
116
|
+
tokensPerExtractedItem: tokensPerItem,
|
|
117
|
+
totalTokens: costSummary.totalTokens || 0,
|
|
118
|
+
totalCost: costSummary.totalCost || 0,
|
|
119
|
+
aiTimeMs: costSummary.totalDurationMs || 0,
|
|
120
|
+
wallClockMs: totalDurationMs,
|
|
121
|
+
},
|
|
122
|
+
compilation: compilationQuality ? {
|
|
123
|
+
score: compilationQuality.score,
|
|
124
|
+
grade: compilationQuality.grade,
|
|
125
|
+
issues: compilationQuality.issues,
|
|
126
|
+
} : null,
|
|
127
|
+
issues: allIssues,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Print the health dashboard to console.
|
|
133
|
+
* @param {object} report - From buildHealthReport
|
|
134
|
+
*/
|
|
135
|
+
function printHealthDashboard(report) {
|
|
136
|
+
const { summary: s, extraction: e, retry: r, efficiency: eff, compilation: c } = report;
|
|
137
|
+
|
|
138
|
+
console.log('');
|
|
139
|
+
console.log('╔══════════════════════════════════════════════╗');
|
|
140
|
+
console.log('║ PIPELINE HEALTH DASHBOARD ║');
|
|
141
|
+
console.log('╚══════════════════════════════════════════════╝');
|
|
142
|
+
console.log('');
|
|
143
|
+
|
|
144
|
+
// Quality overview
|
|
145
|
+
console.log(' Quality Scores:');
|
|
146
|
+
console.log(` Average : ${s.avgQualityScore}/100`);
|
|
147
|
+
console.log(` Range : ${s.minQualityScore}–${s.maxQualityScore}`);
|
|
148
|
+
console.log(` Grades : ✓ ${s.grades.PASS} PASS | ⚠ ${s.grades.WARN} WARN | ✗ ${s.grades.FAIL} FAIL`);
|
|
149
|
+
console.log(` Parse : ${s.parseSuccessRate}% success rate`);
|
|
150
|
+
console.log('');
|
|
151
|
+
|
|
152
|
+
// Extraction density
|
|
153
|
+
console.log(' Extraction Summary:');
|
|
154
|
+
console.log(` Tickets : ${e.totalTickets}`);
|
|
155
|
+
console.log(` Change Requests : ${e.totalChangeRequests}`);
|
|
156
|
+
console.log(` Action Items : ${e.totalActionItems}`);
|
|
157
|
+
console.log(` Blockers : ${e.totalBlockers}`);
|
|
158
|
+
console.log(` Scope Changes : ${e.totalScopeChanges}`);
|
|
159
|
+
console.log(` Total items : ${e.totalItems} across ${s.totalSegments} segment(s)`);
|
|
160
|
+
console.log('');
|
|
161
|
+
|
|
162
|
+
// Per-segment density bars
|
|
163
|
+
if (e.perSegment.length > 0) {
|
|
164
|
+
console.log(' Per-Segment Density:');
|
|
165
|
+
e.perSegment.forEach((seg, i) => {
|
|
166
|
+
const total = seg.tickets + seg.crs + seg.actions + seg.blockers + seg.scopes;
|
|
167
|
+
const bar = '█'.repeat(Math.min(total, 30)) + (total > 30 ? '…' : '');
|
|
168
|
+
console.log(` Seg ${i + 1}: ${bar} (${total} items)`);
|
|
169
|
+
});
|
|
170
|
+
console.log('');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Retry stats
|
|
174
|
+
if (r.segmentsRetried > 0) {
|
|
175
|
+
console.log(' Retry Statistics:');
|
|
176
|
+
console.log(` Segments retried : ${r.segmentsRetried}`);
|
|
177
|
+
console.log(` Retries improved : ${r.retriesImproved}`);
|
|
178
|
+
console.log('');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Efficiency
|
|
182
|
+
console.log(' Efficiency:');
|
|
183
|
+
console.log(` Tokens/item : ${eff.tokensPerExtractedItem.toLocaleString()}`);
|
|
184
|
+
console.log(` AI time : ${(eff.aiTimeMs / 1000).toFixed(1)}s`);
|
|
185
|
+
console.log(` Wall clock : ${(eff.wallClockMs / 1000).toFixed(1)}s`);
|
|
186
|
+
console.log(` Cost : $${eff.totalCost.toFixed(4)}`);
|
|
187
|
+
|
|
188
|
+
// Compilation quality
|
|
189
|
+
if (c) {
|
|
190
|
+
console.log('');
|
|
191
|
+
console.log(' Compilation:');
|
|
192
|
+
console.log(` Score : ${c.score}/100 (${c.grade})`);
|
|
193
|
+
if (c.issues.length > 0) {
|
|
194
|
+
console.log(` Issues: ${c.issues.length}`);
|
|
195
|
+
c.issues.slice(0, 3).forEach(issue => console.log(` • ${issue}`));
|
|
196
|
+
if (c.issues.length > 3) console.log(` ... +${c.issues.length - 3} more`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Top issues
|
|
201
|
+
const criticalIssues = report.issues.filter(i =>
|
|
202
|
+
i.issue.includes('FAIL') || i.issue.includes('Missing required') || i.issue.includes('parse failed')
|
|
203
|
+
);
|
|
204
|
+
if (criticalIssues.length > 0) {
|
|
205
|
+
console.log('');
|
|
206
|
+
console.log(' ⚠ Critical Issues:');
|
|
207
|
+
criticalIssues.slice(0, 5).forEach(i => console.log(` ${i.segment}: ${i.issue}`));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log('');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
buildHealthReport,
|
|
215
|
+
printHealthDashboard,
|
|
216
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI config flag injection — maps --flag values to process.env before
|
|
3
|
+
* any module touches config.js / dotenv.
|
|
4
|
+
*
|
|
5
|
+
* Shared between bin/taskex.js and process_and_upload.js to avoid
|
|
6
|
+
* duplicated flag-parsing logic.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
/** Map of CLI flag names → environment variable names */
|
|
12
|
+
const CONFIG_FLAG_MAP = {
|
|
13
|
+
'gemini-key': 'GEMINI_API_KEY',
|
|
14
|
+
'firebase-key': 'FIREBASE_API_KEY',
|
|
15
|
+
'firebase-project': 'FIREBASE_PROJECT_ID',
|
|
16
|
+
'firebase-bucket': 'FIREBASE_STORAGE_BUCKET',
|
|
17
|
+
'firebase-domain': 'FIREBASE_AUTH_DOMAIN',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Scan process.argv for config flags and inject their values into process.env.
|
|
22
|
+
* Must be called BEFORE any require() that touches config.js / dotenv.
|
|
23
|
+
*
|
|
24
|
+
* Supports both `--key value` and `--key=value` forms.
|
|
25
|
+
*
|
|
26
|
+
* @param {string[]} [argv] - Arguments (defaults to process.argv.slice(2))
|
|
27
|
+
* @returns {string[]} List of env var names that were injected
|
|
28
|
+
*/
|
|
29
|
+
function injectCliFlags(argv) {
|
|
30
|
+
if (!argv) argv = process.argv.slice(2);
|
|
31
|
+
const injected = [];
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < argv.length; i++) {
|
|
34
|
+
if (!argv[i].startsWith('--')) continue;
|
|
35
|
+
|
|
36
|
+
const eqIdx = argv[i].indexOf('=');
|
|
37
|
+
let key, val;
|
|
38
|
+
|
|
39
|
+
if (eqIdx !== -1) {
|
|
40
|
+
key = argv[i].slice(2, eqIdx);
|
|
41
|
+
val = argv[i].slice(eqIdx + 1);
|
|
42
|
+
} else {
|
|
43
|
+
key = argv[i].slice(2);
|
|
44
|
+
if (CONFIG_FLAG_MAP[key] && i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
|
|
45
|
+
val = argv[i + 1];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (key && val && CONFIG_FLAG_MAP[key]) {
|
|
50
|
+
process.env[CONFIG_FLAG_MAP[key]] = val;
|
|
51
|
+
injected.push(CONFIG_FLAG_MAP[key]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return injected;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { injectCliFlags, CONFIG_FLAG_MAP };
|