@juspay/neurolink 9.88.6 → 9.88.8

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.
@@ -337,6 +337,20 @@ function calculateDataQualityScore(columns, warnings, totalRows) {
337
337
  /**
338
338
  * Analyze all columns in parsed CSV data
339
339
  */
340
+ /**
341
+ * Confidence for a CSV processing result, weighted by data quality.
342
+ *
343
+ * The result used to report a static `confidence: 100` regardless of how clean
344
+ * the data was. This ties confidence to `dataQualityScore` so it actually
345
+ * varies with quality factors (ragged rows, high null rate, type
346
+ * inconsistency): a cleanly-parsed CSV stays at 100, a messy one is pulled
347
+ * halfway toward its quality score. It never drops below 50 for a CSV that
348
+ * parsed — the *format* is certain, only the *data* is imperfect.
349
+ */
350
+ function csvResultConfidence(dataQualityScore) {
351
+ const clamped = Math.max(0, Math.min(100, dataQualityScore));
352
+ return Math.round((100 + clamped) / 2);
353
+ }
340
354
  function analyzeColumns(rows) {
341
355
  if (rows.length === 0) {
342
356
  return {
@@ -425,6 +439,33 @@ function detectHasHeaders(headerValues, dataRows) {
425
439
  * - Lines with significantly different delimiter count than line 2
426
440
  * - Lines that don't match CSV structure of subsequent lines
427
441
  */
442
+ /** Strip a leading UTF-8 BOM (U+FEFF) if present. */
443
+ function stripBom(text) {
444
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
445
+ }
446
+ /**
447
+ * Count commas that fall OUTSIDE double-quoted fields (RFC 4180, escaped-quote
448
+ * aware). A quoted comma is field content, not a column separator, so counting
449
+ * naively would misclassify a quoted-comma header line.
450
+ */
451
+ function countUnquotedCommas(line) {
452
+ let count = 0;
453
+ let inQuotes = false;
454
+ for (let i = 0; i < line.length; i++) {
455
+ const ch = line[i];
456
+ if (ch === '"') {
457
+ if (inQuotes && line[i + 1] === '"') {
458
+ i++;
459
+ continue;
460
+ }
461
+ inQuotes = !inQuotes;
462
+ }
463
+ else if (ch === "," && !inQuotes) {
464
+ count++;
465
+ }
466
+ }
467
+ return count;
468
+ }
428
469
  function isMetadataLine(lines) {
429
470
  if (!lines[0] || lines.length < 2) {
430
471
  return false;
@@ -434,8 +475,8 @@ function isMetadataLine(lines) {
434
475
  if (firstLine.match(/^sep=/i)) {
435
476
  return true;
436
477
  }
437
- const firstCommaCount = (firstLine.match(/,/g) || []).length;
438
- const secondCommaCount = (secondLine.match(/,/g) || []).length;
478
+ const firstCommaCount = countUnquotedCommas(firstLine);
479
+ const secondCommaCount = countUnquotedCommas(secondLine);
439
480
  if (firstCommaCount === 0 && secondCommaCount > 0) {
440
481
  return true;
441
482
  }
@@ -574,7 +615,7 @@ export class CSVProcessor {
574
615
  content: limitedCSV,
575
616
  mimeType: "text/csv",
576
617
  metadata: {
577
- confidence: 100,
618
+ confidence: csvResultConfidence(dataQualityScore),
578
619
  size: content.length,
579
620
  rowCount,
580
621
  totalLines: limitedLines.length,
@@ -648,7 +689,7 @@ export class CSVProcessor {
648
689
  content: formatted,
649
690
  mimeType: "text/csv",
650
691
  metadata: {
651
- confidence: 100,
692
+ confidence: csvResultConfidence(dataQualityScore),
652
693
  size: content.length,
653
694
  rowCount,
654
695
  columnCount,
@@ -676,6 +717,9 @@ export class CSVProcessor {
676
717
  * @returns Array of row objects
677
718
  */
678
719
  static async parseCSVFile(filePath, maxRows = 1000) {
720
+ if (typeof filePath !== "string" || filePath.trim().length === 0) {
721
+ throw new Error("CSVProcessor.parseCSVFile: filePath must be a non-empty string");
722
+ }
679
723
  const clampedMaxRows = Math.max(1, Math.min(10000, maxRows));
680
724
  const fs = await import("fs");
681
725
  logger.debug("[CSVProcessor] Starting file parsing", {
@@ -701,6 +745,8 @@ export class CSVProcessor {
701
745
  }
702
746
  });
703
747
  lineReader.on("end", () => resolve());
748
+ // Metadata sniffing is best-effort — a read error here must not crash.
749
+ lineReader.on("error", () => resolve());
704
750
  });
705
751
  await fileHandle.close();
706
752
  const hasMetadataLine = isMetadataLine(firstLines);
@@ -718,6 +764,12 @@ export class CSVProcessor {
718
764
  source.destroy();
719
765
  parser.destroy();
720
766
  };
767
+ // .pipe() does not forward source errors (disk read/permission failures)
768
+ // to the parser, so listen on the source directly or it throws unhandled.
769
+ source.on("error", (error) => {
770
+ abort();
771
+ reject(new Error(`[CSVProcessor] CSV file read failed after ${count} row(s) (${filePath}): ${error.message}`));
772
+ });
721
773
  source
722
774
  .pipe(parser)
723
775
  .on("data", (row) => {
@@ -739,7 +791,7 @@ export class CSVProcessor {
739
791
  })
740
792
  .on("error", (error) => {
741
793
  logger.error("[CSVProcessor] File parsing failed:", error);
742
- reject(error);
794
+ reject(new Error(`[CSVProcessor] CSV parsing failed after ${count} row(s): ${error.message}`));
743
795
  });
744
796
  });
745
797
  }
@@ -752,13 +804,19 @@ export class CSVProcessor {
752
804
  * @returns Array of row objects
753
805
  */
754
806
  static async parseCSVString(csvString, maxRows = 1000) {
807
+ if (typeof csvString !== "string" || csvString.trim().length === 0) {
808
+ throw new Error("CSVProcessor.parseCSVString: csvString must be a non-empty string");
809
+ }
810
+ // Strip a leading UTF-8 BOM (common in Excel exports) so it does not glue
811
+ // onto the first column name and break downstream key lookups.
812
+ const normalized = stripBom(csvString);
755
813
  const clampedMaxRows = Math.max(1, Math.min(10000, maxRows));
756
814
  logger.debug("[CSVProcessor] Starting string parsing", {
757
- inputLength: csvString.length,
815
+ inputLength: normalized.length,
758
816
  maxRows: clampedMaxRows,
759
817
  });
760
818
  // Detect and skip metadata line
761
- const lines = splitCsvLines(csvString);
819
+ const lines = splitCsvLines(normalized);
762
820
  const hasMetadataLine = isMetadataLine(lines);
763
821
  const csvData = hasMetadataLine
764
822
  ? lines.slice(1).join("\n")
@@ -775,6 +833,13 @@ export class CSVProcessor {
775
833
  source.destroy();
776
834
  parser.destroy();
777
835
  };
836
+ // .pipe() does not forward source-stream errors to the destination, so a
837
+ // source failure must be listened for directly or it throws as an
838
+ // unhandled EventEmitter error and can crash the process.
839
+ source.on("error", (error) => {
840
+ abort();
841
+ reject(new Error(`[CSVProcessor] CSV source stream failed after ${count} row(s): ${error.message}`));
842
+ });
778
843
  source
779
844
  .pipe(parser)
780
845
  .on("data", (row) => {
@@ -792,7 +857,7 @@ export class CSVProcessor {
792
857
  })
793
858
  .on("error", (error) => {
794
859
  logger.error("[CSVProcessor] Parsing failed:", error);
795
- reject(error);
860
+ reject(new Error(`[CSVProcessor] CSV parsing failed after ${count} row(s): ${error.message}`));
796
861
  });
797
862
  });
798
863
  }
@@ -2166,13 +2166,38 @@ class ContentHeuristicStrategy {
2166
2166
  const hasUniformLengths = stdDev / avgLength < 0.75;
2167
2167
  return hasReasonableLengths && noBinaryChars && hasUniformLengths;
2168
2168
  }
2169
- // Count delimiters per line and check consistency
2170
- const delimRegex = delimiter === "|" ? /\|/g : new RegExp(delimiter, "g");
2171
- const counts = lines.map((line) => (line.match(delimRegex) || []).length);
2169
+ // Count delimiters per line and check consistency. Delimiters inside a
2170
+ // double-quoted field are field content, not column separators (RFC 4180)
2171
+ // a naive count inflates rows with quoted delimiters (e.g. `"Smith, John"`),
2172
+ // which used to make consistency collapse and reject a valid CSV.
2173
+ const counts = lines.map((line) => ContentHeuristicStrategy.countDelimitersOutsideQuotes(line, delimiter));
2172
2174
  const firstCount = counts[0];
2173
2175
  const consistentLines = counts.filter((c) => c === firstCount).length;
2174
2176
  return consistentLines / lines.length >= 0.8;
2175
2177
  }
2178
+ /**
2179
+ * Count occurrences of `delimiter` in `line` that fall OUTSIDE double-quoted
2180
+ * fields, honoring RFC-4180 escaped quotes (`""`). Used by CSV detection so a
2181
+ * delimiter embedded in a quoted value is not mistaken for a column break.
2182
+ */
2183
+ static countDelimitersOutsideQuotes(line, delimiter) {
2184
+ let count = 0;
2185
+ let inQuotes = false;
2186
+ for (let i = 0; i < line.length; i++) {
2187
+ const ch = line[i];
2188
+ if (ch === '"') {
2189
+ if (inQuotes && line[i + 1] === '"') {
2190
+ i++; // escaped quote inside a quoted field — skip the pair
2191
+ continue;
2192
+ }
2193
+ inQuotes = !inQuotes;
2194
+ }
2195
+ else if (ch === delimiter && !inQuotes) {
2196
+ count++;
2197
+ }
2198
+ }
2199
+ return count;
2200
+ }
2176
2201
  looksLikeJSON(text) {
2177
2202
  // hasJsonMarkers now does full validation including JSON.parse
2178
2203
  return hasJsonMarkers(text);
@@ -337,6 +337,20 @@ function calculateDataQualityScore(columns, warnings, totalRows) {
337
337
  /**
338
338
  * Analyze all columns in parsed CSV data
339
339
  */
340
+ /**
341
+ * Confidence for a CSV processing result, weighted by data quality.
342
+ *
343
+ * The result used to report a static `confidence: 100` regardless of how clean
344
+ * the data was. This ties confidence to `dataQualityScore` so it actually
345
+ * varies with quality factors (ragged rows, high null rate, type
346
+ * inconsistency): a cleanly-parsed CSV stays at 100, a messy one is pulled
347
+ * halfway toward its quality score. It never drops below 50 for a CSV that
348
+ * parsed — the *format* is certain, only the *data* is imperfect.
349
+ */
350
+ function csvResultConfidence(dataQualityScore) {
351
+ const clamped = Math.max(0, Math.min(100, dataQualityScore));
352
+ return Math.round((100 + clamped) / 2);
353
+ }
340
354
  function analyzeColumns(rows) {
341
355
  if (rows.length === 0) {
342
356
  return {
@@ -425,6 +439,33 @@ function detectHasHeaders(headerValues, dataRows) {
425
439
  * - Lines with significantly different delimiter count than line 2
426
440
  * - Lines that don't match CSV structure of subsequent lines
427
441
  */
442
+ /** Strip a leading UTF-8 BOM (U+FEFF) if present. */
443
+ function stripBom(text) {
444
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
445
+ }
446
+ /**
447
+ * Count commas that fall OUTSIDE double-quoted fields (RFC 4180, escaped-quote
448
+ * aware). A quoted comma is field content, not a column separator, so counting
449
+ * naively would misclassify a quoted-comma header line.
450
+ */
451
+ function countUnquotedCommas(line) {
452
+ let count = 0;
453
+ let inQuotes = false;
454
+ for (let i = 0; i < line.length; i++) {
455
+ const ch = line[i];
456
+ if (ch === '"') {
457
+ if (inQuotes && line[i + 1] === '"') {
458
+ i++;
459
+ continue;
460
+ }
461
+ inQuotes = !inQuotes;
462
+ }
463
+ else if (ch === "," && !inQuotes) {
464
+ count++;
465
+ }
466
+ }
467
+ return count;
468
+ }
428
469
  function isMetadataLine(lines) {
429
470
  if (!lines[0] || lines.length < 2) {
430
471
  return false;
@@ -434,8 +475,8 @@ function isMetadataLine(lines) {
434
475
  if (firstLine.match(/^sep=/i)) {
435
476
  return true;
436
477
  }
437
- const firstCommaCount = (firstLine.match(/,/g) || []).length;
438
- const secondCommaCount = (secondLine.match(/,/g) || []).length;
478
+ const firstCommaCount = countUnquotedCommas(firstLine);
479
+ const secondCommaCount = countUnquotedCommas(secondLine);
439
480
  if (firstCommaCount === 0 && secondCommaCount > 0) {
440
481
  return true;
441
482
  }
@@ -574,7 +615,7 @@ export class CSVProcessor {
574
615
  content: limitedCSV,
575
616
  mimeType: "text/csv",
576
617
  metadata: {
577
- confidence: 100,
618
+ confidence: csvResultConfidence(dataQualityScore),
578
619
  size: content.length,
579
620
  rowCount,
580
621
  totalLines: limitedLines.length,
@@ -648,7 +689,7 @@ export class CSVProcessor {
648
689
  content: formatted,
649
690
  mimeType: "text/csv",
650
691
  metadata: {
651
- confidence: 100,
692
+ confidence: csvResultConfidence(dataQualityScore),
652
693
  size: content.length,
653
694
  rowCount,
654
695
  columnCount,
@@ -676,6 +717,9 @@ export class CSVProcessor {
676
717
  * @returns Array of row objects
677
718
  */
678
719
  static async parseCSVFile(filePath, maxRows = 1000) {
720
+ if (typeof filePath !== "string" || filePath.trim().length === 0) {
721
+ throw new Error("CSVProcessor.parseCSVFile: filePath must be a non-empty string");
722
+ }
679
723
  const clampedMaxRows = Math.max(1, Math.min(10000, maxRows));
680
724
  const fs = await import("fs");
681
725
  logger.debug("[CSVProcessor] Starting file parsing", {
@@ -701,6 +745,8 @@ export class CSVProcessor {
701
745
  }
702
746
  });
703
747
  lineReader.on("end", () => resolve());
748
+ // Metadata sniffing is best-effort — a read error here must not crash.
749
+ lineReader.on("error", () => resolve());
704
750
  });
705
751
  await fileHandle.close();
706
752
  const hasMetadataLine = isMetadataLine(firstLines);
@@ -718,6 +764,12 @@ export class CSVProcessor {
718
764
  source.destroy();
719
765
  parser.destroy();
720
766
  };
767
+ // .pipe() does not forward source errors (disk read/permission failures)
768
+ // to the parser, so listen on the source directly or it throws unhandled.
769
+ source.on("error", (error) => {
770
+ abort();
771
+ reject(new Error(`[CSVProcessor] CSV file read failed after ${count} row(s) (${filePath}): ${error.message}`));
772
+ });
721
773
  source
722
774
  .pipe(parser)
723
775
  .on("data", (row) => {
@@ -739,7 +791,7 @@ export class CSVProcessor {
739
791
  })
740
792
  .on("error", (error) => {
741
793
  logger.error("[CSVProcessor] File parsing failed:", error);
742
- reject(error);
794
+ reject(new Error(`[CSVProcessor] CSV parsing failed after ${count} row(s): ${error.message}`));
743
795
  });
744
796
  });
745
797
  }
@@ -752,13 +804,19 @@ export class CSVProcessor {
752
804
  * @returns Array of row objects
753
805
  */
754
806
  static async parseCSVString(csvString, maxRows = 1000) {
807
+ if (typeof csvString !== "string" || csvString.trim().length === 0) {
808
+ throw new Error("CSVProcessor.parseCSVString: csvString must be a non-empty string");
809
+ }
810
+ // Strip a leading UTF-8 BOM (common in Excel exports) so it does not glue
811
+ // onto the first column name and break downstream key lookups.
812
+ const normalized = stripBom(csvString);
755
813
  const clampedMaxRows = Math.max(1, Math.min(10000, maxRows));
756
814
  logger.debug("[CSVProcessor] Starting string parsing", {
757
- inputLength: csvString.length,
815
+ inputLength: normalized.length,
758
816
  maxRows: clampedMaxRows,
759
817
  });
760
818
  // Detect and skip metadata line
761
- const lines = splitCsvLines(csvString);
819
+ const lines = splitCsvLines(normalized);
762
820
  const hasMetadataLine = isMetadataLine(lines);
763
821
  const csvData = hasMetadataLine
764
822
  ? lines.slice(1).join("\n")
@@ -775,6 +833,13 @@ export class CSVProcessor {
775
833
  source.destroy();
776
834
  parser.destroy();
777
835
  };
836
+ // .pipe() does not forward source-stream errors to the destination, so a
837
+ // source failure must be listened for directly or it throws as an
838
+ // unhandled EventEmitter error and can crash the process.
839
+ source.on("error", (error) => {
840
+ abort();
841
+ reject(new Error(`[CSVProcessor] CSV source stream failed after ${count} row(s): ${error.message}`));
842
+ });
778
843
  source
779
844
  .pipe(parser)
780
845
  .on("data", (row) => {
@@ -792,7 +857,7 @@ export class CSVProcessor {
792
857
  })
793
858
  .on("error", (error) => {
794
859
  logger.error("[CSVProcessor] Parsing failed:", error);
795
- reject(error);
860
+ reject(new Error(`[CSVProcessor] CSV parsing failed after ${count} row(s): ${error.message}`));
796
861
  });
797
862
  });
798
863
  }
@@ -2166,13 +2166,38 @@ class ContentHeuristicStrategy {
2166
2166
  const hasUniformLengths = stdDev / avgLength < 0.75;
2167
2167
  return hasReasonableLengths && noBinaryChars && hasUniformLengths;
2168
2168
  }
2169
- // Count delimiters per line and check consistency
2170
- const delimRegex = delimiter === "|" ? /\|/g : new RegExp(delimiter, "g");
2171
- const counts = lines.map((line) => (line.match(delimRegex) || []).length);
2169
+ // Count delimiters per line and check consistency. Delimiters inside a
2170
+ // double-quoted field are field content, not column separators (RFC 4180)
2171
+ // a naive count inflates rows with quoted delimiters (e.g. `"Smith, John"`),
2172
+ // which used to make consistency collapse and reject a valid CSV.
2173
+ const counts = lines.map((line) => ContentHeuristicStrategy.countDelimitersOutsideQuotes(line, delimiter));
2172
2174
  const firstCount = counts[0];
2173
2175
  const consistentLines = counts.filter((c) => c === firstCount).length;
2174
2176
  return consistentLines / lines.length >= 0.8;
2175
2177
  }
2178
+ /**
2179
+ * Count occurrences of `delimiter` in `line` that fall OUTSIDE double-quoted
2180
+ * fields, honoring RFC-4180 escaped quotes (`""`). Used by CSV detection so a
2181
+ * delimiter embedded in a quoted value is not mistaken for a column break.
2182
+ */
2183
+ static countDelimitersOutsideQuotes(line, delimiter) {
2184
+ let count = 0;
2185
+ let inQuotes = false;
2186
+ for (let i = 0; i < line.length; i++) {
2187
+ const ch = line[i];
2188
+ if (ch === '"') {
2189
+ if (inQuotes && line[i + 1] === '"') {
2190
+ i++; // escaped quote inside a quoted field — skip the pair
2191
+ continue;
2192
+ }
2193
+ inQuotes = !inQuotes;
2194
+ }
2195
+ else if (ch === delimiter && !inQuotes) {
2196
+ count++;
2197
+ }
2198
+ }
2199
+ return count;
2200
+ }
2176
2201
  looksLikeJSON(text) {
2177
2202
  // hasJsonMarkers now does full validation including JSON.parse
2178
2203
  return hasJsonMarkers(text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.88.6",
3
+ "version": "9.88.8",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (58+ servers), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {