@thinkingai/ae-cli 6.0.46 → 6.0.48-customer.1

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 (93) hide show
  1. package/README.md +2 -0
  2. package/README.zh.md +6 -0
  3. package/dist/{auth-ROB2EDYV.js → auth-QPEVVBKT.js} +13 -7
  4. package/dist/auth-UMJAA72O.js +15 -0
  5. package/dist/{capability-GQ47BCFI.js → capability-RUSV77CO.js} +11 -10
  6. package/dist/{chunk-UW5UN47B.js → chunk-533JK3YX.js} +2 -0
  7. package/dist/{chunk-4XXOWOTA.js → chunk-6QK7SYYY.js} +5 -4
  8. package/dist/{chunk-TUKQZTMI.js → chunk-7K24F7T2.js} +2 -0
  9. package/dist/{chunk-OO6XR6LK.js → chunk-AMBFK2K3.js} +2 -0
  10. package/dist/{sync-HKIOZXQE.js → chunk-B5HMQAOM.js} +31 -540
  11. package/dist/chunk-BE73PUFX.js +25216 -0
  12. package/dist/{chunk-DQRPU6EE.js → chunk-BVDST5YT.js} +5 -4
  13. package/dist/{chunk-VTWMIC5L.js → chunk-E2JKXMVO.js} +2 -0
  14. package/dist/{chunk-JHENBQ5B.js → chunk-FR2EDAQM.js} +1 -0
  15. package/dist/{chunk-ZQ47LWTI.js → chunk-GJXRF7Y6.js} +6 -5
  16. package/dist/{chunk-DWO43OIB.js → chunk-JQ3ENZZH.js} +2 -0
  17. package/dist/chunk-JYMGHGCE.js +191 -0
  18. package/dist/{chunk-BYYS3ANB.js → chunk-KEWVSO64.js} +5 -4
  19. package/dist/{chunk-3FY3RJ26.js → chunk-KZ6ZHF5J.js} +2 -1
  20. package/dist/{chunk-2MN54X6H.js → chunk-NSIVXKJH.js} +4 -3
  21. package/dist/{chunk-ZQKDZXDO.js → chunk-O6K2ODUE.js} +3 -2
  22. package/dist/{chunk-J2DEBMRF.js → chunk-RWXIXOZG.js} +22 -9
  23. package/dist/{chunk-4NN5IWVN.js → chunk-T6OSFQZD.js} +2 -0
  24. package/dist/{chunk-3KWQYGYI.js → chunk-TKHSULJT.js} +2 -0
  25. package/dist/{chunk-6EIJSNBD.js → chunk-Y74WTIKJ.js} +2 -0
  26. package/dist/{client-L2YDMHQ6.js → client-N7NGZUNI.js} +4 -3
  27. package/dist/{community-report-client-C7WDGET3.js → community-report-client-6OL6GBQW.js} +3 -2
  28. package/dist/{config-BMYZX2UE.js → config-XNMACCKX.js} +8 -7
  29. package/dist/{data-integration-QEKDWQDY.js → data-integration-FZQQC3KW.js} +923 -98
  30. package/dist/index.js +36 -34
  31. package/dist/{local-data-upload-client-4YYHSYD6.js → local-data-upload-client-EFHDLXFG.js} +3 -2
  32. package/dist/{memory-3ORCR7JH.js → memory-HBJQ2GC5.js} +8 -7
  33. package/dist/{metadata-A6QLH3IS.js → metadata-PGDJE5RQ.js} +12 -11
  34. package/dist/{model-UGRDX4MW.js → model-BHLJ2QMT.js} +10 -9
  35. package/dist/{personal-semantic-preference-QZFAJWGE.js → personal-semantic-preference-E5NZ56DN.js} +11 -10
  36. package/dist/sync-MUWPRM5I.js +522 -0
  37. package/dist/{te-agent-BR6VDBNX.js → te-agent-WQZTMEK4.js} +10 -9
  38. package/dist/{te-analysis-MUKUXJL4.js → te-analysis-YWCSSLYL.js} +107 -20
  39. package/dist/{te-community-ISDQWJU7.js → te-community-BNE6KREL.js} +8 -7
  40. package/dist/{te-dataops-6P5IKWNJ.js → te-dataops-TXLKM2W6.js} +467 -67
  41. package/dist/{te-engage-ELA3C5BM.js → te-engage-VEW4CJHK.js} +26 -10
  42. package/dist/{te-kb-RCLSSH2Q.js → te-kb-BUD6N3NI.js} +311 -24
  43. package/dist/{te-system-FXITO2JG.js → te-system-44LZJDTQ.js} +573 -37
  44. package/dist/{te-team-ADOC2ROP.js → te-team-XMP47YXE.js} +10 -7
  45. package/dist/{update-YCYCKJOO.js → update-UODFOZ4P.js} +7 -6
  46. package/package.json +18 -9
  47. package/skills/ae-analysis/references/analysis_gateway_assets.md +1 -1
  48. package/skills/ae-analysis/references/audience_models.md +18 -0
  49. package/skills/ae-analysis/references/command_index.md +2 -2
  50. package/skills/ae-analysis/references/project_timezone_update.md +13 -4
  51. package/skills/ae-analysis/references/property_create.md +2 -0
  52. package/skills/ae-analysis/references/super_metadata_batch_create.md +2 -0
  53. package/skills/ae-analysis/references/user_cluster_models.md +2 -0
  54. package/skills/ae-analysis/references/user_cluster_update.md +8 -4
  55. package/skills/ae-analysis/references/user_tag_create.md +7 -1
  56. package/skills/ae-analysis/references/user_tag_models.md +8 -2
  57. package/skills/ae-data-integration/SKILL.md +1 -0
  58. package/skills/ae-data-integration/references/error-handling.md +54 -1
  59. package/skills/ae-data-integration/references/source-inspect.md +18 -2
  60. package/skills/ae-data-integration/references/tracking-plan.md +1 -1
  61. package/skills/ae-data-integration/references/transform.md +4 -2
  62. package/skills/ae-data-integration/references/ue-mapping.md +5 -2
  63. package/skills/ae-data-integration/references/ue-routing.md +23 -0
  64. package/skills/ae-dataops/SKILL.md +11 -1
  65. package/skills/ae-dataops/references/dataops-backfill.md +135 -0
  66. package/skills/ae-engage/SKILL.md +3 -0
  67. package/skills/ae-engage/references/build-task-save-guide.md +5 -1
  68. package/skills/ae-engage/references/save-task.md +6 -0
  69. package/skills/ae-generate-tracking-plan/SKILL.md +9 -8
  70. package/skills/ae-generate-tracking-plan/references/business-dimension-mapping.md +1 -1
  71. package/skills/ae-kb/SKILL.md +120 -9
  72. package/skills/ae-system/SKILL.md +28 -30
  73. package/skills/ae-system/references/channel-management.md +303 -0
  74. package/dist/auth-GBMV6TEJ.js +0 -14
  75. package/dist/capability-IINANQJA.js +0 -349
  76. package/dist/chunk-GJLGIMAO.js +0 -556
  77. package/dist/chunk-OMPRXM3V.js +0 -349
  78. package/dist/chunk-QNOLN2LJ.js +0 -509
  79. package/dist/chunk-RJDU7NYP.js +0 -1198
  80. package/dist/chunk-RNAALWJK.js +0 -98
  81. package/dist/chunk-SERWF6G5.js +0 -13
  82. package/dist/chunk-XNVMVIUI.js +0 -71
  83. package/dist/memory-I2WXDTV2.js +0 -893
  84. package/dist/metadata-HC7GBTTD.js +0 -339
  85. package/dist/model-HLHIEFMU.js +0 -139
  86. package/dist/personal-semantic-preference-YXAZBFVW.js +0 -239
  87. package/dist/sync-TFHU2UTG.js +0 -10261
  88. package/dist/te-agent-VLYOV7S4.js +0 -3894
  89. package/dist/te-analysis-JECYCV6K.js +0 -9357
  90. package/dist/te-community-5DMNKJWY.js +0 -1859
  91. package/dist/te-dataops-CVULXNVB.js +0 -2209
  92. package/dist/te-engage-MC5IQZIU.js +0 -4898
  93. package/dist/te-system-K2GYMCTB.js +0 -2213
@@ -1,15 +1,22 @@
1
+ import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
1
2
  import {
3
+ require_excel,
4
+ require_styles_xform,
5
+ require_worksheet_reader,
2
6
  validateAndFix,
3
7
  validateDraft
4
- } from "./chunk-RJDU7NYP.js";
8
+ } from "./chunk-BE73PUFX.js";
9
+ import "./chunk-B5HMQAOM.js";
5
10
  import {
6
11
  getConfigDir
7
- } from "./chunk-3FY3RJ26.js";
12
+ } from "./chunk-KZ6ZHF5J.js";
8
13
  import {
9
14
  CliValidationError,
10
15
  LocalDataUploadError
11
- } from "./chunk-UW5UN47B.js";
12
- import "./chunk-JHENBQ5B.js";
16
+ } from "./chunk-533JK3YX.js";
17
+ import {
18
+ __toESM
19
+ } from "./chunk-FR2EDAQM.js";
13
20
 
14
21
  // src/commands/data-integration/inspect.ts
15
22
  import { basename as basename3 } from "path";
@@ -103,11 +110,14 @@ function assessFileSize(fileName, format, sizeBytes, encoding) {
103
110
  }
104
111
 
105
112
  // src/commands/data-integration/input.ts
113
+ var import_exceljs = __toESM(require_excel(), 1);
114
+ var import_worksheet_reader = __toESM(require_worksheet_reader(), 1);
115
+ var import_styles_xform = __toESM(require_styles_xform(), 1);
106
116
  import { createHash } from "crypto";
107
117
  import { createReadStream as createReadStream2, statSync } from "fs";
108
- import { createRequire as createRequire2 } from "module";
109
118
  import { extname, basename } from "path";
110
119
  import { createInterface } from "readline";
120
+ import { StringDecoder } from "string_decoder";
111
121
  import { pipeline } from "stream/promises";
112
122
  import XLSXMod from "xlsx";
113
123
  import { parse as parseCsv } from "csv-parse";
