task-summary-extractor 9.2.0 → 9.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "task-summary-extractor",
3
- "version": "9.2.0",
3
+ "version": "9.2.2",
4
4
  "description": "AI-powered meeting analysis & document generation CLI — video + document processing, deep dive docs, dynamic mode, interactive CLI with model selection, confidence scoring, learning loop, git progress tracking",
5
5
  "main": "process_and_upload.js",
6
6
  "bin": {
package/src/logger.js CHANGED
@@ -321,16 +321,17 @@ class Logger {
321
321
  /** Flush buffers and close the logger. Safe to call multiple times. */
322
322
  close() {
323
323
  if (this.closed) return;
324
- this.closed = true;
325
324
  clearInterval(this._flushInterval);
326
325
  this.unpatchConsole();
327
326
 
328
- // End active phase if any
327
+ // End active phase if any (must happen BEFORE setting closed flag
328
+ // so _writeStructured inside phaseEnd is not blocked)
329
329
  if (this._activePhase) {
330
330
  this.phaseEnd();
331
331
  }
332
332
 
333
- // Write footer
333
+ // Write footer and session_end BEFORE setting closed flag
334
+ // so _writeStructured is not blocked by the guard
334
335
  const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
335
336
  const footer = `\n=== CLOSED | elapsed: ${elapsed}s | ${new Date().toISOString()} ===\n`;
336
337
  this._detailedBuffer.push(footer);
@@ -342,6 +343,8 @@ class Logger {
342
343
  timestamp: new Date().toISOString(),
343
344
  level: 'info',
344
345
  });
346
+
347
+ this.closed = true;
345
348
  this._flush(true); // sync flush on close to ensure data is written before process exits
346
349
  }
347
350
 
@@ -8,7 +8,7 @@ const { compileFinalResult } = require('../services/gemini');
8
8
 
9
9
  // --- Utils ---
10
10
  const { calculateCompilationBudget } = require('../utils/adaptive-budget');
11
- const { validateAnalysis, formatSchemaLine } = require('../utils/schema-validator');
11
+ const { validateAnalysis, formatSchemaLine, normalizeAnalysis } = require('../utils/schema-validator');
12
12
  const { c } = require('../utils/colors');
13
13
 
14
14
  // --- Shared state ---
@@ -41,7 +41,7 @@ async function phaseCompile(ctx, allSegmentAnalyses) {
41
41
  { thinkingBudget: compBudget.budget }
42
42
  );
43
43
 
44
- compiledAnalysis = compilationResult.compiled;
44
+ compiledAnalysis = normalizeAnalysis(compilationResult.compiled);
45
45
  compilationRun = compilationResult.run;
46
46
 
47
47
  // Track compilation cost
@@ -17,7 +17,7 @@ const { fmtDuration, fmtBytes } = require('../utils/format');
17
17
  const { promptUser } = require('../utils/cli');
18
18
  const { parallelMap } = require('../utils/retry');
19
19
  const { assessQuality, formatQualityLine, getConfidenceStats, THRESHOLDS } = require('../utils/quality-gate');
20
- const { validateAnalysis, formatSchemaLine, schemaScore } = require('../utils/schema-validator');
20
+ const { validateAnalysis, formatSchemaLine, schemaScore, normalizeAnalysis } = require('../utils/schema-validator');
21
21
  const { calculateThinkingBudget } = require('../utils/adaptive-budget');
22
22
  const { detectBoundaryContext, sliceVttForSegment } = require('../utils/context-manager');
23
23
 
@@ -264,7 +264,7 @@ async function phaseProcessVideo(ctx, videoPath, videoIndex) {
264
264
  try {
265
265
  const existingRun = JSON.parse(fs.readFileSync(latestRunPath, 'utf8'));
266
266
  geminiRunFile = path.relative(PROJECT_ROOT, path.join(geminiRunsDir, latestRunFile));
267
- analysis = existingRun.output.parsed || { rawResponse: existingRun.output.raw };
267
+ analysis = normalizeAnalysis(existingRun.output.parsed || { rawResponse: existingRun.output.raw });
268
268
  analysis._geminiMeta = {
269
269
  model: existingRun.run.model,
270
270
  processedAt: existingRun.run.timestamp,
@@ -396,7 +396,7 @@ async function phaseProcessVideo(ctx, videoPath, videoIndex) {
396
396
  geminiFileName = geminiRun.input.videoFile.geminiFileName || null;
397
397
  const usedExternalUrl = geminiRun.input.videoFile.usedExternalUrl || false;
398
398
 
399
- analysis = geminiRun.output.parsed || { rawResponse: geminiRun.output.raw };
399
+ analysis = normalizeAnalysis(geminiRun.output.parsed || { rawResponse: geminiRun.output.raw });
400
400
  analysis._geminiMeta = {
401
401
  model: geminiRun.run.model,
402
402
  processedAt: geminiRun.run.timestamp,
@@ -474,7 +474,7 @@ async function phaseProcessVideo(ctx, videoPath, videoIndex) {
474
474
  const retryRunFilePath = path.join(geminiRunsDir, retryRunFileName);
475
475
  fs.writeFileSync(retryRunFilePath, JSON.stringify(retryRun, null, 2), 'utf8');
476
476
 
477
- const retryAnalysis = retryRun.output.parsed || { rawResponse: retryRun.output.raw };
477
+ const retryAnalysis = normalizeAnalysis(retryRun.output.parsed || { rawResponse: retryRun.output.raw });
478
478
  const retryQuality = assessQuality(retryAnalysis, {
479
479
  parseSuccess: retryRun.output.parseSuccess,
480
480
  rawLength: (retryRun.output.raw || '').length,
@@ -492,7 +492,7 @@ async function phaseProcessVideo(ctx, videoPath, videoIndex) {
492
492
  // Use retry result if better
493
493
  if (retryQuality.score > qualityReport.score) {
494
494
  retryImproved = true;
495
- analysis = retryAnalysis;
495
+ analysis = normalizeAnalysis(retryAnalysis);
496
496
  analysis._geminiMeta = {
497
497
  model: retryRun.run.model,
498
498
  processedAt: retryRun.run.timestamp,
package/src/pipeline.js CHANGED
@@ -231,7 +231,7 @@ async function run() {
231
231
  async function runDocOnly(ctx) {
232
232
  // Lazy imports for doc-only mode
233
233
  const { compileFinalResult } = require('./services/gemini');
234
- const { validateAnalysis, formatSchemaLine } = require('./utils/schema-validator');
234
+ const { validateAnalysis, formatSchemaLine, normalizeAnalysis } = require('./utils/schema-validator');
235
235
  const { renderResultsMarkdown } = require('./renderers/markdown');
236
236
  const { renderResultsHtml } = require('./renderers/html');
237
237
  const { renderResultsPdf } = require('./renderers/pdf');
@@ -297,7 +297,7 @@ async function runDocOnly(ctx) {
297
297
  }
298
298
  );
299
299
 
300
- compiledAnalysis = compilationResult.compiled;
300
+ compiledAnalysis = normalizeAnalysis(compilationResult.compiled);
301
301
  compilationRun = compilationResult.run;
302
302
 
303
303
  if (compilationRun?.tokenUsage) {
@@ -5,7 +5,7 @@
5
5
  "description": "JSON Schema for a single segment's AI analysis output. Uses additionalProperties:true to allow extra fields without failing.",
6
6
  "type": "object",
7
7
  "additionalProperties": true,
8
- "required": ["tickets", "action_items", "change_requests", "summary"],
8
+ "required": ["summary"],
9
9
  "properties": {
10
10
  "tickets": {
11
11
  "type": "array",
@@ -195,6 +195,9 @@ function repairDoubledClosers(text) {
195
195
  * Returns parsed object or null.
196
196
  */
197
197
  function extractJson(rawText) {
198
+ // Guard: null/undefined input (e.g. Gemini returns no text)
199
+ if (!rawText || typeof rawText !== 'string') return null;
200
+
198
201
  // Strategy 1: Strip all markdown fences and try to parse
199
202
  const cleaned = rawText.replace(/```json\s*/gi, '').replace(/```\s*/g, '').trim();
200
203
  let parsed = tryParseWithSanitize(cleaned);
@@ -29,14 +29,16 @@ const THRESHOLDS = {
29
29
 
30
30
  // Required top-level fields in a valid analysis
31
31
  const REQUIRED_FIELDS = [
32
- 'tickets',
33
- 'action_items',
34
- 'change_requests',
35
32
  'summary',
36
33
  ];
37
34
 
38
35
  // Optional but valuable fields (boost score when present)
36
+ // tickets, action_items, change_requests are here because a segment may
37
+ // legitimately contain none of these (e.g. a segment that is only chit-chat).
39
38
  const VALUED_FIELDS = [
39
+ 'tickets',
40
+ 'action_items',
41
+ 'change_requests',
40
42
  'blockers',
41
43
  'scope_changes',
42
44
  'file_references',
@@ -70,11 +72,11 @@ function scoreStructure(analysis) {
70
72
  let bonus = 0;
71
73
  for (const field of VALUED_FIELDS) {
72
74
  if (analysis[field] !== undefined && analysis[field] !== null) {
73
- bonus += 3; // up to 12 bonus points
75
+ bonus += 2; // up to 14 bonus points (7 valued fields)
74
76
  }
75
77
  }
76
78
 
77
- const baseScore = (present / REQUIRED_FIELDS.length) * 80;
79
+ const baseScore = (present / REQUIRED_FIELDS.length) * 70;
78
80
  return { score: Math.min(100, baseScore + bonus), issues };
79
81
  }
80
82
 
@@ -335,7 +337,7 @@ function buildRetryHints(analysis, issues) {
335
337
  const hints = [];
336
338
 
337
339
  if (issues.some(i => i.includes('Missing required field'))) {
338
- hints.push('CRITICAL: Your previous response was missing required fields. You MUST include ALL of: tickets, action_items, change_requests, summary. Use empty arrays [] if no items exist.');
340
+ hints.push('CRITICAL: Your previous response was missing the required "summary" field. You MUST include a "summary" string. Include tickets, action_items, and change_requests arrays if relevant — omit them or use empty arrays [] if none exist in this segment.');
339
341
  }
340
342
 
341
343
  if (issues.some(i => i.includes('JSON parse failed'))) {
@@ -306,9 +306,38 @@ function formatSchemaLine(report) {
306
306
  return ` ${c.warn(`Schema: ${report.errorCount} error(s) — ${report.errors.slice(0, 2).map(e => e.message).join('; ')}`)}`;
307
307
  }
308
308
 
309
+ // ======================== POST-PARSE NORMALIZATION ========================
310
+
311
+ /** Array fields that should default to [] when missing from analysis output. */
312
+ const ARRAY_DEFAULTS = [
313
+ 'tickets', 'action_items', 'change_requests',
314
+ 'blockers', 'scope_changes', 'file_references',
315
+ ];
316
+
317
+ /**
318
+ * Normalize a parsed analysis object by filling in missing array fields
319
+ * with empty arrays. This prevents downstream code from crashing when
320
+ * a segment legitimately has no tickets/action_items/etc.
321
+ *
322
+ * Mutates `data` in-place and returns it for convenience.
323
+ *
324
+ * @param {object} data - Parsed analysis (may have missing fields)
325
+ * @returns {object} The same object with defaults applied
326
+ */
327
+ function normalizeAnalysis(data) {
328
+ if (!data || typeof data !== 'object') return data;
329
+ for (const field of ARRAY_DEFAULTS) {
330
+ if (data[field] === undefined || data[field] === null) {
331
+ data[field] = [];
332
+ }
333
+ }
334
+ return data;
335
+ }
336
+
309
337
  module.exports = {
310
338
  validateAnalysis,
311
339
  buildSchemaRetryHints,
312
340
  schemaScore,
313
341
  formatSchemaLine,
342
+ normalizeAnalysis,
314
343
  };