@@ -626,8 +636,6 @@ function isStrongDateTime(value) {
626
636
 
627
637
  // src/commands/data-integration/input.ts
628
638
  var XLSX = XLSXMod.default ?? XLSXMod;
629
- var require3 = createRequire2(import.meta.url);
630
- var ExcelWorksheetReader = require3("exceljs/lib/stream/xlsx/worksheet-reader");
631
639
  function resolveLocalDataInputMeta(filePath) {
632
640
  let format = resolveFormat(filePath);
633
641
  let delimiter;
@@ -664,10 +672,12 @@ async function inspectLocalDataInput(filePath) {
664
672
  const meta = resolveLocalDataInputMeta(filePath);
665
673
  emitSizeWarning(meta.filePath, meta.format, meta.sizeBytes, meta.encoding);
666
674
  try {
675
+ const discovered = await discoverDataSets(filePath, meta.format);
667
676
  return {
668
677
  ...meta,
669
678
  sha256: await sha256File(filePath),
670
- dataSets: await discoverDataSets(filePath, meta.format)
679
+ dataSets: discovered.visible,
680
+ ...discovered.hidden.length > 0 ? { excludedDataSets: discovered.hidden } : {}
671
681
  };
672
682
  } catch (error) {
673
683
  if (error instanceof CliValidationError) throw error;
@@ -690,15 +700,28 @@ function emitSizeWarning(filePath, format, sizeBytes, encoding) {
690
700
  }
691
701
  function selectDataSet(input, requested) {
692
702
  if (requested) {
693
- const selected = input.dataSets.find((candidate) => candidate.id === requested || candidate.selector === requested || candidate.label === requested);
694
- if (!selected) {
695
- throw new CliValidationError("The requested Sheet or JSON Path was not found.", {
696
- code: "LOCAL_DATA_SET_NOT_FOUND",
697
- hint: `Choose one of: ${input.dataSets.map((item) => item.id).join(", ")}`,
698
- location: { field: "data-set" }
699
- });
703
+ const matches = (candidate) => candidate.id === requested || candidate.selector === requested || candidate.label === requested;
704
+ const selected = input.dataSets.find(matches);
705
+ if (selected) return selected;
706
+ const excluded = input.excludedDataSets?.find(matches);
707
+ if (excluded) {
708
+ process.stderr.write(`Warning: "${excluded.label}" is hidden in the source workbook and was selected explicitly.
709
+ `);
710
+ return excluded;
700
711
  }
701
- return selected;
712
+ throw new CliValidationError("The requested Sheet or JSON Path was not found.", {
713
+ code: "LOCAL_DATA_SET_NOT_FOUND",
714
+ hint: `Choose one of: ${input.dataSets.map((item) => item.id).join(", ")}`,
715
+ location: { field: "data-set" }
716
+ });
717
+ }
718
+ const hidden = input.excludedDataSets ?? [];
719
+ if (input.dataSets.length === 0 && hidden.length > 0) {
720
+ throw new CliValidationError("Every data set in this file is hidden in the source.", {
721
+ code: "LOCAL_DATA_ALL_DATA_SETS_HIDDEN",
722
+ hint: `Confirm with the user, then pass --data-set with one of: ${hidden.map((item) => item.id).join(", ")}`,
723
+ location: { field: "data-set" }
724
+ });
702
725
  }
703
726
  if (input.dataSets.length !== 1) {
704
727
  throw new CliValidationError("This file contains multiple data sets.", {
@@ -736,7 +759,13 @@ async function streamLocalDataRows(input, dataSet, onRow, options = {}) {
736
759
  flattenRules: options.flattenRules,
737
760
  flattenMisses: options.flattenMisses,
738
761
  mergeSheets: options.mergeSheets,
739
- warnRagged: options.warnRagged
762
+ excelDateColumns: options.excelDateColumns,
763
+ cellIssues: options.cellIssues,
764
+ warnRagged: options.warnRagged,
765
+ skipRows: options.skipRows,
766
+ xlsxStructure: options.xlsxStructure,
767
+ fillMergedCells: options.fillMergedCells,
768
+ excludeHiddenRows: options.excludeHiddenRows
740
769
  };
741
770
  const wrappedRow = wrapRowCallback(onRow);
742
771
  try {
@@ -864,6 +893,21 @@ function peekDelimitedRecords(filePath, options = {}) {
864
893
  });
865
894
  return records.slice(0, options.limit ?? 10);
866
895
  }
896
+ var TITLE_ROW_SCAN_LIMIT = 3;
897
+ var TITLE_ROW_MIN_HEADER_WIDTH = 3;
898
+ function detectLeadingTitleRows(rows) {
899
+ const width = (row) => row.filter((value) => !isMissing(value)).length;
900
+ const titles = [];
901
+ for (let index = 0; index < Math.min(rows.length, TITLE_ROW_SCAN_LIMIT); index += 1) {
902
+ const cells = width(rows[index]);
903
+ if (cells > 1) break;
904
+ titles.push({ row: index + 1, non_empty_cells: cells });
905
+ }
906
+ if (titles.length === 0) return [];
907
+ const next = rows[titles.length];
908
+ if (!next || width(next) < TITLE_ROW_MIN_HEADER_WIDTH) return [];
909
+ return titles;
910
+ }
867
911
  function escapeRegex(source) {
868
912
  return source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
869
913
  }
@@ -874,21 +918,29 @@ function isNumeric(value) {
874
918
  }
875
919
  async function discoverDataSets(filePath, format) {
876
920
  if (format === "csv" || format === "tsv" || format === "jsonl") {
877
- return [{ id: "$", kind: "file", label: basename(filePath), selector: "$" }];
921
+ return { visible: [{ id: "$", kind: "file", label: basename(filePath), selector: "$" }], hidden: [] };
878
922
  }
879
923
  if (format === "xls") {
880
924
  const workbook = XLSX.readFile(filePath, { dense: true });
881
- return workbook.SheetNames.map((name) => ({ id: `sheet:${name}`, kind: "sheet", label: name, selector: name }));
925
+ return {
926
+ visible: workbook.SheetNames.map((name) => ({ id: `sheet:${name}`, kind: "sheet", label: name, selector: name })),
927
+ hidden: []
928
+ };
882
929
  }
883
930
  if (format === "xlsx") {
884
- return (await readXlsxSheetDefinitions(filePath)).map((sheet) => ({
931
+ const sheets = await readXlsxSheetDefinitions(filePath);
932
+ const toDataSet = (sheet) => ({
885
933
  id: `sheet:${sheet.name}`,
886
934
  kind: "sheet",
887
935
  label: sheet.name,
888
936
  selector: sheet.name
889
- }));
937
+ });
938
+ return {
939
+ visible: sheets.filter((sheet) => !sheet.hidden).map(toDataSet),
940
+ hidden: sheets.filter((sheet) => sheet.hidden).map(toDataSet)
941
+ };
890
942
  }
891
- return discoverJsonDataSets(filePath);
943
+ return { visible: await discoverJsonDataSets(filePath), hidden: [] };
892
944
  }
893
945
  async function discoverJsonDataSets(filePath) {
894
946
  const encoding = detectEncoding(filePath);
@@ -934,9 +986,11 @@ function childPath(parent) {
934
986
  async function streamDelimited(filePath, onRow, options) {
935
987
  const delimiter = options.delimiter ?? ",";
936
988
  const encoding = options.encoding ?? "utf-8";
989
+ const skipRows = Math.max(0, options.skipRows ?? 0);
937
990
  let headerNames = options.headerNames;
938
991
  if (!headerNames && options.noHeader) {
939
- const firstRecord = peekDelimitedRecords(filePath, { delimiter, encoding, limit: 1 })[0] ?? [];
992
+ const peeked = peekDelimitedRecords(filePath, { delimiter, encoding, limit: skipRows + 1 });
993
+ const firstRecord = peeked[skipRows] ?? [];
940
994
  headerNames = firstRecord.map((_, index) => `col_${index + 1}`);
941
995
  }
942
996
  const parser = parseCsv({
@@ -950,9 +1004,14 @@ async function streamDelimited(filePath, onRow, options) {
950
1004
  decodeTextStream(filePath, encoding).pipe(parser);
951
1005
  let count = 0;
952
1006
  let widthMismatches = 0;
1007
+ let skipped = 0;
953
1008
  let resolvedHeaders = headerNames;
954
1009
  for await (const raw of parser) {
955
1010
  const values = raw;
1011
+ if (skipped < skipRows) {
1012
+ skipped += 1;
1013
+ continue;
1014
+ }
956
1015
  if (!resolvedHeaders) {
957
1016
  resolvedHeaders = dedupeHeaders(values.map((header) => String(header).trim()));
958
1017
  continue;
@@ -1009,13 +1068,22 @@ async function streamJson(filePath, selector, onRow, options) {
1009
1068
  }
1010
1069
  async function streamXlsx(filePath, sheetName, onRow, options) {
1011
1070
  const sheetDefinitions = await readXlsxSheetDefinitions(filePath);
1012
- const targets = options.mergeSheets ? sheetDefinitions : sheetDefinitions.filter((sheet) => sheet.name === sheetName);
1071
+ const targets = options.mergeSheets ? sheetDefinitions.filter((sheet) => !sheet.hidden) : sheetDefinitions.filter((sheet) => sheet.name === sheetName);
1072
+ if (options.mergeSheets) {
1073
+ const excluded = sheetDefinitions.filter((sheet) => sheet.hidden).map((sheet) => sheet.name);
1074
+ if (excluded.length > 0) {
1075
+ process.stderr.write(`Warning: skipped ${excluded.length} hidden worksheet(s): ${excluded.join(", ")}.
1076
+ `);
1077
+ }
1078
+ }
1013
1079
  if (targets.length === 0) {
1014
1080
  throw new CliValidationError("The requested Excel Sheet was not found.", { code: "LOCAL_DATA_SET_NOT_FOUND" });
1015
1081
  }
1016
1082
  const archive = await unzipper.Open.file(filePath);
1017
- const sharedStringsEntry = archive.files.find((entry) => entry.path === "xl/sharedStrings.xml");
1018
- const sharedStrings = sharedStringsEntry ? await readXlsxSharedStrings(sharedStringsEntry) : [];
1083
+ const workbook = await readXlsxWorkbookContext(archive.files);
1084
+ const skipRows = Math.max(0, options.skipRows ?? 0);
1085
+ const collector = options.xlsxStructure;
1086
+ if (collector) collector.filled = Boolean(options.fillMergedCells);
1019
1087
  let count = 0;
1020
1088
  for (const definition of targets) {
1021
1089
  const worksheetEntry = archive.files.find((entry) => entry.path === definition.entryPath);
@@ -1025,19 +1093,37 @@ async function streamXlsx(filePath, sheetName, onRow, options) {
1025
1093
  location: { field: "input-file" }
1026
1094
  });
1027
1095
  }
1028
- const worksheet = new ExcelWorksheetReader({
1029
- workbook: {
1030
- sharedStrings,
1031
- styles: { getStyleModel: () => null },
1032
- properties: { model: {} }
1033
- },
1096
+ const structure = collector || options.fillMergedCells || options.excludeHiddenRows ? await readXlsxSheetStructure(worksheetEntry) : EMPTY_XLSX_SHEET_STRUCTURE;
1097
+ if (collector) {
1098
+ collector.mergedRanges += structure.merged.length;
1099
+ collector.hiddenRowCount += structure.hiddenRows.size;
1100
+ for (const range of structure.merged) {
1101
+ if (collector.mergedRefs.length < XLSX_STRUCTURE_SAMPLE_LIMIT) collector.mergedRefs.push(range.ref);
1102
+ }
1103
+ for (const rowNumber of structure.hiddenRows) {
1104
+ if (collector.hiddenRows.length < XLSX_STRUCTURE_SAMPLE_LIMIT) collector.hiddenRows.push(rowNumber);
1105
+ }
1106
+ }
1107
+ const merges = structure.merged.length > 0 ? new MergedRegionTracker(structure.merged, Boolean(options.fillMergedCells)) : void 0;
1108
+ const hiddenRows = options.excludeHiddenRows ? structure.hiddenRows : void 0;
1109
+ let hiddenColumnsResolved = false;
1110
+ const worksheet = new import_worksheet_reader.default({
1111
+ workbook,
1034
1112
  id: definition.id,
1035
1113
  iterator: worksheetEntry.stream(),
1036
1114
  options: { worksheets: "emit", hyperlinks: "ignore" }
1037
1115
  });
1038
1116
  let headers = options.headerNames;
1117
+ let skipped = 0;
1039
1118
  for await (const excelRow of worksheet) {
1040
- const values = Array.from({ length: Math.max(0, excelRow.cellCount) }, (_, index) => normalizeExcelValue(excelRow.getCell(index + 1).value));
1119
+ const rawValues = Array.from({ length: Math.max(0, excelRow.cellCount) }, (_, index) => readExcelCellValue(excelRow.getCell(index + 1)));
1120
+ const reads = rawValues.map(readExcelCell);
1121
+ const values = reads.map((read) => read.value);
1122
+ merges?.observe(excelRow.number, values);
1123
+ if (skipped < skipRows) {
1124
+ skipped += 1;
1125
+ continue;
1126
+ }
1041
1127
  if (options.noHeader && !headers) {
1042
1128
  headers = values.map((_, index) => `col_${index + 1}`);
1043
1129
  }
@@ -1046,7 +1132,44 @@ async function streamXlsx(filePath, sheetName, onRow, options) {
1046
1132
  continue;
1047
1133
  }
1048
1134
  if (values.every(isMissing)) continue;
1135
+ if (hiddenRows?.has(excelRow.number)) {
1136
+ if (collector) collector.excludedHiddenRows += 1;
1137
+ continue;
1138
+ }
1139
+ const covered = merges?.covered(excelRow.number, values);
1049
1140
  count += 1;
1141
+ if (collector) {
1142
+ if (!hiddenColumnsResolved) {
1143
+ hiddenColumnsResolved = true;
1144
+ for (const span of structure.hiddenColumns) {
1145
+ for (let column = span.min; column <= Math.min(span.max, headers.length); column += 1) {
1146
+ const header = headers[column - 1];
1147
+ if (header) collector.hiddenColumns.add(header);
1148
+ }
1149
+ }
1150
+ }
1151
+ for (const column of covered ?? []) {
1152
+ const header = headers[column - 1];
1153
+ if (!header) continue;
1154
+ collector.coveredCells.set(header, (collector.coveredCells.get(header) ?? 0) + 1);
1155
+ }
1156
+ }
1157
+ if (options.excelDateColumns) {
1158
+ rawValues.forEach((raw, index) => {
1159
+ const header = headers?.[index];
1160
+ if (header && isExcelDateCell(raw)) options.excelDateColumns?.add(header);
1161
+ });
1162
+ }
1163
+ const cellIssues = options.cellIssues;
1164
+ if (cellIssues) {
1165
+ reads.forEach((read, index) => {
1166
+ const header = headers?.[index];
1167
+ if (!read.issue || !header) return;
1168
+ const byColumn = cellIssues.get(read.issue) ?? /* @__PURE__ */ new Map();
1169
+ byColumn.set(header, (byColumn.get(header) ?? 0) + 1);
1170
+ cellIssues.set(read.issue, byColumn);
1171
+ });
1172
+ }
1050
1173
  const row = Object.fromEntries(headers.map((header, index) => [header, values[index] ?? null]));
1051
1174
  await onRow(
1052
1175
  options.flattenRules && Object.keys(options.flattenRules).length > 0 ? flattenDelimitedRow(row, options.flattenRules) : row,
@@ -1066,28 +1189,29 @@ async function readXlsxSheetDefinitions(filePath) {
1066
1189
  location: { field: "input-file" }
1067
1190
  });
1068
1191
  }
1069
- const [workbookXml, relationshipsXml] = await Promise.all([
1070
- workbookEntry.buffer().then((value) => value.toString("utf8")),
1071
- relationshipsEntry.buffer().then((value) => value.toString("utf8"))
1072
- ]);
1073
1192
  const targets = /* @__PURE__ */ new Map();
1074
- for (const match of relationshipsXml.matchAll(/<Relationship\b([^>]*)\/?\s*>/g)) {
1075
- const attributes = parseXmlAttributes(match[1]);
1076
- if (attributes.Id && attributes.Target) targets.set(attributes.Id, attributes.Target);
1077
- }
1193
+ await parseXmlTags(relationshipsEntry, (tag) => {
1194
+ if (xmlLocalName(tag.name) !== "Relationship") return;
1195
+ const id = xmlAttributeByLocalName(tag.attributes, "Id");
1196
+ const target = xmlAttributeByLocalName(tag.attributes, "Target");
1197
+ if (id && target) targets.set(id, target);
1198
+ });
1078
1199
  const sheets = [];
1079
- for (const match of workbookXml.matchAll(/<sheet\b([^>]*)\/?\s*>/g)) {
1080
- const attributes = parseXmlAttributes(match[1]);
1081
- const relationshipId = attributes["r:id"];
1200
+ await parseXmlTags(workbookEntry, (tag) => {
1201
+ if (xmlLocalName(tag.name) !== "sheet") return;
1202
+ const relationshipId = xmlAttributeByLocalName(tag.attributes, "id");
1082
1203
  const target = relationshipId ? targets.get(relationshipId) : void 0;
1083
1204
  const fileNumber = target?.match(/worksheets\/sheet(\d+)\.xml$/)?.[1];
1084
- if (!attributes.name || !relationshipId || !fileNumber) continue;
1205
+ const name = xmlAttributeByLocalName(tag.attributes, "name");
1206
+ if (!name || !relationshipId || !fileNumber) return;
1207
+ const state = xmlAttributeByLocalName(tag.attributes, "state");
1085
1208
  sheets.push({
1086
- id: Number(attributes.sheetId ?? fileNumber),
1087
- name: decodeXml(attributes.name),
1088
- entryPath: normalizeXlsxEntryPath(target)
1209
+ id: Number(xmlAttributeByLocalName(tag.attributes, "sheetId") ?? fileNumber),
1210
+ name: decodeXml(name),
1211
+ entryPath: normalizeXlsxEntryPath(target),
1212
+ hidden: state === "hidden" || state === "veryHidden"
1089
1213
  });
1090
- }
1214
+ });
1091
1215
  if (sheets.length === 0) {
1092
1216
  throw new CliValidationError("XLSX workbook contains no readable Sheets.", {
1093
1217
  code: "LOCAL_DATA_XLSX_INVALID",
@@ -1110,25 +1234,21 @@ function readXlsSheetHeaders(filePath) {
1110
1234
  async function readXlsxSheetHeaders(filePath) {
1111
1235
  const sheetDefinitions = await readXlsxSheetDefinitions(filePath);
1112
1236
  const archive = await unzipper.Open.file(filePath);
1113
- const sharedStringsEntry = archive.files.find((entry) => entry.path === "xl/sharedStrings.xml");
1114
- const sharedStrings = sharedStringsEntry ? await readXlsxSharedStrings(sharedStringsEntry) : [];
1237
+ const workbook = await readXlsxWorkbookContext(archive.files);
1115
1238
  const sheets = [];
1116
1239
  for (const definition of sheetDefinitions) {
1240
+ if (definition.hidden) continue;
1117
1241
  const worksheetEntry = archive.files.find((entry) => entry.path === definition.entryPath);
1118
1242
  if (!worksheetEntry) continue;
1119
- const worksheet = new ExcelWorksheetReader({
1120
- workbook: {
1121
- sharedStrings,
1122
- styles: { getStyleModel: () => null },
1123
- properties: { model: {} }
1124
- },
1243
+ const worksheet = new import_worksheet_reader.default({
1244
+ workbook,
1125
1245
  id: definition.id,
1126
1246
  iterator: worksheetEntry.stream(),
1127
1247
  options: { worksheets: "emit", hyperlinks: "ignore" }
1128
1248
  });
1129
1249
  let headers = [];
1130
1250
  for await (const excelRow of worksheet) {
1131
- const values = Array.from({ length: Math.max(0, excelRow.cellCount) }, (_, index) => normalizeExcelValue(excelRow.getCell(index + 1).value));
1251
+ const values = Array.from({ length: Math.max(0, excelRow.cellCount) }, (_, index) => readExcelCell(readExcelCellValue(excelRow.getCell(index + 1))).value);
1132
1252
  headers = firstRowHeaders(values);
1133
1253
  break;
1134
1254
  }
@@ -1139,13 +1259,82 @@ async function readXlsxSheetHeaders(filePath) {
1139
1259
  function firstRowHeaders(values) {
1140
1260
  return dedupeHeaders(values.map((value, index) => String(value ?? `column_${index + 1}`).trim()));
1141
1261
  }
1262
+ async function peekXlsxRows(filePath, sheetName, limit = 10) {
1263
+ const sheetDefinitions = await readXlsxSheetDefinitions(filePath);
1264
+ const definition = sheetDefinitions.find((sheet) => sheet.name === sheetName);
1265
+ if (!definition) return [];
1266
+ const archive = await unzipper.Open.file(filePath);
1267
+ const worksheetEntry = archive.files.find((entry) => entry.path === definition.entryPath);
1268
+ if (!worksheetEntry) return [];
1269
+ const workbook = await readXlsxWorkbookContext(archive.files);
1270
+ const worksheet = new import_worksheet_reader.default({
1271
+ workbook,
1272
+ id: definition.id,
1273
+ iterator: worksheetEntry.stream(),
1274
+ options: { worksheets: "emit", hyperlinks: "ignore" }
1275
+ });
1276
+ const rows = [];
1277
+ for await (const excelRow of worksheet) {
1278
+ rows.push(Array.from({ length: Math.max(0, excelRow.cellCount) }, (_, index) => readExcelCell(readExcelCellValue(excelRow.getCell(index + 1))).value));
1279
+ if (rows.length >= limit) break;
1280
+ }
1281
+ return rows;
1282
+ }
1283
+ var NO_XLSX_STYLES = { getStyleModel: () => null };
1284
+ var ELAPSED_TIME_FORMATS = /* @__PURE__ */ new Set(["mm:ss", "mmss.0", "[h]:mm:ss"]);
1285
+ function isElapsedTimeFormat(numFmt) {
1286
+ return ELAPSED_TIME_FORMATS.has(numFmt.trim()) || /\[[hms]+\]/i.test(numFmt);
1287
+ }
1288
+ async function readXlsxWorkbookContext(files) {
1289
+ const sharedStringsEntry = files.find((entry) => entry.path === "xl/sharedStrings.xml");
1290
+ const stylesEntry = files.find((entry) => entry.path === "xl/styles.xml");
1291
+ const workbookEntry = files.find((entry) => entry.path === "xl/workbook.xml");
1292
+ return {
1293
+ sharedStrings: sharedStringsEntry ? await readXlsxSharedStrings(sharedStringsEntry) : [],
1294
+ styles: await readXlsxStyles(stylesEntry),
1295
+ properties: { model: { date1904: workbookEntry ? await readXlsxDate1904(workbookEntry) : false } }
1296
+ };
1297
+ }
1298
+ async function readXlsxStyles(entry) {
1299
+ if (!entry) return NO_XLSX_STYLES;
1300
+ let styles;
1301
+ try {
1302
+ styles = new import_styles_xform.default();
1303
+ styles.init();
1304
+ await styles.parseStream(entry.stream());
1305
+ } catch {
1306
+ return NO_XLSX_STYLES;
1307
+ }
1308
+ return {
1309
+ getStyleModel: (id) => {
1310
+ let model;
1311
+ try {
1312
+ model = styles.getStyleModel(id);
1313
+ } catch {
1314
+ return null;
1315
+ }
1316
+ if (!model?.numFmt || !isElapsedTimeFormat(model.numFmt)) return model;
1317
+ const { numFmt, ...rest } = model;
1318
+ return rest;
1319
+ }
1320
+ };
1321
+ }
1322
+ async function readXlsxDate1904(entry) {
1323
+ let date1904 = false;
1324
+ await parseXmlTags(entry, (tag) => {
1325
+ if (xmlLocalName(tag.name) !== "workbookPr") return;
1326
+ const value = xmlAttributeByLocalName(tag.attributes, "date1904");
1327
+ date1904 = value === "1" || value === "true";
1328
+ });
1329
+ return date1904;
1330
+ }
1142
1331
  async function readXlsxSharedStrings(entry) {
1143
1332
  const values = [];
1144
1333
  let inItem = false;
1145
1334
  let current = "";
1146
1335
  const parser = new SaxesParser();
1147
1336
  parser.on("opentag", (tag) => {
1148
- if (tag.name === "si") {
1337
+ if (xmlLocalName(tag.name) === "si") {
1149
1338
  inItem = true;
1150
1339
  current = "";
1151
1340
  }
@@ -1154,26 +1343,147 @@ async function readXlsxSharedStrings(entry) {
1154
1343
  if (inItem) current += text;
1155
1344
  });
1156
1345
  parser.on("closetag", (tag) => {
1157
- if (tag.name === "si") {
1346
+ if (xmlLocalName(tag.name) === "si") {
1158
1347
  values.push(current);
1159
1348
  inItem = false;
1160
1349
  current = "";
1161
1350
  }
1162
1351
  });
1352
+ const decoder = new StringDecoder("utf8");
1163
1353
  for await (const chunk of entry.stream()) {
1164
- parser.write(Buffer.from(chunk).toString("utf8"));
1354
+ parser.write(decoder.write(Buffer.from(chunk)));
1165
1355
  }
1356
+ parser.write(decoder.end());
1166
1357
  parser.close();
1167
1358
  return values;
1168
1359
  }
1360
+ var EMPTY_XLSX_SHEET_STRUCTURE = {
1361
+ merged: [],
1362
+ hiddenRows: /* @__PURE__ */ new Set(),
1363
+ hiddenColumns: []
1364
+ };
1365
+ async function readXlsxSheetStructure(entry) {
1366
+ const merged = [];
1367
+ const hiddenRows = /* @__PURE__ */ new Set();
1368
+ const hiddenColumns = [];
1369
+ try {
1370
+ const parser = new SaxesParser();
1371
+ parser.on("opentag", (tag) => {
1372
+ const attributes = tag.attributes;
1373
+ if (xmlLocalName(tag.name) === "mergeCell") {
1374
+ const range = parseMergedRef(attributes.ref ?? "");
1375
+ if (range) merged.push(range);
1376
+ return;
1377
+ }
1378
+ if (xmlLocalName(tag.name) === "row") {
1379
+ const rowNumber = Number(attributes.r);
1380
+ if (isXlsxFlagSet(attributes.hidden) && Number.isInteger(rowNumber) && rowNumber >= 1) {
1381
+ hiddenRows.add(rowNumber);
1382
+ }
1383
+ return;
1384
+ }
1385
+ if (xmlLocalName(tag.name) === "col" && isXlsxFlagSet(attributes.hidden)) {
1386
+ const min = Number(attributes.min);
1387
+ const max = Number(attributes.max);
1388
+ if (Number.isInteger(min) && min >= 1) {
1389
+ hiddenColumns.push({ min, max: Number.isInteger(max) && max >= min ? max : min });
1390
+ }
1391
+ }
1392
+ });
1393
+ const decoder = new StringDecoder("utf8");
1394
+ for await (const chunk of entry.stream()) {
1395
+ parser.write(decoder.write(Buffer.from(chunk)));
1396
+ }
1397
+ parser.write(decoder.end());
1398
+ parser.close();
1399
+ } catch {
1400
+ return EMPTY_XLSX_SHEET_STRUCTURE;
1401
+ }
1402
+ return { merged, hiddenRows, hiddenColumns };
1403
+ }
1404
+ function isXlsxFlagSet(value) {
1405
+ return value === "1" || value === "true";
1406
+ }
1407
+ function parseMergedRef(ref) {
1408
+ const match = /^([A-Za-z]+)(\d+):([A-Za-z]+)(\d+)$/.exec(ref.trim());
1409
+ if (!match) return void 0;
1410
+ const left = xlsxColumnNumber(match[1]);
1411
+ const top = Number(match[2]);
1412
+ const right = xlsxColumnNumber(match[3]);
1413
+ const bottom = Number(match[4]);
1414
+ if (right < left || bottom < top || top < 1) return void 0;
1415
+ return { ref: ref.trim().toUpperCase(), top, bottom, left, right };
1416
+ }
1417
+ function xlsxColumnNumber(letters) {
1418
+ let column = 0;
1419
+ for (const letter of letters.toUpperCase()) column = column * 26 + (letter.charCodeAt(0) - 64);
1420
+ return column;
1421
+ }
1422
+ var MergedRegionTracker = class {
1423
+ constructor(ranges, fill) {
1424
+ this.fill = fill;
1425
+ this.pending = [...ranges].sort((left, right) => left.top - right.top || left.left - right.left);
1426
+ }
1427
+ fill;
1428
+ pending;
1429
+ next = 0;
1430
+ active = [];
1431
+ /** Advance to `rowNumber`, taking the anchor value of every range that starts on it. */
1432
+ observe(rowNumber, values) {
1433
+ while (this.next < this.pending.length && this.pending[this.next].top <= rowNumber) {
1434
+ this.active.push({ range: this.pending[this.next], anchor: void 0 });
1435
+ this.next += 1;
1436
+ }
1437
+ if (this.active.length === 0) return;
1438
+ this.active = this.active.filter((entry) => entry.range.bottom >= rowNumber);
1439
+ for (const entry of this.active) {
1440
+ if (entry.range.top === rowNumber) entry.anchor = values[entry.range.left - 1];
1441
+ }
1442
+ }
1443
+ /**
1444
+ * The 1-based columns whose cell on this row is empty only because a merge covers it, filled from
1445
+ * the anchor when enabled. `observe` must have run for the same row first.
1446
+ */
1447
+ covered(rowNumber, values) {
1448
+ const columns = [];
1449
+ for (const entry of this.active) {
1450
+ if (isMissing(entry.anchor)) continue;
1451
+ const { range } = entry;
1452
+ for (let column = range.left; column <= range.right; column += 1) {
1453
+ if (range.top === rowNumber && column === range.left) continue;
1454
+ if (!isMissing(values[column - 1])) continue;
1455
+ columns.push(column);
1456
+ if (this.fill) values[column - 1] = entry.anchor;
1457
+ }
1458
+ }
1459
+ return columns;
1460
+ }
1461
+ };
1169
1462
  function normalizeXlsxEntryPath(target) {
1170
1463
  const normalized = target.replace(/^\//, "");
1171
1464
  return normalized.startsWith("xl/") ? normalized : `xl/${normalized.replace(/^\.\//, "")}`;
1172
1465
  }
1173
- function parseXmlAttributes(source) {
1174
- const attributes = {};
1175
- for (const match of source.matchAll(/([\w:-]+)="([^"]*)"/g)) attributes[match[1]] = match[2];
1176
- return attributes;
1466
+ function xmlLocalName(qname) {
1467
+ const colon = qname.lastIndexOf(":");
1468
+ return colon === -1 ? qname : qname.slice(colon + 1);
1469
+ }
1470
+ function xmlAttributeByLocalName(attributes, local) {
1471
+ for (const [name, value] of Object.entries(attributes)) {
1472
+ if (xmlLocalName(name) === local) return value;
1473
+ }
1474
+ return void 0;
1475
+ }
1476
+ async function parseXmlTags(entry, onOpenTag) {
1477
+ const parser = new SaxesParser();
1478
+ parser.on("opentag", (tag) => {
1479
+ onOpenTag({ name: tag.name, attributes: tag.attributes });
1480
+ });
1481
+ const decoder = new StringDecoder("utf8");
1482
+ for await (const chunk of entry.stream()) {
1483
+ parser.write(decoder.write(Buffer.from(chunk)));
1484
+ }
1485
+ parser.write(decoder.end());
1486
+ parser.close();
1177
1487
  }
1178
1488
  function decodeXml(source) {
1179
1489
  return source.replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
@@ -1185,10 +1495,12 @@ async function streamXls(filePath, sheetName, onRow, options) {
1185
1495
  throw new CliValidationError("The requested Excel Sheet was not found.", { code: "LOCAL_DATA_SET_NOT_FOUND" });
1186
1496
  }
1187
1497
  let count = 0;
1498
+ const skipRows = Math.max(0, options.skipRows ?? 0);
1188
1499
  for (const name of names) {
1189
1500
  const sheet = workbook.Sheets[name];
1190
1501
  if (!sheet) continue;
1191
- const rows = XLSX.utils.sheet_to_json(sheet, { header: 1, raw: true, defval: null });
1502
+ const allRows = XLSX.utils.sheet_to_json(sheet, { header: 1, raw: true, defval: null });
1503
+ const rows = allRows.slice(skipRows);
1192
1504
  if (rows.length === 0) continue;
1193
1505
  let headers = options.headerNames;
1194
1506
  let start = 0;
@@ -1212,16 +1524,127 @@ async function streamXls(filePath, sheetName, onRow, options) {
1212
1524
  }
1213
1525
  return count;
1214
1526
  }
1215
- function normalizeExcelValue(value) {
1216
- if (value instanceof Date) return value;
1527
+ function excelDateToWallClock(value) {
1528
+ const pad = (part, width = 2) => String(part).padStart(width, "0");
1529
+ const date = `${value.getUTCFullYear()}-${pad(value.getUTCMonth() + 1)}-${pad(value.getUTCDate())}`;
1530
+ const [hours, minutes, seconds, ms] = [
1531
+ value.getUTCHours(),
1532
+ value.getUTCMinutes(),
1533
+ value.getUTCSeconds(),
1534
+ value.getUTCMilliseconds()
1535
+ ];
1536
+ if (hours === 0 && minutes === 0 && seconds === 0 && ms === 0) return date;
1537
+ const time = `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
1538
+ return ms === 0 ? `${date} ${time}` : `${date} ${time}.${pad(ms, 3)}`;
1539
+ }
1540
+ function isExcelDateCell(value) {
1541
+ if (value instanceof Date) return true;
1542
+ return Boolean(value && typeof value === "object" && "result" in value && value.result instanceof Date);
1543
+ }
1544
+ function readExcelCellValue(cell) {
1545
+ if (cell.type !== import_exceljs.default.ValueType.Formula) return cell.value;
1546
+ const model = cell.model;
1547
+ if (!model || !("result" in model)) return cell.value;
1548
+ return { formula: model.formula ?? "", result: model.result };
1549
+ }
1550
+ var CELL_ISSUE_ORDER = ["formula_no_cached_value", "error_value", "unreadable_object"];
1551
+ var CELL_ISSUE_MESSAGES = {
1552
+ formula_no_cached_value: "hold a formula whose last computed result is not stored in the file, and were read as missing. Excel normally saves that result next to the formula; a file exported without it has no value to upload. Recalculate and re-export in Excel, or export values instead of formulas. This tool never evaluates a formula and never guesses a result.",
1553
+ error_value: "hold an Excel error value (#N/A, #DIV/0!, \u2026), and were read as missing. Fix them in the source file if those cells were meant to carry data.",
1554
+ unreadable_object: "arrived in a cell shape this tool does not recognize, and were read as missing rather than passed on as an object. Report the file if these cells do carry data."
1555
+ };
1556
+ function cellIssueWarnings(cellIssues) {
1557
+ const warnings = [];
1558
+ for (const issue of CELL_ISSUE_ORDER) {
1559
+ const byColumn = cellIssues.get(issue);
1560
+ if (!byColumn || byColumn.size === 0) continue;
1561
+ const columns = [...byColumn.entries()].map(([column, count]) => `${column} (${count})`).join(", ");
1562
+ const total = [...byColumn.values()].reduce((sum, count) => sum + count, 0);
1563
+ warnings.push(`${total} cell(s) ${CELL_ISSUE_MESSAGES[issue]} Affected columns: ${columns}.`);
1564
+ }
1565
+ return warnings;
1566
+ }
1567
+ function cellIssueCounts(cellIssues) {
1568
+ const counts = {};
1569
+ for (const issue of CELL_ISSUE_ORDER) {
1570
+ const byColumn = cellIssues.get(issue);
1571
+ if (!byColumn || byColumn.size === 0) continue;
1572
+ counts[issue] = Object.fromEntries(byColumn);
1573
+ }
1574
+ return counts;
1575
+ }
1576
+ var XLSX_STRUCTURE_SAMPLE_LIMIT = 10;
1577
+ function createXlsxStructureCollector() {
1578
+ return {
1579
+ mergedRanges: 0,
1580
+ mergedRefs: [],
1581
+ coveredCells: /* @__PURE__ */ new Map(),
1582
+ filled: false,
1583
+ hiddenRows: [],
1584
+ hiddenRowCount: 0,
1585
+ excludedHiddenRows: 0,
1586
+ hiddenColumns: /* @__PURE__ */ new Set()
1587
+ };
1588
+ }
1589
+ function xlsxStructureReport(collector) {
1590
+ const hasFindings = collector.mergedRanges > 0 || collector.hiddenRowCount > 0 || collector.hiddenColumns.size > 0;
1591
+ if (!hasFindings) return void 0;
1592
+ return {
1593
+ ...collector.mergedRanges > 0 ? {
1594
+ merged_ranges: collector.mergedRanges,
1595
+ merged_range_samples: collector.mergedRefs,
1596
+ merged_covered_cells: Object.fromEntries(collector.coveredCells),
1597
+ merged_cells_filled: collector.filled
1598
+ } : {},
1599
+ ...collector.hiddenRowCount > 0 ? {
1600
+ hidden_rows: collector.hiddenRowCount,
1601
+ hidden_row_samples: collector.hiddenRows,
1602
+ excluded_hidden_rows: collector.excludedHiddenRows
1603
+ } : {},
1604
+ ...collector.hiddenColumns.size > 0 ? { hidden_columns: [...collector.hiddenColumns] } : {}
1605
+ };
1606
+ }
1607
+ function xlsxStructureWarnings(collector, excludedColumns) {
1608
+ const warnings = [];
1609
+ const sample = (values, total) => values.join(", ") + (total > values.length ? ", \u2026" : "");
1610
+ if (collector.coveredCells.size > 0) {
1611
+ const columns = [...collector.coveredCells.entries()].map(([column, count]) => `${column} (${count})`).join(", ");
1612
+ const total = [...collector.coveredCells.values()].reduce((sum, count) => sum + count, 0);
1613
+ warnings.push(collector.filled ? `${total} cell(s) were empty only because a merged block covers them, and were filled from the value on the block's first row. Affected columns: ${columns}.` : `${total} cell(s) are empty only because a merged block covers them: Excel shows the value on the block's first row only, so every row below it reads as missing. Affected columns: ${columns}. Pass --fill-merged-cells to copy each block's value down its own range, or unmerge and fill the column in the source file.`);
1614
+ }
1615
+ if (collector.hiddenRowCount > 0) {
1616
+ const rows = sample(collector.hiddenRows, collector.hiddenRowCount);
1617
+ warnings.push(collector.excludedHiddenRows > 0 ? `Skipped ${collector.excludedHiddenRows} row(s) hidden in the source worksheet (source row ${rows}).` : `${collector.hiddenRowCount} row(s) are hidden in the source worksheet (source row ${rows}) and were read as data. Pass --exclude-hidden-rows to leave them out.`);
1618
+ }
1619
+ if (collector.hiddenColumns.size > 0) {
1620
+ const excluded = new Set(excludedColumns ?? []);
1621
+ const kept = [...collector.hiddenColumns].filter((column) => !excluded.has(column));
1622
+ if (kept.length > 0) {
1623
+ warnings.push(`${kept.length} column(s) are hidden in the source worksheet and were read as data: ${kept.join(", ")}. List them in the mapping's exclude_columns to leave them out.`);
1624
+ }
1625
+ }
1626
+ return warnings;
1627
+ }
1628
+ function readExcelCell(value) {
1629
+ if (value instanceof Date) return { value: excelDateToWallClock(value) };
1217
1630
  if (value && typeof value === "object") {
1218
- if ("result" in value) return value.result ?? null;
1219
- if ("text" in value) return String(value.text);
1631
+ if ("error" in value) return { value: null, issue: "error_value" };
1632
+ if ("formula" in value || "sharedFormula" in value) {
1633
+ if (!("result" in value)) return { value: null, issue: "formula_no_cached_value" };
1634
+ const result = value.result;
1635
+ if (result === null || result === void 0) return { value: null, issue: "formula_no_cached_value" };
1636
+ if (typeof result === "object" && "error" in result) return { value: null, issue: "error_value" };
1637
+ if (typeof result === "number" && Number.isNaN(result)) return { value: null, issue: "error_value" };
1638
+ if (result instanceof Date) return { value: excelDateToWallClock(result) };
1639
+ return { value: result };
1640
+ }
1641
+ if ("text" in value) return { value: String(value.text) };
1220
1642
  if ("richText" in value) {
1221
- return value.richText.map((part) => part.text).join("");
1643
+ return { value: value.richText.map((part) => part.text).join("") };
1222
1644
  }
1645
+ return { value: null, issue: "unreadable_object" };
1223
1646
  }
1224
- return value ?? null;
1647
+ return { value: value ?? null };
1225
1648
  }
1226
1649
  function dedupeHeaders(headers) {
1227
1650
  const counts = /* @__PURE__ */ new Map();
@@ -1263,7 +1686,11 @@ var MAPPING_VERSION = "ae-data-integration-mapping/v1";
1263
1686
  // src/commands/data-integration/mapping.ts
1264
1687
  var VALID_PROPERTY_NAME = /^[a-z][a-z0-9_]{0,49}$/;
1265
1688
  var VALID_CHILD_PROPERTY_NAME = /^[a-z][a-z0-9_]{0,49}(\.[a-z][a-z0-9_]{0,49})+$/;
1689
+ var VALID_EVENT_NAME = /^[A-Za-z][A-Za-z0-9_]{0,49}$/;
1266
1690
  var FLATTEN_SUPPORTED_FORMATS = ["csv", "tsv", "json", "jsonl", "xls", "xlsx"];
1691
+ var SKIP_ROWS_SUPPORTED_FORMATS = ["csv", "tsv", "xls", "xlsx"];
1692
+ var XLSX_STRUCTURE_SUPPORTED_FORMATS = ["xlsx"];
1693
+ var XLSX_STRUCTURE_FIELDS = ["fill_merged_cells", "exclude_hidden_rows"];
1267
1694
  function readLocalDataMapping(raw, options) {
1268
1695
  const trimmed = raw.trim();
1269
1696
  let text;
@@ -1319,7 +1746,7 @@ function validateMapping(value, options) {
1319
1746
  if (value.mode !== "user_set" && !value.event_name_field && !value.default_event_name) {
1320
1747
  throw mappingError("Track mappings require an event field or default event name.");
1321
1748
  }
1322
- if (value.default_event_name && !VALID_PROPERTY_NAME.test(value.default_event_name)) {
1749
+ if (value.default_event_name && !VALID_EVENT_NAME.test(value.default_event_name)) {
1323
1750
  throw mappingError("The default event name is not a legal AE event name.");
1324
1751
  }
1325
1752
  if (!Array.isArray(value.properties)) throw mappingError("Mapping properties must be an array.");
@@ -1353,7 +1780,7 @@ function validateMapping(value, options) {
1353
1780
  if (value.event_meta !== void 0) {
1354
1781
  if (!isRecord(value.event_meta)) throw mappingError("event_meta must be an object keyed by AE event name.");
1355
1782
  for (const [name, meta] of Object.entries(value.event_meta)) {
1356
- if (!VALID_PROPERTY_NAME.test(name) || !isRecord(meta) || meta.desc !== void 0 && (typeof meta.desc !== "string" || !meta.desc.trim() || meta.desc.length > 200) || meta.tag !== void 0 && (typeof meta.tag !== "string" || !meta.tag.trim() || meta.tag.length > 64)) {
1783
+ if (!VALID_EVENT_NAME.test(name) || !isRecord(meta) || meta.desc !== void 0 && (typeof meta.desc !== "string" || !meta.desc.trim() || meta.desc.length > 200) || meta.tag !== void 0 && (typeof meta.tag !== "string" || !meta.tag.trim() || meta.tag.length > 64)) {
1357
1784
  throw mappingError("event_meta entries need a legal AE event name and non-empty desc/tag strings.");
1358
1785
  }
1359
1786
  }
@@ -1366,7 +1793,7 @@ function validateMapping(value, options) {
1366
1793
  if (!isStringMap(map)) throw mappingError(`value_mapping.${key} must map strings to strings.`);
1367
1794
  if (key === "event_name") {
1368
1795
  for (const target of Object.values(map)) {
1369
- if (!VALID_PROPERTY_NAME.test(target)) {
1796
+ if (!VALID_EVENT_NAME.test(target)) {
1370
1797
  throw mappingError("value_mapping.event_name values must be legal AE event names.");
1371
1798
  }
1372
1799
  }
@@ -1399,6 +1826,23 @@ function validateMapping(value, options) {
1399
1826
  if (value.headers !== void 0 && !isNonEmptyStringArray(value.headers, true)) {
1400
1827
  throw mappingError("headers must be a non-empty array of unique strings.");
1401
1828
  }
1829
+ if (value.skip_rows !== void 0) {
1830
+ if (typeof value.skip_rows !== "number" || !Number.isInteger(value.skip_rows) || value.skip_rows < 1) {
1831
+ throw mappingError("skip_rows must be a positive integer.");
1832
+ }
1833
+ if (!SKIP_ROWS_SUPPORTED_FORMATS.includes(String(value.source.format))) {
1834
+ throw mappingError(`skip_rows is not supported for ${String(value.source.format)} input.`);
1835
+ }
1836
+ }
1837
+ for (const field of XLSX_STRUCTURE_FIELDS) {
1838
+ if (value[field] === void 0) continue;
1839
+ if (typeof value[field] !== "boolean") {
1840
+ throw mappingError(`${field} must be a boolean when provided.`);
1841
+ }
1842
+ if (!XLSX_STRUCTURE_SUPPORTED_FORMATS.includes(String(value.source.format))) {
1843
+ throw mappingError(`${field} is not supported for ${String(value.source.format)} input.`);
1844
+ }
1845
+ }
1402
1846
  if (value.missing_time !== void 0 && value.missing_time !== "now") {
1403
1847
  throw mappingError('missing_time must be "now" when provided.');
1404
1848
  }
@@ -1433,6 +1877,9 @@ function validateMapping(value, options) {
1433
1877
  function isValidAeName(value) {
1434
1878
  return VALID_PROPERTY_NAME.test(value);
1435
1879
  }
1880
+ function isValidEventName(value) {
1881
+ return VALID_EVENT_NAME.test(value);
1882
+ }
1436
1883
  function sourceColumns(mapping) {
1437
1884
  const columns = /* @__PURE__ */ new Set();
1438
1885
  const flattenOut = new Set(Object.keys(mapping.flatten_rules ?? {}));
@@ -1674,6 +2121,18 @@ var GLOBAL_UNIQUE_SAMPLE_LIMIT = 2e5;
1674
2121
  var IDENTITY_MAX_LENGTH = 128;
1675
2122
  var COLUMN_SAMPLE_LIMIT = 5;
1676
2123
  var SAMPLE_TRUNCATE_LENGTH = 40;
2124
+ var VALUE_FREQUENCY_LIMIT = 200;
2125
+ var VALUE_FREQUENCY_TOP = 10;
2126
+ var NUMERIC_SAMPLE_LIMIT = 5e3;
2127
+ var SUMMARY_ROW_CANDIDATE_LIMIT = 50;
2128
+ var SUMMARY_ROW_WARNING_LIMIT = 10;
2129
+ var SUMMARY_LABEL_MAX_LENGTH = 24;
2130
+ var SUMMARY_LABELS_CJK = ["\u5408\u8BA1", "\u603B\u8BA1", "\u5C0F\u8BA1", "\u6C47\u603B"];
2131
+ var SUMMARY_LABELS_LATIN = /* @__PURE__ */ new Set(["total", "totals", "subtotal", "sub total", "grand total", "sum"]);
2132
+ var DUPLICATE_KEY_TRACK_LIMIT = 2e5;
2133
+ var DUPLICATE_GROUP_LIMIT = 20;
2134
+ var DUPLICATE_GROUP_ROW_LIMIT = 10;
2135
+ var DUPLICATE_WARNING_GROUP_LIMIT = 3;
1677
2136
  var NESTED_TREE_SAMPLE_LIMIT = 1e3;
1678
2137
  var USER_PROFILE_TYPES = /* @__PURE__ */ new Set([
1679
2138
  "user_set",
@@ -1708,10 +2167,17 @@ async function profileLocalData(input, dataSet, sourceTimezone = "Asia/Shanghai"
1708
2167
  let nestedObjects = [];
1709
2168
  let nestedSeen = 0;
1710
2169
  const delimitedNested = /* @__PURE__ */ new Map();
2170
+ const excelDateColumns = /* @__PURE__ */ new Set();
2171
+ const cellIssues = /* @__PURE__ */ new Map();
2172
+ const xlsxStructure = input.format === "xlsx" ? createXlsxStructureCollector() : void 0;
2173
+ const summaryCandidates = [];
2174
+ let summaryCandidatesTruncated = false;
2175
+ let duplicateKeys;
2176
+ let duplicateKeysResolved = false;
1711
2177
  await streamLocalDataRows(
1712
2178
  input,
1713
2179
  dataSet,
1714
- (row) => {
2180
+ (row, rowNumber) => {
1715
2181
  rowCount += 1;
1716
2182
  if (collectNestedTree && row !== null && typeof row === "object" && !Array.isArray(row)) {
1717
2183
  nestedSeen += 1;
@@ -1722,6 +2188,10 @@ async function profileLocalData(input, dataSet, sourceTimezone = "Asia/Shanghai"
1722
2188
  if (slot < NESTED_TREE_SAMPLE_LIMIT) nestedObjects[slot] = row;
1723
2189
  }
1724
2190
  }
2191
+ let labelColumn;
2192
+ let numericCells;
2193
+ let keyColumnsPresent = false;
2194
+ let keyColumnsFilled = false;
1725
2195
  for (const name of /* @__PURE__ */ new Set([...columns.keys(), ...Object.keys(row)])) {
1726
2196
  let accumulator = columns.get(name);
1727
2197
  if (!accumulator) {
@@ -1738,11 +2208,23 @@ async function profileLocalData(input, dataSet, sourceTimezone = "Asia/Shanghai"
1738
2208
  ipValidCount: 0,
1739
2209
  lanIpCount: 0,
1740
2210
  samples: [],
1741
- sampleSet: /* @__PURE__ */ new Set()
2211
+ sampleSet: /* @__PURE__ */ new Set(),
2212
+ valueCounts: /* @__PURE__ */ new Map(),
2213
+ valueCountsOverflow: false,
2214
+ numericCount: 0,
2215
+ numericSum: 0,
2216
+ numericMin: Number.POSITIVE_INFINITY,
2217
+ numericMax: Number.NEGATIVE_INFINITY,
2218
+ numericSamples: [],
2219
+ keyLike: matchesName(name, ACCOUNT_NAMES) || matchesName(name, DISTINCT_NAMES) || matchesName(name, TIME_NAMES)
1742
2220
  };
1743
2221
  columns.set(name, accumulator);
1744
2222
  }
1745
2223
  const value = row[name];
2224
+ if (accumulator.keyLike) {
2225
+ keyColumnsPresent = true;
2226
+ if (!isMissing2(value)) keyColumnsFilled = true;
2227
+ }
1746
2228
  if (isMissing2(value)) {
1747
2229
  accumulator.missing += 1;
1748
2230
  continue;
@@ -1758,7 +2240,17 @@ async function profileLocalData(input, dataSet, sourceTimezone = "Asia/Shanghai"
1758
2240
  } else {
1759
2241
  accumulator.uniqueOverflow = true;
1760
2242
  }
1761
- if (options.collectSamples) recordSample(accumulator, value);
2243
+ if (options.collectSamples) {
2244
+ recordSample(accumulator, value);
2245
+ recordValueFrequency(accumulator, value);
2246
+ }
2247
+ if (type === "number") {
2248
+ const numeric = Number(value);
2249
+ recordNumeric(accumulator, numeric, options.collectSamples === true);
2250
+ if (Number.isFinite(numeric)) (numericCells ??= []).push([name, numeric]);
2251
+ } else if (labelColumn === void 0 && matchesSummaryLabel(value)) {
2252
+ labelColumn = name;
2253
+ }
1762
2254
  if (collectDelimitedTree && typeof value === "string") {
1763
2255
  const trimmed = value.trim();
1764
2256
  if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
@@ -1801,6 +2293,19 @@ async function profileLocalData(input, dataSet, sourceTimezone = "Asia/Shanghai"
1801
2293
  if (normalized) recognizedRecordTypes.add(normalized);
1802
2294
  }
1803
2295
  }
2296
+ if (labelColumn !== void 0 || keyColumnsPresent && !keyColumnsFilled && numericCells !== void 0) {
2297
+ if (summaryCandidates.length < SUMMARY_ROW_CANDIDATE_LIMIT) {
2298
+ summaryCandidates.push({ row: rowNumber, labelColumn, numericCells: numericCells ?? [] });
2299
+ } else {
2300
+ summaryCandidatesTruncated = true;
2301
+ }
2302
+ }
2303
+ if (!duplicateKeysResolved) {
2304
+ duplicateKeysResolved = true;
2305
+ const keyColumns = resolveDuplicateKeyColumns(Object.keys(row), options.duplicateKeyFields);
2306
+ if (keyColumns) duplicateKeys = createDuplicateKeyTracker(keyColumns);
2307
+ }
2308
+ if (duplicateKeys) recordDuplicateKey(duplicateKeys, row, rowNumber);
1804
2309
  },
1805
2310
  {
1806
2311
  delimiter: options.delimiter,
@@ -1809,7 +2314,13 @@ async function profileLocalData(input, dataSet, sourceTimezone = "Asia/Shanghai"
1809
2314
  noHeader: options.noHeader,
1810
2315
  flattenRules: options.flattenRules,
1811
2316
  mergeSheets: options.mergeSheets,
1812
- warnRagged: options.warnRagged
2317
+ excelDateColumns,
2318
+ cellIssues,
2319
+ warnRagged: options.warnRagged,
2320
+ skipRows: options.skipRows,
2321
+ xlsxStructure,
2322
+ fillMergedCells: options.fillMergedCells,
2323
+ excludeHiddenRows: options.excludeHiddenRows
1813
2324
  }
1814
2325
  );
1815
2326
  const delimitedNestedTree = /* @__PURE__ */ new Map();
@@ -1836,6 +2347,9 @@ async function profileLocalData(input, dataSet, sourceTimezone = "Asia/Shanghai"
1836
2347
  recognizedRecordTypes,
1837
2348
  sourceTimezone,
1838
2349
  headerNames: options.headerNames,
2350
+ skipRows: options.skipRows,
2351
+ fillMergedCells: options.fillMergedCells,
2352
+ excludeHiddenRows: options.excludeHiddenRows,
1839
2353
  timeFormatByColumn,
1840
2354
  nestedTree
1841
2355
  });
@@ -1851,6 +2365,16 @@ async function profileLocalData(input, dataSet, sourceTimezone = "Asia/Shanghai"
1851
2365
  if (column.lanIpCount > 0) warnings.push(`${column.name}: ${column.lanIpCount} value(s) are private/LAN IP addresses; AE cannot resolve geo information for them.`);
1852
2366
  }
1853
2367
  }
2368
+ if (excelDateColumns.size > 0) {
2369
+ warnings.push(`${[...excelDateColumns].join(", ")}: read as date/time value(s) from the Excel number format rather than as Excel serial numbers. If these columns were uploaded to AE before, they may have been received as number properties, whose type is now locked; confirm with the user before mapping them.`);
2370
+ }
2371
+ warnings.push(...cellIssueWarnings(cellIssues));
2372
+ if (xlsxStructure) warnings.push(...xlsxStructureWarnings(xlsxStructure));
2373
+ const structureReport = xlsxStructure ? xlsxStructureReport(xlsxStructure) : void 0;
2374
+ const summaryRows = confirmSummaryRows(summaryCandidates, columns);
2375
+ if (summaryRows.length > 0) warnings.push(summaryRowWarning(summaryRows, summaryCandidatesTruncated));
2376
+ const duplicateKeyReport = duplicateKeys ? buildDuplicateKeyReport(duplicateKeys) : void 0;
2377
+ if (duplicateKeyReport) warnings.push(duplicateKeyWarning(duplicateKeyReport));
1854
2378
  if (rowCount === 0) warnings.push("The selected data set contains no data rows.");
1855
2379
  return {
1856
2380
  version: "ae-local-data-profile/v1",
@@ -1869,7 +2393,10 @@ async function profileLocalData(input, dataSet, sourceTimezone = "Asia/Shanghai"
1869
2393
  (recommendedMapping.account_id_field || recommendedMapping.distinct_id_field) && recommendedMapping.time.field
1870
2394
  ),
1871
2395
  warnings,
1872
- ...nestedTree ? { nested_tree: nestedTree } : {}
2396
+ ...nestedTree ? { nested_tree: nestedTree } : {},
2397
+ ...structureReport ? { xlsx_structure: structureReport } : {},
2398
+ ...summaryRows.length > 0 ? { summary_rows: summaryRows } : {},
2399
+ ...duplicateKeyReport ? { duplicate_keys: duplicateKeyReport } : {}
1873
2400
  };
1874
2401
  }
1875
2402
  function normalizeAeName(input, fallback) {
@@ -1964,6 +2491,9 @@ function recommendMapping(input) {
1964
2491
  },
1965
2492
  ...time ? { time_format: input.timeFormatByColumn.get(time.name) } : {},
1966
2493
  ...input.headerNames && input.headerNames.length > 0 ? { headers: input.headerNames } : {},
2494
+ ...input.skipRows ? { skip_rows: input.skipRows } : {},
2495
+ ...input.fillMergedCells ? { fill_merged_cells: true } : {},
2496
+ ...input.excludeHiddenRows ? { exclude_hidden_rows: true } : {},
1967
2497
  ...recordType ? { record_type_field: recordType.name } : {},
1968
2498
  ...event ? { event_name_field: event.name } : {},
1969
2499
  ...mode !== "user_set" && !event ? { default_event_name: normalizeAeName(defaultEventSource, "local_event") } : {},
@@ -1976,7 +2506,7 @@ function formatColumnProfile(column, rowCount, includeSamples) {
1976
2506
  const nonNullTypes = [...column.types.entries()].filter(([type]) => type !== "null").sort((left, right) => right[1] - left[1]);
1977
2507
  const inferred = nonNullTypes.length === 0 ? "unknown" : nonNullTypes.length === 1 ? nonNullTypes[0][0] : compatibleTypes(nonNullTypes.map(([type]) => type)) ? nonNullTypes[0][0] : "mixed";
1978
2508
  const finalType = inferred === "number" && idLikeColumn(column.name) ? "string" : inferred;
1979
- return {
2509
+ const profile = {
1980
2510
  name: column.name,
1981
2511
  inferred_type: finalType,
1982
2512
  missing_count: column.missing,
@@ -1988,6 +2518,15 @@ function formatColumnProfile(column, rowCount, includeSamples) {
1988
2518
  time_parse_ratio: ratio(column.timeParseCount, column.nonMissing),
1989
2519
  ...includeSamples ? { samples: column.samples } : {}
1990
2520
  };
2521
+ if (includeSamples) {
2522
+ const frequency = formatValueFrequency(column);
2523
+ if (frequency) profile.value_frequency = frequency;
2524
+ if (finalType === "number") {
2525
+ const summary = formatNumericSummary(column);
2526
+ if (summary) profile.numeric_summary = summary;
2527
+ }
2528
+ }
2529
+ return profile;
1991
2530
  }
1992
2531
  function findCandidate(columns, names) {
1993
2532
  return columns.find((column) => matchesName(column.name, names));
@@ -2202,6 +2741,176 @@ function recordSample(accumulator, value) {
2202
2741
  accumulator.sampleSet.add(text);
2203
2742
  accumulator.samples.push(text);
2204
2743
  }
2744
+ function recordValueFrequency(accumulator, value) {
2745
+ if (accumulator.valueCountsOverflow) return;
2746
+ const text = truncateSample2(value);
2747
+ const seen = accumulator.valueCounts.get(text);
2748
+ if (seen !== void 0) {
2749
+ accumulator.valueCounts.set(text, seen + 1);
2750
+ return;
2751
+ }
2752
+ if (accumulator.valueCounts.size >= VALUE_FREQUENCY_LIMIT) {
2753
+ accumulator.valueCountsOverflow = true;
2754
+ accumulator.valueCounts.clear();
2755
+ return;
2756
+ }
2757
+ accumulator.valueCounts.set(text, 1);
2758
+ }
2759
+ function recordNumeric(accumulator, numeric, retainForQuantiles) {
2760
+ if (!Number.isFinite(numeric)) return;
2761
+ accumulator.numericCount += 1;
2762
+ accumulator.numericSum += numeric;
2763
+ if (numeric < accumulator.numericMin) accumulator.numericMin = numeric;
2764
+ if (numeric > accumulator.numericMax) accumulator.numericMax = numeric;
2765
+ if (!retainForQuantiles) return;
2766
+ if (accumulator.numericSamples.length < NUMERIC_SAMPLE_LIMIT) {
2767
+ accumulator.numericSamples.push(numeric);
2768
+ return;
2769
+ }
2770
+ const slot = randomInt(accumulator.numericCount);
2771
+ if (slot < NUMERIC_SAMPLE_LIMIT) accumulator.numericSamples[slot] = numeric;
2772
+ }
2773
+ function formatValueFrequency(accumulator) {
2774
+ if (accumulator.valueCountsOverflow || accumulator.valueCounts.size === 0) return void 0;
2775
+ const ordered = [...accumulator.valueCounts.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
2776
+ if (ordered[0][1] < 2) return void 0;
2777
+ return ordered.slice(0, VALUE_FREQUENCY_TOP).map(([value, count]) => ({ value, count, ratio: ratio(count, accumulator.nonMissing) }));
2778
+ }
2779
+ function formatNumericSummary(accumulator) {
2780
+ if (accumulator.numericCount === 0 || accumulator.numericSamples.length === 0) return void 0;
2781
+ const sorted = [...accumulator.numericSamples].sort((left, right) => left - right);
2782
+ return {
2783
+ count: accumulator.numericCount,
2784
+ min: round(accumulator.numericMin),
2785
+ max: round(accumulator.numericMax),
2786
+ sum: round(accumulator.numericSum),
2787
+ mean: round(accumulator.numericSum / accumulator.numericCount),
2788
+ p25: round(quantile(sorted, 0.25)),
2789
+ median: round(quantile(sorted, 0.5)),
2790
+ p75: round(quantile(sorted, 0.75)),
2791
+ quantiles_approximate: accumulator.numericCount > accumulator.numericSamples.length
2792
+ };
2793
+ }
2794
+ function quantile(sorted, fraction) {
2795
+ const position = (sorted.length - 1) * fraction;
2796
+ const lower = Math.floor(position);
2797
+ const upper = Math.ceil(position);
2798
+ if (lower === upper) return sorted[lower];
2799
+ return sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
2800
+ }
2801
+ function round(value) {
2802
+ return Number.isFinite(value) ? Number(value.toFixed(6)) : value;
2803
+ }
2804
+ function matchesSummaryLabel(value) {
2805
+ if (typeof value !== "string") return false;
2806
+ const text = value.trim().replace(/[\s::、,,.。]+$/u, "");
2807
+ if (text.length === 0 || text.length > SUMMARY_LABEL_MAX_LENGTH) return false;
2808
+ if (SUMMARY_LABELS_CJK.some((label) => text.startsWith(label))) return true;
2809
+ return SUMMARY_LABELS_LATIN.has(text.toLowerCase());
2810
+ }
2811
+ function confirmSummaryRows(candidates, columns) {
2812
+ const confirmed = [];
2813
+ for (const candidate of candidates) {
2814
+ const totalColumns = [];
2815
+ for (const [name, value] of candidate.numericCells) {
2816
+ const accumulator = columns.get(name);
2817
+ if (!accumulator || accumulator.numericCount < 3 || value === 0) continue;
2818
+ const total = accumulator.numericSum;
2819
+ if (Math.abs(value * 2 - total) <= Math.max(1e-6, Math.abs(total) * 1e-9)) totalColumns.push(name);
2820
+ }
2821
+ const signals = [];
2822
+ if (candidate.labelColumn !== void 0) signals.push("total_label");
2823
+ if (totalColumns.length > 0) signals.push("column_total");
2824
+ if (signals.length === 0) continue;
2825
+ confirmed.push({
2826
+ row: candidate.row,
2827
+ signals,
2828
+ ...candidate.labelColumn !== void 0 ? { label_column: candidate.labelColumn } : {},
2829
+ ...totalColumns.length > 0 ? { total_columns: totalColumns } : {}
2830
+ });
2831
+ }
2832
+ return confirmed;
2833
+ }
2834
+ function summaryRowWarning(rows, truncated) {
2835
+ const shown = rows.slice(0, SUMMARY_ROW_WARNING_LIMIT).map((entry) => entry.row);
2836
+ const remaining = rows.length - shown.length;
2837
+ const labelled = rows.filter((entry) => entry.signals.includes("total_label")).length;
2838
+ const totals = rows.filter((entry) => entry.signals.includes("column_total")).length;
2839
+ const reasons = [
2840
+ labelled > 0 ? `${labelled} carry a total-like label (\u5408\u8BA1 / \u603B\u8BA1 / \u5C0F\u8BA1 / \u6C47\u603B / Total / Subtotal)` : void 0,
2841
+ totals > 0 ? `${totals} hold a number equal to the total of its column's other rows` : void 0
2842
+ ].filter((reason) => reason !== void 0).join("; ");
2843
+ const scope = truncated ? ` The scan stopped after ${SUMMARY_ROW_CANDIDATE_LIMIT} candidate rows, so there may be more.` : "";
2844
+ return `${rows.length} row(s) read as a summary line rather than an observation: ${reasons}. Rows ${shown.join(", ")}${remaining > 0 ? ` and ${remaining} more` : ""} (data-row ordinals, the numbering invalid.rows.jsonl and --salvage-from use; see summary_rows).${scope} Nothing was removed and nothing changed: convert writes these rows as records, and every number reported for their columns counts them, so a real total row makes that column's sum twice its actual total and its max the total. There is no flag that drops a data row; when the user confirms a row is a total, remove it from the source file or re-export without it, then inspect again.`;
2845
+ }
2846
+ function resolveDuplicateKeyColumns(names, explicit) {
2847
+ if (explicit && explicit.length > 0) {
2848
+ const unique = [...new Set(explicit.filter((name) => name.length > 0))];
2849
+ return unique.length > 1 ? unique : void 0;
2850
+ }
2851
+ const identity = names.find((name) => matchesName(name, ACCOUNT_NAMES)) ?? names.find((name) => matchesName(name, DISTINCT_NAMES));
2852
+ const time = names.find((name) => matchesName(name, TIME_NAMES));
2853
+ if (!identity || !time) return void 0;
2854
+ const event = names.find((name) => matchesName(name, EVENT_NAMES));
2855
+ return event ? [identity, time, event] : [identity, time];
2856
+ }
2857
+ function createDuplicateKeyTracker(columns) {
2858
+ return { columns, firstRow: /* @__PURE__ */ new Map(), groups: /* @__PURE__ */ new Map(), checkedRows: 0, overflow: false };
2859
+ }
2860
+ function recordDuplicateKey(tracker, row, rowNumber) {
2861
+ const parts = [];
2862
+ for (const name of tracker.columns) {
2863
+ const value = row[name];
2864
+ if (isMissing2(value)) return;
2865
+ parts.push(sampleText(value).trim());
2866
+ }
2867
+ tracker.checkedRows += 1;
2868
+ const hash = createHash2("sha256").update(JSON.stringify(parts)).digest("hex");
2869
+ const group = tracker.groups.get(hash);
2870
+ if (group) {
2871
+ group.count += 1;
2872
+ if (group.rows.length < DUPLICATE_GROUP_ROW_LIMIT) group.rows.push(rowNumber);
2873
+ else group.rowsTruncated = true;
2874
+ return;
2875
+ }
2876
+ const firstRow = tracker.firstRow.get(hash);
2877
+ if (firstRow !== void 0) {
2878
+ tracker.groups.set(hash, { count: 2, rows: [firstRow, rowNumber], rowsTruncated: false });
2879
+ return;
2880
+ }
2881
+ if (tracker.firstRow.size >= DUPLICATE_KEY_TRACK_LIMIT) {
2882
+ tracker.overflow = true;
2883
+ return;
2884
+ }
2885
+ tracker.firstRow.set(hash, rowNumber);
2886
+ }
2887
+ function buildDuplicateKeyReport(tracker) {
2888
+ if (tracker.groups.size === 0) return void 0;
2889
+ const ordered = [...tracker.groups.entries()].sort(([, left], [, right]) => right.count - left.count || left.rows[0] - right.rows[0]);
2890
+ let extraRows = 0;
2891
+ for (const [, group] of ordered) extraRows += group.count - 1;
2892
+ const groups = ordered.slice(0, DUPLICATE_GROUP_LIMIT).map(([hash, group]) => ({
2893
+ key_hash: hash.slice(0, 16),
2894
+ count: group.count,
2895
+ rows: group.rows,
2896
+ ...group.rowsTruncated ? { rows_truncated: true } : {}
2897
+ }));
2898
+ return {
2899
+ key_columns: tracker.columns,
2900
+ checked_rows: tracker.checkedRows,
2901
+ duplicate_groups: ordered.length,
2902
+ extra_rows: extraRows,
2903
+ groups,
2904
+ ...ordered.length > DUPLICATE_GROUP_LIMIT ? { groups_truncated: true } : {},
2905
+ ...tracker.overflow ? { tracking_truncated: true } : {}
2906
+ };
2907
+ }
2908
+ function duplicateKeyWarning(report) {
2909
+ const examples = report.groups.slice(0, DUPLICATE_WARNING_GROUP_LIMIT).map((group) => `rows ${group.rows.join(", ")}${group.rows_truncated ? ", \u2026" : ""} (${group.count}\xD7)`).join("; ");
2910
+ const remaining = report.duplicate_groups - Math.min(report.groups.length, DUPLICATE_WARNING_GROUP_LIMIT);
2911
+ const scope = report.tracking_truncated ? ` Tracking stopped after ${DUPLICATE_KEY_TRACK_LIMIT} distinct keys, so there may be more.` : "";
2912
+ return `${report.duplicate_groups} business key(s) appear on more than one row, ${report.extra_rows} extra row(s) in total, over ${report.checked_rows} row(s) that carried a value in every key column. Key: ${report.key_columns.join(" + ")}. Examples: ${examples}${remaining > 0 ? ` and ${remaining} more group(s)` : ""} (data-row ordinals, the numbering invalid.rows.jsonl and --salvage-from use; see duplicate_keys).${scope} Nothing was removed: repeated rows are sometimes real (one order line per product), and AE appends every accepted event with no way to un-send it, so this has to be settled before upload. Ask the user whether the repeats are separate observations; if they are not, remove them from the source file and inspect again. Values are compared as written, so two spellings of the same instant are two different keys, and a source with a unique key of its own (an order id) is not compared on it unless the mapping names it.`;
2913
+ }
2205
2914
  function truncateSample2(value) {
2206
2915
  const text = sampleText(value);
2207
2916
  if (text.length <= SAMPLE_TRUNCATE_LENGTH) return text;
@@ -2239,9 +2948,21 @@ var dataIntegrationInspect = {
2239
2948
  { name: "data-set", type: "string", sensitive: true, desc: "Sheet or JSON Path ID returned by a discovery-only inspection." },
2240
2949
  { name: "source-timezone", type: "string", default: "Asia/Shanghai", desc: "IANA timezone used to interpret source times without an offset." },
2241
2950
  { name: "headers", type: "string", sensitive: true, desc: "Comma-separated explicit column names for a headerless file; the first row is data." },
2242
- { name: "headerless", type: "boolean", default: false, desc: "Treat the first row as data and auto-generate col_1..col_N names." }
2951
+ { name: "headerless", type: "boolean", default: false, desc: "Treat the first row as data and auto-generate col_1..col_N names." },
2952
+ { name: "skip-rows", type: "number", default: 0, desc: "Discard the first N rows before the header row is read, for exports with a title or banner above the header. Delimited and Excel only." },
2953
+ { name: "fill-merged-cells", type: "boolean", default: false, desc: "Copy each merged block value into the cells its own range covers, so rows below the first row of a merged block keep the value Excel displays. XLSX only; off by default because those cells are empty in the file." },
2954
+ { name: "exclude-hidden-rows", type: "boolean", default: false, desc: "Leave rows hidden in the source worksheet out of the profile. XLSX only; off by default because a hidden row may still be real data." }
2243
2955
  ],
2244
2956
  risk: "read",
2957
+ validate: (ctx) => {
2958
+ const skipRows = ctx.num("skip-rows");
2959
+ if (!Number.isInteger(skipRows) || skipRows < 0) {
2960
+ throw new CliValidationError("Skipped rows must be a non-negative integer.", {
2961
+ code: "LOCAL_DATA_SKIP_ROWS_INVALID",
2962
+ location: { field: "skip-rows" }
2963
+ });
2964
+ }
2965
+ },
2245
2966
  // Fast size/time pre-check (stat + format/encoding sniff only — no sha256, no profile).
2246
2967
  // Lets agents surface the estimate before committing to a multi-minute full inspection.
2247
2968
  dryRun: async (ctx) => {
@@ -2272,46 +2993,69 @@ var dataIntegrationInspect = {
2272
2993
  const noHeader = ctx.bool("headerless");
2273
2994
  const sourceTimezone = ctx.str("source-timezone");
2274
2995
  const requested = ctx.str("data-set").trim() || void 0;
2996
+ const skipRows = ctx.num("skip-rows");
2997
+ const fillMergedCells = ctx.bool("fill-merged-cells");
2998
+ const excludeHiddenRows = ctx.bool("exclude-hidden-rows");
2275
2999
  if (inputFiles.length === 1) {
2276
3000
  const input = await inspectLocalDataInput(inputFiles[0]);
2277
3001
  const headerConsistency = await readExcelHeaderConsistency(input);
3002
+ const excludedSheets = summarizeExcludedSheets(input);
2278
3003
  if (!requested && input.dataSets.length > 1) {
2279
3004
  return {
2280
3005
  version: "ae-local-data-profile/v1",
2281
3006
  selection_required: true,
2282
3007
  source: { format: input.format, size_bytes: input.sizeBytes, sha256: input.sha256 },
2283
3008
  data_sets: input.dataSets,
3009
+ ...excludedSheets,
2284
3010
  ...headerConsistency ?? {},
2285
3011
  next_step: "Run inspect again with --data-set, then review the recommended mapping."
2286
3012
  };
2287
3013
  }
2288
3014
  const dataSet = selectDataSet(input, requested);
2289
- const headerPresence = headerNames || noHeader ? void 0 : detectHeaderPresence(input);
3015
+ const headerPresence = headerNames || noHeader ? void 0 : detectHeaderPresence(input, skipRows);
3016
+ const headerSignal = headerNames || noHeader ? void 0 : await detectHeaderSignal(input, dataSet, skipRows);
2290
3017
  const profile = await profileLocalData(input, dataSet, sourceTimezone, {
2291
3018
  collectSamples: true,
2292
3019
  collectNestedTree: true,
2293
3020
  headerNames,
2294
- noHeader: noHeader || Boolean(headerPresence)
3021
+ noHeader: noHeader || Boolean(headerPresence),
3022
+ skipRows,
3023
+ fillMergedCells,
3024
+ excludeHiddenRows
2295
3025
  });
2296
3026
  const annotated = headerPresence ? annotateHeaderless(profile, headerPresence) : profile;
2297
- return headerConsistency ? { ...annotated, ...headerConsistency } : annotated;
3027
+ return {
3028
+ ...annotated,
3029
+ ...excludedSheets,
3030
+ ...headerConsistency ?? {},
3031
+ ...annotateRowSkips(annotated.warnings, skipRows, headerSignal)
3032
+ };
2298
3033
  }
2299
3034
  const files = [];
2300
3035
  for (const inputFile of inputFiles) {
2301
3036
  const input = await inspectLocalDataInput(inputFile);
2302
3037
  const dataSet = selectDataSet(input);
2303
- const headerPresence = headerNames || noHeader ? void 0 : detectHeaderPresence(input);
3038
+ const headerPresence = headerNames || noHeader ? void 0 : detectHeaderPresence(input, skipRows);
3039
+ const headerSignal = headerNames || noHeader ? void 0 : await detectHeaderSignal(input, dataSet, skipRows);
2304
3040
  const profile = await profileLocalData(input, dataSet, sourceTimezone, {
2305
3041
  collectSamples: true,
2306
3042
  collectNestedTree: true,
2307
3043
  headerNames,
2308
- noHeader: noHeader || Boolean(headerPresence)
3044
+ noHeader: noHeader || Boolean(headerPresence),
3045
+ skipRows,
3046
+ fillMergedCells,
3047
+ excludeHiddenRows
2309
3048
  });
2310
3049
  const annotated = headerPresence ? annotateHeaderless(profile, headerPresence) : profile;
2311
3050
  const headerConsistency = await readExcelHeaderConsistency(input);
2312
3051
  files.push({
2313
3052
  file: basename3(inputFile),
2314
- profile: headerConsistency ? { ...annotated, ...headerConsistency } : annotated
3053
+ profile: {
3054
+ ...annotated,
3055
+ ...summarizeExcludedSheets(input),
3056
+ ...headerConsistency ?? {},
3057
+ ...annotateRowSkips(annotated.warnings, skipRows, headerSignal)
3058
+ }
2315
3059
  });
2316
3060
  }
2317
3061
  return {
@@ -2322,6 +3066,13 @@ var dataIntegrationInspect = {
2322
3066
  };
2323
3067
  }
2324
3068
  };
3069
+ function summarizeExcludedSheets(input) {
3070
+ const excluded = input.excludedDataSets ?? [];
3071
+ if (excluded.length === 0) return {};
3072
+ return {
3073
+ excluded_sheets: excluded.map((dataSet) => ({ name: dataSet.label, reason: "hidden", data_set: dataSet.id }))
3074
+ };
3075
+ }
2325
3076
  async function readExcelHeaderConsistency(input) {
2326
3077
  if (input.format !== "xls" && input.format !== "xlsx") return void 0;
2327
3078
  return summarizeHeaderConsistency(await readExcelSheetHeaders(input.filePath, input.format));
@@ -2332,10 +3083,11 @@ function summarizeHeaderConsistency(sheets) {
2332
3083
  const allSame = sheets.every((sheet) => JSON.stringify(sheet.headers) === first);
2333
3084
  return allSame ? { header_consistency: "all_same" } : { header_consistency: "different", header_details: sheets.map((sheet) => ({ name: sheet.name, headers: sheet.headers })) };
2334
3085
  }
2335
- function detectHeaderPresence(input) {
3086
+ function detectHeaderPresence(input, skipRows = 0) {
2336
3087
  if (input.format !== "csv" && input.format !== "tsv") return void 0;
2337
3088
  const delimiter = input.delimiter ?? (input.format === "tsv" ? " " : ",");
2338
- const records = peekDelimitedRecords(input.filePath, { delimiter, encoding: input.encoding, limit: 10 });
3089
+ const peeked = peekDelimitedRecords(input.filePath, { delimiter, encoding: input.encoding, limit: 10 + skipRows });
3090
+ const records = peeked.slice(skipRows);
2339
3091
  const detection = detectHeaderRow(records);
2340
3092
  if (detection.hasHeaders) return void 0;
2341
3093
  return {
@@ -2343,6 +3095,41 @@ function detectHeaderPresence(input) {
2343
3095
  autoHeaders: (records[0] ?? []).map((_, index) => `col_${index + 1}`)
2344
3096
  };
2345
3097
  }
3098
+ async function detectHeaderSignal(input, dataSet, skipRows) {
3099
+ const rows = await peekHeaderRows(input, dataSet, skipRows);
3100
+ if (!rows || rows.length === 0) return void 0;
3101
+ const titleRows = detectLeadingTitleRows(rows).map((title) => ({ ...title, row: title.row + skipRows }));
3102
+ if (input.format !== "xlsx") return titleRows.length > 0 ? { titleRows } : void 0;
3103
+ const detection = detectHeaderRow(rows.map((row) => row.map((value) => isEmptyCell(value) ? "" : String(value))));
3104
+ if (detection.hasHeaders && titleRows.length === 0) return void 0;
3105
+ return { ...detection.hasHeaders ? {} : { detection }, titleRows };
3106
+ }
3107
+ async function peekHeaderRows(input, dataSet, skipRows) {
3108
+ if (input.format === "csv" || input.format === "tsv") {
3109
+ const delimiter = input.delimiter ?? (input.format === "tsv" ? " " : ",");
3110
+ return peekDelimitedRecords(input.filePath, { delimiter, encoding: input.encoding, limit: 10 + skipRows }).slice(skipRows);
3111
+ }
3112
+ if (input.format === "xlsx" && dataSet.kind === "sheet") {
3113
+ return (await peekXlsxRows(input.filePath, dataSet.label, 10 + skipRows)).slice(skipRows);
3114
+ }
3115
+ return void 0;
3116
+ }
3117
+ function isEmptyCell(value) {
3118
+ return value === null || value === void 0 || value === "";
3119
+ }
3120
+ var TITLE_ROW_WARNING = "The first rows here hold a single value each, which looks like a title or banner above the real header row; the header was still read from the first row. See leading_title_rows and re-run with --skip-rows N if that is what they are.";
3121
+ var EXCEL_HEADERLESS_WARNING = "The first row of this worksheet looks like data rather than a header, but it was still used as the header. See header_signal, and re-run with --headers or --headerless if it is data.";
3122
+ function annotateRowSkips(warnings, skipRows, signal) {
3123
+ const extra = [];
3124
+ if (signal?.titleRows.length) extra.push(TITLE_ROW_WARNING);
3125
+ if (signal?.detection) extra.push(EXCEL_HEADERLESS_WARNING);
3126
+ return {
3127
+ warnings: extra.length > 0 ? [...warnings, ...extra] : warnings,
3128
+ ...skipRows > 0 ? { skipped_rows: skipRows } : {},
3129
+ ...signal?.titleRows.length ? { leading_title_rows: signal.titleRows } : {},
3130
+ ...signal?.detection ? { header_signal: signal.detection } : {}
3131
+ };
3132
+ }
2346
3133
  function annotateHeaderless(profile, presence) {
2347
3134
  return {
2348
3135
  ...profile,
@@ -2362,7 +3149,7 @@ function splitHeaders(raw) {
2362
3149
  // src/commands/data-integration/plan.ts
2363
3150
  import { writeFile } from "fs/promises";
2364
3151
  var VALID_LOCALES = /* @__PURE__ */ new Set(["zh", "en", "ja", "ko"]);
2365
- var EVENT_NAME_RE = /^[a-z][a-z0-9_]*$/;
3152
+ var EVENT_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
2366
3153
  function toPropType(type) {
2367
3154
  switch (type) {
2368
3155
  case "number":
@@ -2439,7 +3226,7 @@ function resolveEventNames(options) {
2439
3226
  if (!EVENT_NAME_RE.test(name)) {
2440
3227
  throw new CliValidationError(`The event name "${name}" is not a legal AE event name.`, {
2441
3228
  code: "LOCAL_DATA_PLAN_INVALID_EVENT_NAME",
2442
- hint: "Event names must match ^[a-z][a-z0-9_]*$.",
3229
+ hint: "Event names must match ^[A-Za-z][A-Za-z0-9_]*$.",
2443
3230
  location: { field: "event-name" }
2444
3231
  });
2445
3232
  }
@@ -2567,11 +3354,24 @@ async function convertLocalData(options) {
2567
3354
  const dataSet = options.mergeSheets ? { id: "all-sheets", kind: "sheet", label: "all-sheets", selector: "all-sheets" } : selectDataSet(input, options.mapping.source.data_set);
2568
3355
  const streamOptions = {
2569
3356
  headerNames: options.mapping.headers,
3357
+ skipRows: options.mapping.skip_rows,
2570
3358
  flattenRules: options.mapping.flatten_rules,
2571
3359
  mergeSheets: options.mergeSheets,
3360
+ // Both XLSX layout decisions come from the mapping, so the rows converted here are the rows
3361
+ // inspect profiled — the mapping is the only place the user's answer to them is recorded.
3362
+ fillMergedCells: options.mapping.fill_merged_cells,
3363
+ excludeHiddenRows: options.mapping.exclude_hidden_rows,
2572
3364
  // The profile pass inside convert is internal (it writes profile.json); the ragged-row
2573
3365
  // warning is surfaced by the conversion pass below instead, so suppress it here.
2574
- warnRagged: false
3366
+ warnRagged: false,
3367
+ // The mapping's own columns are the key worth checking for repeats: they are what AE will
3368
+ // receive as identity, time, and event name. A file-wide `default_event_name` is the same on
3369
+ // every row, so it distinguishes nothing and is left out.
3370
+ duplicateKeyFields: [
3371
+ options.mapping.account_id_field ?? options.mapping.distinct_id_field,
3372
+ options.mapping.time.field,
3373
+ options.mapping.event_name_field
3374
+ ].filter((field) => typeof field === "string" && field.length > 0)
2575
3375
  };
2576
3376
  const salvageSet = options.salvageFrom ? readSalvageRowNumbers(options.salvageFrom) : void 0;
2577
3377
  let salvageMatched = 0;
@@ -2596,6 +3396,8 @@ async function convertLocalData(options) {
2596
3396
  const skippedFields = {};
2597
3397
  let lanIpRecords = 0;
2598
3398
  const flattenMisses = {};
3399
+ const cellIssues = /* @__PURE__ */ new Map();
3400
+ const xlsxStructure = input.format === "xlsx" ? createXlsxStructureCollector() : void 0;
2599
3401
  const rowCount = await streamLocalDataRows(
2600
3402
  input,
2601
3403
  dataSet,
@@ -2624,9 +3426,14 @@ async function convertLocalData(options) {
2624
3426
  },
2625
3427
  {
2626
3428
  headerNames: streamOptions.headerNames,
3429
+ skipRows: streamOptions.skipRows,
2627
3430
  flattenRules: streamOptions.flattenRules,
2628
3431
  flattenMisses,
2629
- mergeSheets: streamOptions.mergeSheets
3432
+ cellIssues,
3433
+ mergeSheets: streamOptions.mergeSheets,
3434
+ fillMergedCells: streamOptions.fillMergedCells,
3435
+ excludeHiddenRows: streamOptions.excludeHiddenRows,
3436
+ xlsxStructure
2630
3437
  }
2631
3438
  );
2632
3439
  if (userSetBuffer.length > 0) flushSortChunk(outputDir, userSetBuffer, sortChunks);
@@ -2635,6 +3442,17 @@ async function convertLocalData(options) {
2635
3442
  process.stderr.write(`Warning: flatten rule "${outColumn}" did not materialize for ${count} row(s).
2636
3443
  `);
2637
3444
  }
3445
+ for (const warning of cellIssueWarnings(cellIssues)) {
3446
+ process.stderr.write(`Warning: ${warning}
3447
+ `);
3448
+ }
3449
+ if (xlsxStructure) {
3450
+ for (const warning of xlsxStructureWarnings(xlsxStructure, options.mapping.exclude_columns)) {
3451
+ process.stderr.write(`Warning: ${warning}
3452
+ `);
3453
+ }
3454
+ }
3455
+ const structureReport = xlsxStructure ? xlsxStructureReport(xlsxStructure) : void 0;
2638
3456
  if (salvageSet && salvageMatched === 0) {
2639
3457
  throw new CliValidationError("The salvage file lists no rows from this source.", {
2640
3458
  code: "LOCAL_DATA_SALVAGE_NO_MATCH",
@@ -2656,6 +3474,7 @@ async function convertLocalData(options) {
2656
3474
  writeSecureJson(mappingPath, options.mapping);
2657
3475
  writeSecureText(transformPath, createTransformScript(options.inputFile, mappingPath));
2658
3476
  const validBytes = statSize(validPath);
3477
+ const sourceRows = salvageSet ? salvageMatched : rowCount;
2659
3478
  const blockedReasons = [
2660
3479
  ...rowCount === 0 ? ["The source contained no data rows."] : [],
2661
3480
  ...rowCount > 0 && validRecords === 0 ? ["No valid UE records were generated."] : [],
@@ -2677,13 +3496,18 @@ async function convertLocalData(options) {
2677
3496
  valid_file: basename4(validPath),
2678
3497
  valid_sha256: await sha256File(validPath),
2679
3498
  invalid_file: basename4(invalidPath),
3499
+ source_rows: sourceRows,
2680
3500
  valid_records: validRecords,
2681
3501
  invalid_records: invalidRecords,
2682
3502
  valid_bytes: validBytes,
2683
3503
  record_types: recordTypes,
2684
3504
  ...Object.keys(skippedFields).length > 0 ? { skipped_fields: skippedFields } : {},
2685
3505
  ...lanIpRecords > 0 ? { lan_ip_records: lanIpRecords } : {},
2686
- ...Object.keys(flattenMisses).length > 0 ? { flatten_misses: flattenMisses } : {}
3506
+ ...Object.keys(flattenMisses).length > 0 ? { flatten_misses: flattenMisses } : {},
3507
+ ...cellIssues.size > 0 ? { unreadable_cells: cellIssueCounts(cellIssues) } : {},
3508
+ ...structureReport ? { xlsx_structure: structureReport } : {},
3509
+ ...profile.summary_rows ? { summary_rows: profile.summary_rows } : {},
3510
+ ...profile.duplicate_keys ? { duplicate_keys: profile.duplicate_keys } : {}
2687
3511
  },
2688
3512
  blocked_reasons: blockedReasons
2689
3513
  };
@@ -2701,6 +3525,7 @@ async function convertLocalDataMulti(options) {
2701
3525
  const profile = await profileLocalData(input, dataSet, sourceTimezone, {
2702
3526
  collectSamples: true,
2703
3527
  headerNames: options.mapping.headers,
3528
+ skipRows: options.mapping.skip_rows,
2704
3529
  flattenRules: options.mapping.flatten_rules,
2705
3530
  warnRagged: false
2706
3531
  });
@@ -2772,7 +3597,7 @@ function convertRow(row, rowNumber, mapping, now) {
2772
3597
  if (recordType === "track") {
2773
3598
  const rawEvent = String(stripQuotes(mapping.event_name_field ? row[mapping.event_name_field] ?? "" : mapping.default_event_name ?? ""));
2774
3599
  eventName = mapping.value_mapping?.event_name && mapping.value_mapping.event_name[rawEvent] !== void 0 ? mapping.value_mapping.event_name[rawEvent] : rawEvent;
2775
- if (!isValidAeName(eventName)) {
3600
+ if (!isValidEventName(eventName)) {
2776
3601
  errors.push({ code: "INVALID_EVENT_NAME", field: mapping.event_name_field });
2777
3602
  }
2778
3603
  }
@@ -3619,13 +4444,13 @@ import { dirname as dirname2, join as join3, resolve as resolve3 } from "path";
3619
4444
  import { tmpdir } from "os";
3620
4445
 
3621
4446
  // src/commands/data-integration/archive.ts
3622
- import archiver from "archiver";
4447
+ import { ZipArchive } from "archiver";
3623
4448
  import { createWriteStream as createWriteStream2, readdirSync as readdirSync2, statSync as statSync4 } from "fs";
3624
4449
  import { join as join2, relative, sep } from "path";
3625
4450
  async function zipPackage(dir, zipPath) {
3626
4451
  await new Promise((resolvePromise, rejectPromise) => {
3627
4452
  const output = createWriteStream2(zipPath, { mode: 384 });
3628
- const zip = archiver("zip", { zlib: { level: 9 } });
4453
+ const zip = new ZipArchive({ zlib: { level: 9 } });
3629
4454
  output.on("close", resolvePromise);
3630
4455
  output.on("error", rejectPromise);
3631
4456
  zip.on("error", rejectPromise);