@forzalabs/remora 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8538,7 +8538,7 @@ var require_node2 = __commonJS({
8538
8538
  var require_tail_file = __commonJS({
8539
8539
  "../../node_modules/winston/lib/winston/tail-file.js"(exports2, module2) {
8540
8540
  "use strict";
8541
- var fs31 = require("fs");
8541
+ var fs33 = require("fs");
8542
8542
  var { StringDecoder } = require("string_decoder");
8543
8543
  var { Stream } = require_readable();
8544
8544
  function noop() {
@@ -8559,7 +8559,7 @@ var require_tail_file = __commonJS({
8559
8559
  stream.emit("end");
8560
8560
  stream.emit("close");
8561
8561
  };
8562
- fs31.open(options2.file, "a+", "0644", (err2, fd) => {
8562
+ fs33.open(options2.file, "a+", "0644", (err2, fd) => {
8563
8563
  if (err2) {
8564
8564
  if (!iter) {
8565
8565
  stream.emit("error", err2);
@@ -8571,10 +8571,10 @@ var require_tail_file = __commonJS({
8571
8571
  }
8572
8572
  (function read() {
8573
8573
  if (stream.destroyed) {
8574
- fs31.close(fd, noop);
8574
+ fs33.close(fd, noop);
8575
8575
  return;
8576
8576
  }
8577
- return fs31.read(fd, buffer, 0, buffer.length, pos, (error, bytes) => {
8577
+ return fs33.read(fd, buffer, 0, buffer.length, pos, (error, bytes) => {
8578
8578
  if (error) {
8579
8579
  if (!iter) {
8580
8580
  stream.emit("error", error);
@@ -8633,7 +8633,7 @@ var require_tail_file = __commonJS({
8633
8633
  var require_file = __commonJS({
8634
8634
  "../../node_modules/winston/lib/winston/transports/file.js"(exports2, module2) {
8635
8635
  "use strict";
8636
- var fs31 = require("fs");
8636
+ var fs33 = require("fs");
8637
8637
  var path22 = require("path");
8638
8638
  var asyncSeries = require_series();
8639
8639
  var zlib3 = require("zlib");
@@ -8838,7 +8838,7 @@ var require_file = __commonJS({
8838
8838
  let buff = "";
8839
8839
  let results = [];
8840
8840
  let row = 0;
8841
- const stream = fs31.createReadStream(file, {
8841
+ const stream = fs33.createReadStream(file, {
8842
8842
  encoding: "utf8"
8843
8843
  });
8844
8844
  stream.on("error", (err2) => {
@@ -8990,7 +8990,7 @@ var require_file = __commonJS({
8990
8990
  stat(callback) {
8991
8991
  const target = this._getFile();
8992
8992
  const fullpath = path22.join(this.dirname, target);
8993
- fs31.stat(fullpath, (err2, stat) => {
8993
+ fs33.stat(fullpath, (err2, stat) => {
8994
8994
  if (err2 && err2.code === "ENOENT") {
8995
8995
  debug("ENOENT\xA0ok", fullpath);
8996
8996
  this.filename = target;
@@ -9095,7 +9095,7 @@ var require_file = __commonJS({
9095
9095
  _createStream(source) {
9096
9096
  const fullpath = path22.join(this.dirname, this.filename);
9097
9097
  debug("create stream start", fullpath, this.options);
9098
- const dest = fs31.createWriteStream(fullpath, this.options).on("error", (err2) => debug(err2)).on("close", () => debug("close", dest.path, dest.bytesWritten)).on("open", () => {
9098
+ const dest = fs33.createWriteStream(fullpath, this.options).on("error", (err2) => debug(err2)).on("close", () => debug("close", dest.path, dest.bytesWritten)).on("open", () => {
9099
9099
  debug("file open ok", fullpath);
9100
9100
  this.emit("open", fullpath);
9101
9101
  source.pipe(dest);
@@ -9174,7 +9174,7 @@ var require_file = __commonJS({
9174
9174
  const isZipped = this.zippedArchive ? ".gz" : "";
9175
9175
  const filePath = `${basename}${isOldest}${ext}${isZipped}`;
9176
9176
  const target = path22.join(this.dirname, filePath);
9177
- fs31.unlink(target, callback);
9177
+ fs33.unlink(target, callback);
9178
9178
  }
9179
9179
  /**
9180
9180
  * Roll files forward based on integer, up to maxFiles. e.g. if base if
@@ -9197,17 +9197,17 @@ var require_file = __commonJS({
9197
9197
  tasks.push(function(i6, cb) {
9198
9198
  let fileName = `${basename}${i6 - 1}${ext}${isZipped}`;
9199
9199
  const tmppath = path22.join(this.dirname, fileName);
9200
- fs31.exists(tmppath, (exists) => {
9200
+ fs33.exists(tmppath, (exists) => {
9201
9201
  if (!exists) {
9202
9202
  return cb(null);
9203
9203
  }
9204
9204
  fileName = `${basename}${i6}${ext}${isZipped}`;
9205
- fs31.rename(tmppath, path22.join(this.dirname, fileName), cb);
9205
+ fs33.rename(tmppath, path22.join(this.dirname, fileName), cb);
9206
9206
  });
9207
9207
  }.bind(this, x2));
9208
9208
  }
9209
9209
  asyncSeries(tasks, () => {
9210
- fs31.rename(
9210
+ fs33.rename(
9211
9211
  path22.join(this.dirname, `${basename}${ext}${isZipped}`),
9212
9212
  path22.join(this.dirname, `${basename}1${ext}${isZipped}`),
9213
9213
  callback
@@ -9223,22 +9223,22 @@ var require_file = __commonJS({
9223
9223
  * @private
9224
9224
  */
9225
9225
  _compressFile(src, dest, callback) {
9226
- fs31.access(src, fs31.F_OK, (err2) => {
9226
+ fs33.access(src, fs33.F_OK, (err2) => {
9227
9227
  if (err2) {
9228
9228
  return callback();
9229
9229
  }
9230
9230
  var gzip = zlib3.createGzip();
9231
- var inp = fs31.createReadStream(src);
9232
- var out = fs31.createWriteStream(dest);
9231
+ var inp = fs33.createReadStream(src);
9232
+ var out = fs33.createWriteStream(dest);
9233
9233
  out.on("finish", () => {
9234
- fs31.unlink(src, callback);
9234
+ fs33.unlink(src, callback);
9235
9235
  });
9236
9236
  inp.pipe(gzip).pipe(out);
9237
9237
  });
9238
9238
  }
9239
9239
  _createLogDirIfNotExist(dirPath) {
9240
- if (!fs31.existsSync(dirPath)) {
9241
- fs31.mkdirSync(dirPath, { recursive: true });
9240
+ if (!fs33.existsSync(dirPath)) {
9241
+ fs33.mkdirSync(dirPath, { recursive: true });
9242
9242
  }
9243
9243
  }
9244
9244
  };
@@ -11809,7 +11809,7 @@ var CONSTANTS, Constants_default;
11809
11809
  var init_Constants = __esm({
11810
11810
  "../../packages/constants/src/Constants.ts"() {
11811
11811
  CONSTANTS = {
11812
- cliVersion: "2.3.0",
11812
+ cliVersion: "2.5.0",
11813
11813
  backendVersion: 1,
11814
11814
  backendPort: 5088,
11815
11815
  workerVersion: 2,
@@ -11934,11 +11934,36 @@ var init_Constants = __esm({
11934
11934
  * mode when its next line arrives — its byte counter lives in the bucket, not the stream.
11935
11935
  */
11936
11936
  MAX_OPEN_SPLIT_STREAMS: 64,
11937
+ /**
11938
+ * Record layouts one FIXED output may write into a single file, which is also how many write
11939
+ * streams its block-ordering pass holds open. A copybook of this size is already unusual —
11940
+ * the NASCO POD feed writes five transaction types — and the bound stops a generated config
11941
+ * from opening a descriptor per distinct value of the wrong field.
11942
+ */
11943
+ MAX_FIXED_LAYOUTS: 64,
11937
11944
  /**
11938
11945
  * Rows a sample may ask for out of the pipeline. A sample is a look at the data, never an
11939
11946
  * export of it — `remora sample -s` and the preview's Data tab are both held to this.
11940
11947
  */
11941
- MAX_SAMPLE_ROWS: 100
11948
+ MAX_SAMPLE_ROWS: 100,
11949
+ /**
11950
+ * Values held in memory while one parquet row group is encoded. A parquet writer buffers a
11951
+ * whole row group before it can compress and flush it, so this — not the dataset size — is
11952
+ * what bounds the export's peak memory. Budgeted in CELLS rather than rows because the two
11953
+ * differ by three orders of magnitude across consumers: 100,000 rows of a 4-column consumer
11954
+ * is 400k values, of a 1,300-field one it is 130 million.
11955
+ */
11956
+ PARQUET_ROW_GROUP_CELLS: 1e6,
11957
+ /**
11958
+ * Floor on the derived row count, so a consumer with hundreds of columns still writes row
11959
+ * groups big enough to compress and to read back without a seek per handful of rows.
11960
+ */
11961
+ PARQUET_MIN_ROW_GROUP_ROWS: 1e3,
11962
+ /**
11963
+ * Ceiling on the derived row count, so a one-column consumer does not buffer a million rows
11964
+ * to save a few row-group footers.
11965
+ */
11966
+ PARQUET_MAX_ROW_GROUP_ROWS: 1e5
11942
11967
  }
11943
11968
  };
11944
11969
  Constants_default = CONSTANTS;
@@ -12032,11 +12057,26 @@ var init_ExecutorScope = __esm({
12032
12057
  import_path2 = __toESM(require("path"), 1);
12033
12058
  import_fs2 = __toESM(require("fs"), 1);
12034
12059
  import_promises = __toESM(require("fs/promises"), 1);
12060
+ init_src();
12035
12061
  init_src3();
12036
12062
  ExecutorScopeClass = class {
12037
12063
  constructor() {
12038
12064
  this.WORKERS_FOLDER = "workers";
12039
12065
  this.PRODUCERS_FOLDER = "producers";
12066
+ /**
12067
+ * The scope folders THIS process opened and has not cleared. The only list `deepClear` may
12068
+ * delete from: two remora processes share one `.temp`, so a listing cannot tell a crashed run's
12069
+ * leftovers from another process's live run.
12070
+ */
12071
+ this._ownedScopes = /* @__PURE__ */ new Set();
12072
+ /** Registers a scope as this process's, so a crash on the way out clears it. */
12073
+ this.open = (scope) => {
12074
+ Affirm_default(scope?.folder, "Invalid executor scope");
12075
+ this._ownedScopes.add(scope.folder);
12076
+ return scope;
12077
+ };
12078
+ /** The scopes this process still holds. Empty once every run it started has cleaned up. */
12079
+ this.getOwnedScopes = () => [...this._ownedScopes];
12040
12080
  this.getBasePath = () => import_path2.default.join(Constants_default.defaults.REMORA_PATH, Constants_default.defaults.PRODUCER_TEMP_FOLDER);
12041
12081
  this.getWorkerPath = (scope, workerId) => {
12042
12082
  return import_path2.default.join(
@@ -12047,6 +12087,13 @@ var init_ExecutorScope = __esm({
12047
12087
  `${workerId}.dataset`
12048
12088
  );
12049
12089
  };
12090
+ /**
12091
+ * Where a producer's source is staged before any worker reads it: a CSV extract WITH a header,
12092
+ * written by whichever driver read the source.
12093
+ *
12094
+ * `.extract`, not `.dataset`: the two are different formats, and one extension meaning both is
12095
+ * how a reader ends up applying the internal positional rules to a file that follows none.
12096
+ */
12050
12097
  this.getProducerPath = (scope, producer, sourceFileKey) => {
12051
12098
  return import_path2.default.join(
12052
12099
  this.getBasePath(),
@@ -12054,7 +12101,7 @@ var init_ExecutorScope = __esm({
12054
12101
  scope.folder,
12055
12102
  this.PRODUCERS_FOLDER,
12056
12103
  producer.name,
12057
- `${sourceFileKey}.dataset`
12104
+ `${sourceFileKey}.extract`
12058
12105
  );
12059
12106
  };
12060
12107
  this.getMainPath = (scope) => {
@@ -12090,16 +12137,32 @@ var init_ExecutorScope = __esm({
12090
12137
  if (import_fs2.default.existsSync(scopePath)) {
12091
12138
  await import_promises.default.rm(scopePath, { recursive: true, force: true });
12092
12139
  }
12140
+ this._ownedScopes.delete(scope.folder);
12093
12141
  };
12142
+ /**
12143
+ * Clears what this process left open, on the way out. Synchronous: its one caller is the `exit`
12144
+ * handler. A run that finished cleared its own scope, so a clean exit finds nothing to do.
12145
+ */
12094
12146
  this.deepClear = () => {
12095
12147
  const basePath = this.getBasePath();
12096
- const openScopes = this.getOpenScopes();
12097
- for (const scopeFolder of openScopes) {
12148
+ for (const scopeFolder of this._ownedScopes) {
12098
12149
  const scopePath = import_path2.default.join(basePath, scopeFolder);
12099
12150
  if (import_fs2.default.existsSync(scopePath)) {
12100
12151
  import_fs2.default.rmSync(scopePath, { recursive: true, force: true });
12101
12152
  }
12102
12153
  }
12154
+ this._ownedScopes.clear();
12155
+ };
12156
+ /**
12157
+ * Every scope folder on disk, whoever opened it. Answers "what is in .temp?", not "what is mine
12158
+ * to delete" — nothing may be removed on the strength of it (see `getOwnedScopes`). The parallel
12159
+ * canary samples it to watch concurrent runs come and go.
12160
+ */
12161
+ this.getOpenScopes = () => {
12162
+ const basePath = this.getBasePath();
12163
+ if (!import_fs2.default.existsSync(basePath))
12164
+ return [];
12165
+ return import_fs2.default.readdirSync(basePath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((folder) => folder !== "logs" && folder !== "usage");
12103
12166
  };
12104
12167
  this.ensurePath = (fileUri) => {
12105
12168
  const dir = import_path2.default.dirname(fileUri);
@@ -12108,12 +12171,6 @@ var init_ExecutorScope = __esm({
12108
12171
  if (!import_fs2.default.existsSync(fileUri))
12109
12172
  import_fs2.default.writeFileSync(fileUri, "");
12110
12173
  };
12111
- this.getOpenScopes = () => {
12112
- const basePath = this.getBasePath();
12113
- if (!import_fs2.default.existsSync(basePath))
12114
- return [];
12115
- return import_fs2.default.readdirSync(basePath, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((folder) => folder !== "logs" && folder !== "usage");
12116
- };
12117
12174
  }
12118
12175
  };
12119
12176
  ExecutorScope = new ExecutorScopeClass();
@@ -12167,8 +12224,7 @@ var init_ProcessShutdownManager = __esm({
12167
12224
  if (this._cleaned)
12168
12225
  return;
12169
12226
  this._cleaned = true;
12170
- const openScopes = ExecutorScope_default.getOpenScopes();
12171
- const scopeCount = openScopes.length;
12227
+ const scopeCount = ExecutorScope_default.getOwnedScopes().length;
12172
12228
  const shutdownState = this._shutdownState;
12173
12229
  const shutdownDescription = `${shutdownState?.type ?? "intentional"} shutdown (${shutdownState?.reason ?? `exit:${code}`})`;
12174
12230
  if (scopeCount === 0) {
@@ -12579,7 +12635,7 @@ var init_ResourcesUtils = __esm({
12579
12635
  Affirm_default(subConsumer, `No producer found with name "${cProd.name}"`);
12580
12636
  return this.getConsumerOutputColumns(subConsumer);
12581
12637
  } else {
12582
- const dims = producer.dimensions.map((x2) => ({
12638
+ return producer.dimensions.map((x2) => ({
12583
12639
  consumerAlias: null,
12584
12640
  consumerKey: null,
12585
12641
  nameInProducer: x2.name,
@@ -12587,15 +12643,6 @@ var init_ResourcesUtils = __esm({
12587
12643
  dimension: x2,
12588
12644
  owner: cProd.name
12589
12645
  }));
12590
- const meas = producer.measures?.map((x2) => ({
12591
- consumerAlias: null,
12592
- consumerKey: null,
12593
- nameInProducer: x2.name,
12594
- aliasInProducer: x2.name,
12595
- measure: x2,
12596
- owner: cProd.name
12597
- })) ?? [];
12598
- return [...dims, ...meas];
12599
12646
  }
12600
12647
  });
12601
12648
  };
@@ -12655,7 +12702,6 @@ var init_ResourcesUtils = __esm({
12655
12702
  nameInProducer: col.nameInProducer,
12656
12703
  aliasInProducer: col.nameInProducer,
12657
12704
  dimension: col.dimension,
12658
- measure: col.measure,
12659
12705
  owner: consumer.name
12660
12706
  });
12661
12707
  }
@@ -12669,7 +12715,6 @@ var init_ResourcesUtils = __esm({
12669
12715
  nameInProducer: match2.name,
12670
12716
  aliasInProducer: match2.name,
12671
12717
  dimension: match2.column.dimension,
12672
- measure: match2.column.measure,
12673
12718
  owner: consumer.name
12674
12719
  });
12675
12720
  }
@@ -12683,7 +12728,6 @@ var init_ResourcesUtils = __esm({
12683
12728
  nameInProducer: finalKey,
12684
12729
  aliasInProducer: finalKey,
12685
12730
  dimension: match?.dimension ?? { name: finalKey, type: "string" },
12686
- measure: match?.measure,
12687
12731
  owner: consumer.name
12688
12732
  });
12689
12733
  }
@@ -13135,14 +13179,11 @@ var init_Validator = __esm({
13135
13179
  return errors2;
13136
13180
  };
13137
13181
  errors.push(...validateTransformations(consumer.fields));
13138
- const duplicatesOutputs = Algo_default.duplicatesObject(consumer.outputs, "format");
13139
- if (duplicatesOutputs.length > 0) {
13140
- const duplicatesTypes = Algo_default.uniq(duplicatesOutputs.map((x2) => x2.format));
13141
- errors.push(`There are outputs with the same type. (duplicates type: ${duplicatesTypes.join(" and ")})`);
13142
- }
13182
+ if ((consumer.outputs ?? []).length > 1)
13183
+ errors.push(`Consumer "${consumer.name}" declares ${consumer.outputs.length} outputs (${consumer.outputs.map((x2) => x2.format).join(", ")}). A consumer writes exactly one output \u2014 only the first was ever written \u2014 so split it into one consumer per output.`);
13143
13184
  for (const output of consumer.outputs) {
13144
13185
  const format4 = output.format.toUpperCase();
13145
- if (format4 === "CSV" || format4 === "JSON" || format4 === "PARQUET" || format4 === "TXT") {
13186
+ if (format4 === "CSV" || format4 === "FIXED" || format4 === "JSON" || format4 === "PARQUET" || format4 === "TXT") {
13146
13187
  if (!output.exportDestination)
13147
13188
  errors.push(`A static file output must have an export destination set (${format4})`);
13148
13189
  else if (!Environment_default.getSource(output.exportDestination))
@@ -13152,6 +13193,7 @@ var init_Validator = __esm({
13152
13193
  errors.push(...this.validateOmitNull(consumer));
13153
13194
  errors.push(...this.validateHeader(consumer));
13154
13195
  errors.push(...this.validateSplit(consumer));
13196
+ errors.push(...this.validateFixedOutput(consumer));
13155
13197
  const dimensionFields = consumer.fields.filter((x2) => x2.key !== "*" && !x2.fixed && !x2.copyFrom && !x2.lineCount);
13156
13198
  const dimensionKeys = dimensionFields.map((x2) => `${x2.from ?? "_default_"}::${x2.key}`);
13157
13199
  const duplicateDimensionKeys = dimensionKeys.filter((k7, i6) => dimensionKeys.indexOf(k7) !== i6);
@@ -13220,7 +13262,7 @@ var init_Validator = __esm({
13220
13262
  this.validateSplit = (consumer) => {
13221
13263
  const errors = [];
13222
13264
  const { split: split2 } = consumer;
13223
- const positionalFormats = ["CSV", "TXT", "PARQUET"];
13265
+ const positionalFormats = ["CSV", "FIXED", "TXT", "PARQUET"];
13224
13266
  if (!Algo_default.hasVal(split2)) return errors;
13225
13267
  if (Algo_default.hasVal(consumer.maximumFileSize))
13226
13268
  errors.push(`Consumer "${consumer.name}" sets both "maximumFileSize" and "split". They are the same cap: move the value to "split": { "maxFileSizeGb": ${consumer.maximumFileSize} } and remove "maximumFileSize".`);
@@ -13237,50 +13279,83 @@ var init_Validator = __esm({
13237
13279
  const missingKeys = fieldKeys.includes("*") ? [] : keys.filter((key) => !fieldKeys.includes(key));
13238
13280
  if (missingKeys.length > 0)
13239
13281
  errors.push(`"split.by" references key(s) "${missingKeys.join(", ")}" that are not present in the consumer "${consumer.name}".`);
13240
- const positional = (consumer.outputs ?? []).filter((x2) => positionalFormats.includes(x2.format.toUpperCase()));
13241
- if (positional.length === 0) return errors;
13282
+ const format4 = consumer.outputs?.[0]?.format.toUpperCase();
13283
+ if (!positionalFormats.includes(format4)) return errors;
13242
13284
  const hiddenKeys = keys.filter((key) => (consumer.fields ?? []).some((x2) => (x2.alias ?? x2.key) === key && x2.hidden));
13243
13285
  if (hiddenKeys.length > 0)
13244
- errors.push(`"split.by" of consumer "${consumer.name}" references hidden field(s) "${hiddenKeys.join(", ")}", which the ${positional.map((x2) => x2.format.toUpperCase()).join("/")} output does not write \u2014 a hidden field can't be split on in a positional format.`);
13286
+ errors.push(`"split.by" of consumer "${consumer.name}" references hidden field(s) "${hiddenKeys.join(", ")}", which the ${format4} output does not write \u2014 a hidden field can't be split on in a positional format.`);
13245
13287
  return errors;
13246
13288
  };
13247
13289
  /**
13248
- * Checks `omitNull`, which drops a record's null fields on export. It only means anything where a
13249
- * record carries its own field names, and it is applied when the record is serialized — one record
13250
- * shape for every output of the consumer — so a positional sibling output would silently lose its
13251
- * column alignment. Both cases are rejected rather than ignored.
13290
+ * Checks `omitNull`, which drops a record's null fields on export. It only means anything where
13291
+ * a record carries its own field names: leaving a positional column out shifts every column
13292
+ * after it, so a positional output declaring it is rejected rather than ignored.
13252
13293
  */
13253
13294
  this.validateOmitNull = (consumer) => {
13254
13295
  const errors = [];
13255
- const positionalFormats = ["CSV", "TXT", "PARQUET"];
13296
+ const positionalFormats = ["CSV", "FIXED", "TXT", "PARQUET"];
13256
13297
  for (const output of (consumer.outputs ?? []).filter((x2) => x2.omitNull === true)) {
13257
13298
  const format4 = output.format.toUpperCase();
13258
- if (positionalFormats.includes(format4)) {
13299
+ if (positionalFormats.includes(format4))
13259
13300
  errors.push(`The ${format4} output of consumer "${consumer.name}" declares "omitNull", which only applies to JSON/API output: a positional column can't be omitted from a row.`);
13260
- continue;
13261
- }
13262
- const positional = (consumer.outputs ?? []).filter((x2) => positionalFormats.includes(x2.format.toUpperCase()));
13263
- if (positional.length > 0)
13264
- errors.push(`The ${format4} output of consumer "${consumer.name}" declares "omitNull", but the consumer also writes ${positional.map((x2) => x2.format.toUpperCase()).join(" and ")}. Every output is serialized from one record, so omitting fields would break the positional one \u2014 split them into two consumers.`);
13265
13301
  }
13266
13302
  return errors;
13267
13303
  };
13268
13304
  /**
13269
13305
  * Checks `header`, which writes the field names as the first line of an exported file. A JSON
13270
- * record carries its own field names and an API response is not a file, so the flag means
13271
- * nothing there — rejected rather than ignored, so a config that asks for a header always gets
13272
- * one.
13306
+ * record carries its own field names, a parquet file carries its own schema, and an API response
13307
+ * is not a file, so the flag means nothing there — rejected rather than ignored, so a config that
13308
+ * asks for a header always gets one.
13273
13309
  */
13274
13310
  this.validateHeader = (consumer) => {
13275
13311
  const errors = [];
13276
- const positionalFormats = ["CSV", "TXT", "PARQUET"];
13312
+ const headerFormats = ["CSV", "TXT"];
13277
13313
  for (const output of (consumer.outputs ?? []).filter((x2) => Algo_default.hasVal(x2.header))) {
13278
13314
  const format4 = output.format.toUpperCase();
13279
- if (!positionalFormats.includes(format4))
13280
- errors.push(`The ${format4} output of consumer "${consumer.name}" declares "header", which only applies to CSV/TXT output: a ${format4} record already carries its own field names.`);
13315
+ if (!headerFormats.includes(format4))
13316
+ errors.push(`The ${format4} output of consumer "${consumer.name}" declares "header", which only applies to CSV/TXT output: ${this.headerlessReason(format4)}.`);
13281
13317
  }
13282
13318
  return errors;
13283
13319
  };
13320
+ /** Why the format in hand has no header line, as the clause that finishes the sentence above. */
13321
+ this.headerlessReason = (format4) => {
13322
+ if (format4 === "FIXED") return "a positional line has no room for one, and a field name would not sit on the columns it describes";
13323
+ if (format4 === "PARQUET") return `a ${format4} file carries its own schema`;
13324
+ return `a ${format4} record already carries its own field names`;
13325
+ };
13326
+ /**
13327
+ * Checks a consumer's FIXED output from its configuration alone: the properties only a
13328
+ * positional file has, the ones it cannot have, and whether anything downstream tries to read
13329
+ * one back. The layout itself — widths, columns and record lengths — is resolved against the
13330
+ * dataset's columns, which the producers define, so it is checked by `compile` and again before
13331
+ * a run starts rather than here.
13332
+ *
13333
+ * A property declared on the wrong format is rejected rather than ignored, the way a `position`
13334
+ * on a non-FIXED producer is: silently dropping a rule the author wrote is how a file ships
13335
+ * looking almost right.
13336
+ */
13337
+ this.validateFixedOutput = (consumer) => {
13338
+ Affirm_default(consumer, "Invalid consumer");
13339
+ const errors = [];
13340
+ const output = consumer.outputs?.[0];
13341
+ if (!output) return errors;
13342
+ const format4 = output.format?.toUpperCase();
13343
+ const positioned = (consumer.fields ?? []).filter((x2) => x2.position).map((x2) => `"${x2.alias ?? x2.key}"`);
13344
+ if (format4 !== "FIXED") {
13345
+ const fixedOnly = ["recordLength", "defaults", "lineEnding", "layouts"];
13346
+ const declared = fixedOnly.filter((name) => Algo_default.hasVal(output[name]));
13347
+ if (declared.length > 0)
13348
+ errors.push(`The ${format4} output of consumer "${consumer.name}" declares ${declared.map((x2) => `"${x2}"`).join(", ")}, which only ${declared.length > 1 ? "apply" : "applies"} to a FIXED output, where ${declared.length > 1 ? "they are" : "it is"} ignored.`);
13349
+ if (positioned.length > 0)
13350
+ errors.push(`Consumer "${consumer.name}" declares "position" on field(s) ${positioned.join(", ")} but its output format is not "FIXED", where positions are ignored.`);
13351
+ return errors;
13352
+ }
13353
+ if (Algo_default.hasVal(output.delimiter))
13354
+ errors.push(`The FIXED output of consumer "${consumer.name}" declares a "delimiter". A positional line has none: its fields are found by the columns they occupy.`);
13355
+ if ((consumer.fields ?? []).some((x2) => x2.key === "*"))
13356
+ errors.push(`Consumer "${consumer.name}" selects fields with the wildcard key "*", which a FIXED output can't write: every field of a positional line declares its own "width", so the field list has to be explicit.`);
13357
+ return errors;
13358
+ };
13284
13359
  /**
13285
13360
  * Checks a field whose key selects columns by pattern ("addr*Street"). A pattern that matches
13286
13361
  * nothing is always a mistake — a renamed dimension, or a producer that never had those columns —
@@ -14861,7 +14936,7 @@ A consumer takes the data from one or more producers and changes it's shape to t
14861
14936
  # RULES
14862
14937
  - If a field is not needed, do not add it e.g.
14863
14938
  - Only import a producer once
14864
- - Awlays include this exact property as the first -> $schema": "https://raw.githubusercontent.com/ForzaLabs/remora-public/refs/heads/main/json_schemas/consumer-schema.json",
14939
+ - Awlays include this exact property as the first -> "$schema": "https://raw.githubusercontent.com/ForzaLabs/remora-public/refs/heads/main/json_schemas/consumer-schema.json",
14865
14940
  - Use "API" as the only valid output format.
14866
14941
  - The "from" must contain only the name of the producer
14867
14942
 
@@ -33669,6 +33744,96 @@ var init_CSVParser = __esm({
33669
33744
  }
33670
33745
  });
33671
33746
 
33747
+ // ../../packages/parsing/src/DatasetCodec.ts
33748
+ var NULL_FIELD, NEEDS_ESCAPE, DatasetCodecClass, DatasetCodec, DatasetCodec_default;
33749
+ var init_DatasetCodec = __esm({
33750
+ "../../packages/parsing/src/DatasetCodec.ts"() {
33751
+ init_src();
33752
+ NULL_FIELD = "";
33753
+ NEEDS_ESCAPE = /["\n\r]/;
33754
+ DatasetCodecClass = class {
33755
+ constructor() {
33756
+ /**
33757
+ * One value as an internal positional field: quoted, with `"`, `\n` and `\r` escaped - or the
33758
+ * unquoted null marker when there is no value.
33759
+ */
33760
+ this.encodeField = (value) => {
33761
+ if (value === null || value === void 0) return NULL_FIELD;
33762
+ const text = typeof value === "string" ? value : String(value);
33763
+ if (!NEEDS_ESCAPE.test(text)) return `"${text}"`;
33764
+ return `"${text.replaceAll('"', '""').replaceAll("\r", "\\r").replaceAll("\n", "\\n")}"`;
33765
+ };
33766
+ /** One record as an internal positional line. The values are already in the schema's order. */
33767
+ this.encodeRow = (values, delimiter = ",") => {
33768
+ Affirm_default(values, "Invalid values");
33769
+ Affirm_default(delimiter, "Invalid delimiter");
33770
+ let row = "";
33771
+ for (let index = 0; index < values.length; index++) {
33772
+ if (index > 0)
33773
+ row += delimiter;
33774
+ row += this.encodeField(values[index]);
33775
+ }
33776
+ return row;
33777
+ };
33778
+ /**
33779
+ * One internal positional line split back into its raw fields, with a null still a null. The
33780
+ * array may be SHORTER than the schema's column list; a reader treats a missing entry as null.
33781
+ */
33782
+ this.decodeRow = (line, delimiter = ",") => {
33783
+ Affirm_default(delimiter, "Invalid delimiter");
33784
+ const fields = [];
33785
+ const length = line?.length ?? 0;
33786
+ const delimiterCode = delimiter.charCodeAt(0);
33787
+ let fieldStart = 0;
33788
+ let fieldEnd = 0;
33789
+ let inQuotes = false;
33790
+ let hasQuotes = false;
33791
+ let index = 0;
33792
+ while (index < length) {
33793
+ const char = line.charCodeAt(index);
33794
+ if (char === 34) {
33795
+ if (!inQuotes) {
33796
+ inQuotes = true;
33797
+ hasQuotes = true;
33798
+ fieldStart = index + 1;
33799
+ } else if (line.charCodeAt(index + 1) === 34) {
33800
+ index++;
33801
+ } else {
33802
+ inQuotes = false;
33803
+ fieldEnd = index;
33804
+ }
33805
+ } else if (char === delimiterCode && !inQuotes) {
33806
+ fields.push(this._field(line, fieldStart, fieldEnd, hasQuotes));
33807
+ fieldStart = index + 1;
33808
+ fieldEnd = 0;
33809
+ hasQuotes = false;
33810
+ }
33811
+ index++;
33812
+ }
33813
+ fields.push(this._field(line, fieldStart, fieldEnd, hasQuotes));
33814
+ return fields;
33815
+ };
33816
+ /**
33817
+ * Re-encodes an internal line with another delimiter, which is what a `TXT` export is. Not via
33818
+ * `CSVParser`: a plain parse reads both the null marker and `""` as the empty string.
33819
+ */
33820
+ this.redelimit = (line, delimiter) => {
33821
+ Affirm_default(delimiter, "Invalid delimiter");
33822
+ return this.encodeRow(this.decodeRow(line), delimiter);
33823
+ };
33824
+ /** One field's text, or null for the unquoted marker. Only a quoted token carries a value. */
33825
+ this._field = (line, start, end, hasQuotes) => {
33826
+ if (!hasQuotes) return null;
33827
+ const raw = line.slice(start, end);
33828
+ return raw.indexOf('"') === -1 ? raw : raw.replaceAll('""', '"');
33829
+ };
33830
+ }
33831
+ };
33832
+ DatasetCodec = new DatasetCodecClass();
33833
+ DatasetCodec_default = DatasetCodec;
33834
+ }
33835
+ });
33836
+
33672
33837
  // ../../packages/parsing/src/FixedWidthParser.ts
33673
33838
  var FixedWidthParserClass, FixedWidthParser, FixedWidthParser_default;
33674
33839
  var init_FixedWidthParser = __esm({
@@ -33823,6 +33988,307 @@ var init_FixedWidthParser = __esm({
33823
33988
  }
33824
33989
  });
33825
33990
 
33991
+ // ../../packages/parsing/src/FixedWidthWriter.ts
33992
+ var BASE_FILL, TYPE_FILL, FixedWidthWriterClass, FixedWidthWriter, FixedWidthWriter_default;
33993
+ var init_FixedWidthWriter = __esm({
33994
+ "../../packages/parsing/src/FixedWidthWriter.ts"() {
33995
+ init_src();
33996
+ init_src3();
33997
+ BASE_FILL = { align: "left", pad: " ", nullFill: " ", overflow: "fail" };
33998
+ TYPE_FILL = {
33999
+ string: BASE_FILL,
34000
+ number: { ...BASE_FILL, align: "right", pad: "0" },
34001
+ datetime: BASE_FILL,
34002
+ boolean: BASE_FILL
34003
+ };
34004
+ FixedWidthWriterClass = class {
34005
+ constructor() {
34006
+ /**
34007
+ * Resolves one record layout for writing: every column's fill rules decided, its columns
34008
+ * computed, and the record length agreed with what the output declares.
34009
+ *
34010
+ * Called once per layout when an export starts, never per record. `columns` is the line as the
34011
+ * dataset schema has it — line order, visible fields only — and `positions` carries each one's
34012
+ * `position` block, keyed by the same name the column has.
34013
+ *
34014
+ * `context` is the phrase that names this layout in an error, e.g. `the FIXED output of consumer
34015
+ * "x"`, so a message reads as one sentence wherever the layout came from.
34016
+ */
34017
+ this.compilePlan = (columns, positions, output, context = "the FIXED output") => {
34018
+ Affirm_default(output, "Invalid consumer output");
34019
+ const selected = columns?.map((column, index) => ({ column, index }));
34020
+ return this.plan(selected, positions, output.defaults, output.recordLength, context);
34021
+ };
34022
+ /**
34023
+ * A FIXED output resolved for INSPECTION rather than for writing: every problem with it, and the
34024
+ * layouts it would write. What `compile` reports and prints.
34025
+ *
34026
+ * The rules are not restated here. The layouts are resolved by the same code the export runs,
34027
+ * and what that throws is reported rather than raised — so a layout `compile` accepts is a
34028
+ * layout the export can write. What review adds is that every field's own declaration is
34029
+ * checked first, across the whole output: an author with six fields missing a width sees six
34030
+ * lines from one `compile`, not one line from each of six.
34031
+ *
34032
+ * A field named by a layout that the dataset has no column for is left to the resolution below,
34033
+ * which names it as the layout's error rather than as a field with no width.
34034
+ */
34035
+ this.review = (dsSchema, positions, output, consumerName) => {
34036
+ Affirm_default(dsSchema, "Invalid dataset schema");
34037
+ Affirm_default(output, "Invalid consumer output");
34038
+ const context = `the FIXED output${consumerName ? ` of consumer "${consumerName}"` : ""}`;
34039
+ const declared = this.declarationErrors(dsSchema, positions, output, context);
34040
+ if (declared.length > 0) return { errors: declared, layouts: [] };
34041
+ try {
34042
+ if (!output.layouts)
34043
+ return { errors: [], layouts: [{ value: null, plan: this.compilePlan(dsSchema.columns, positions, output, context) }] };
34044
+ const layouts = this.compileLayouts(dsSchema, positions, output, consumerName);
34045
+ return { errors: [], layouts: layouts.order.map((value) => ({ value, plan: layouts.plans.get(value) })) };
34046
+ } catch (e7) {
34047
+ return { errors: [this.reason(e7)], layouts: [] };
34048
+ }
34049
+ };
34050
+ /** Every field's own declaration, over every field the output writes, before any offset is computed. */
34051
+ this.declarationErrors = (dsSchema, positions, output, context) => {
34052
+ const written = output.layouts ? [...new Set((output.layouts.order ?? []).flatMap((value) => output.layouts.values?.[value]?.fields ?? []))] : dsSchema.columns.map((column) => column.name);
34053
+ const errors = [];
34054
+ for (const name of written) {
34055
+ const column = dsSchema.columns.find((x2) => x2.name === name);
34056
+ if (column) errors.push(...this.fieldErrors(column, positions?.[name], output.defaults, context));
34057
+ }
34058
+ return errors;
34059
+ };
34060
+ /**
34061
+ * What an affirm said, as a sentence a report can start a line with: without the prefix it
34062
+ * carries for a log, and with its first letter upper-cased — the messages are written to be
34063
+ * embedded ("... of the FIXED output of consumer x ..."), so some begin with that context
34064
+ * rather than with a capital.
34065
+ */
34066
+ this.reason = (error) => {
34067
+ const prefix2 = "Affirm failed: ";
34068
+ const message = error instanceof Error ? error.message : String(error);
34069
+ const sentence = message.startsWith(prefix2) ? message.slice(prefix2.length) : message;
34070
+ return sentence.charAt(0).toUpperCase() + sentence.slice(1);
34071
+ };
34072
+ /**
34073
+ * The record layouts of a multi-layout FIXED output, compiled in `order` so the export writes
34074
+ * its blocks in the order the author declared.
34075
+ *
34076
+ * Each layout names a subset of the dataset's columns, in its own column order, so a field's
34077
+ * place on the written line has nothing to do with its place on the staged one — which is what
34078
+ * `IFixedWriteField.index` carries. The output's `defaults` stand under every layout's own.
34079
+ */
34080
+ this.compileLayouts = (dsSchema, positions, output, consumerName) => {
34081
+ Affirm_default(dsSchema, "Invalid dataset schema");
34082
+ Affirm_default(output?.layouts, "Invalid fixed-width layouts");
34083
+ const { discriminator, order, values } = output.layouts;
34084
+ const where = consumerName ? ` of consumer "${consumerName}"` : "";
34085
+ Affirm_default(discriminator, `The FIXED layouts${where} name no discriminator field`);
34086
+ Affirm_default.hasItems(order, `The FIXED layouts${where} declare no "order", which is the order their record blocks are written in`);
34087
+ Affirm_default.noDupes(order, `The FIXED layouts${where} name a discriminator value twice in "order"`);
34088
+ Affirm_default(
34089
+ order.length <= Constants_default.defaults.MAX_FIXED_LAYOUTS,
34090
+ `The FIXED layouts${where} declare ${order.length} record layouts, more than the ${Constants_default.defaults.MAX_FIXED_LAYOUTS} one output may write`
34091
+ );
34092
+ Affirm_default(
34093
+ output.recordLength === void 0 || output.recordLength === null,
34094
+ `The FIXED output${where} declares both "layouts" and a "recordLength". Each layout declares its own; the output has no single one.`
34095
+ );
34096
+ const index = dsSchema.columns.findIndex((column) => column.name === discriminator);
34097
+ Affirm_default(index >= 0, `The FIXED discriminator "${discriminator}"${where} is not one of the consumer's visible fields`);
34098
+ const plans = /* @__PURE__ */ new Map();
34099
+ for (const value of order)
34100
+ plans.set(value, this.layoutPlan(values?.[value], dsSchema, positions, output, value, consumerName));
34101
+ return { discriminator, index, order, plans };
34102
+ };
34103
+ /** One entry of the `layouts` map as a plan: its named fields, found on the staged line, in its order. */
34104
+ this.layoutPlan = (layout, dsSchema, positions, output, value, consumerName) => {
34105
+ const of = consumerName ? ` of consumer "${consumerName}"` : "";
34106
+ const where = `the FIXED layout "${value}"${of}`;
34107
+ Affirm_default(layout, `The FIXED "order"${of} names the discriminator value "${value}", which has no layout under "values"`);
34108
+ Affirm_default.hasItems(layout.fields, `${where} writes no field`);
34109
+ Affirm_default.noDupes(layout.fields, `${where} writes a field twice`);
34110
+ const selected = layout.fields.map((name) => {
34111
+ const index = dsSchema.columns.findIndex((column) => column.name === name);
34112
+ Affirm_default(index >= 0, `The field "${name}" of ${where} is not one of the consumer's visible fields`);
34113
+ return { column: dsSchema.columns[index], index };
34114
+ });
34115
+ return this.plan(selected, positions, this.mergeDefaults(output.defaults, layout.defaults), layout.recordLength, where);
34116
+ };
34117
+ /**
34118
+ * One record layout ready to write: every column's fill rules decided, its columns computed from
34119
+ * the widths before it, and the record length agreed with what the layout declares.
34120
+ */
34121
+ this.plan = (selected, positions, defaults, recordLength, context) => {
34122
+ Affirm_default.hasItems(selected, `${context} has no field to write`);
34123
+ Affirm_default(positions, "Invalid fixed-width field positions");
34124
+ const fields = [];
34125
+ let start = 1;
34126
+ for (const { column, index } of selected) {
34127
+ const field = this.resolveField(column, positions[column.name], defaults, start, context);
34128
+ fields.push({ ...field, index });
34129
+ start = field.end + 1;
34130
+ }
34131
+ const composed = start - 1;
34132
+ Affirm_default(
34133
+ recordLength === void 0 || recordLength === null || recordLength === composed,
34134
+ `${context} declares a recordLength of ${recordLength}, but its ${fields.length} field widths sum to ${composed}. A field, a filler or a width is missing.`
34135
+ );
34136
+ return { fields, recordLength: composed };
34137
+ };
34138
+ /**
34139
+ * The layout's per-type fill rules over the output's, type by type — so an output states a rule
34140
+ * once for the whole file and a layout only names where it differs.
34141
+ */
34142
+ this.mergeDefaults = (output, layout) => {
34143
+ if (!layout) return output;
34144
+ if (!output) return layout;
34145
+ const merged = { ...output };
34146
+ for (const type of Object.keys(layout))
34147
+ merged[type] = this.override(output[type] ?? {}, layout[type]);
34148
+ return merged;
34149
+ };
34150
+ /**
34151
+ * The layout a record is written with, by the value of the discriminator field on its staged
34152
+ * line. A value with no layout stops the run: it is a record the file has no block for, and
34153
+ * writing it under another layout would put its fields at the wrong columns.
34154
+ */
34155
+ this.planFor = (values, layouts) => {
34156
+ const value = values?.[layouts.index];
34157
+ const plan = value === null || value === void 0 ? null : layouts.plans.get(String(value));
34158
+ Affirm_default(plan, `No FIXED layout for the value ${value === null || value === void 0 ? "null" : `"${value}"`} of the discriminator field "${layouts.discriminator}". The layouts cover ${layouts.order.map((x2) => `"${x2}"`).join(", ")}.`);
34159
+ return plan;
34160
+ };
34161
+ /**
34162
+ * Everything wrong with the way ONE field is declared, as messages rather than as a throw: no
34163
+ * `position` at all, a width that is not a whole number of columns, a fill character that is not
34164
+ * one character.
34165
+ *
34166
+ * Not the field's place on the line — that is only meaningful once every field before it stands
34167
+ * on its own, so `resolveField` checks it where it computes it. This is the one statement of the
34168
+ * rules: the export affirms them, and `review` collects them for every field of an output so
34169
+ * `compile` reports six missing widths in one run rather than one per run.
34170
+ */
34171
+ this.fieldErrors = (column, position, defaults, context) => {
34172
+ Affirm_default(column, "Invalid dataset column");
34173
+ const where = `field "${column.name}"${context ? ` of ${context}` : ""}`;
34174
+ if (!position)
34175
+ return [`Missing "position" on ${where}: every visible field of a FIXED output declares the width it occupies`];
34176
+ const errors = [];
34177
+ if (!Number.isInteger(position.width) || position.width < 1)
34178
+ errors.push(`"position.width" of ${where} must be a whole number of 1 or greater`);
34179
+ const fill2 = this.fillOf(column.type, position, defaults);
34180
+ if (fill2.pad.length !== 1)
34181
+ errors.push(`"position.pad" of ${where} must be a single character, not "${fill2.pad}"`);
34182
+ if (fill2.nullFill.length !== 1)
34183
+ errors.push(`"position.nullFill" of ${where} must be a single character, not "${fill2.nullFill}"`);
34184
+ return errors;
34185
+ };
34186
+ /**
34187
+ * One column with its fill rules resolved and its place on the line computed. The field's own
34188
+ * `position` wins over the output's `defaults` for its type, which wins over the type's own rule,
34189
+ * so a wide layout states a rule once and names only its exceptions.
34190
+ */
34191
+ this.resolveField = (column, position, defaults, start, context) => {
34192
+ const declaration = this.fieldErrors(column, position, defaults, context);
34193
+ Affirm_default(declaration.length === 0, declaration.join(" "));
34194
+ const where = `field "${column.name}"${context ? ` of ${context}` : ""}`;
34195
+ const fill2 = this.fillOf(column.type, position, defaults);
34196
+ const end = start + position.width - 1;
34197
+ Affirm_default(
34198
+ position.start === void 0 || position.start === null || position.start === start,
34199
+ `"position.start" of ${where} says column ${position.start}, but the widths before it place the field at column ${start}. A field or a filler is missing, or two are out of order.`
34200
+ );
34201
+ return {
34202
+ name: column.name,
34203
+ width: position.width,
34204
+ start,
34205
+ end,
34206
+ align: fill2.align,
34207
+ pad: fill2.pad,
34208
+ nullText: fill2.nullFill.repeat(position.width),
34209
+ overflow: fill2.overflow
34210
+ };
34211
+ };
34212
+ /** The three fill sources folded into one, nearest declaration first. */
34213
+ this.fillOf = (type, position, defaults) => {
34214
+ const base = TYPE_FILL[type] ?? TYPE_FILL.string;
34215
+ return this.override(this.override(base, defaults?.[type]), position);
34216
+ };
34217
+ /** `fill` with whatever `override` actually declares. An absent rule leaves the one below it standing. */
34218
+ this.override = (fill2, override) => {
34219
+ if (!override) return fill2;
34220
+ const merged = { ...fill2 };
34221
+ if (override.align) merged.align = override.align;
34222
+ if (override.pad) merged.pad = override.pad;
34223
+ if (override.nullFill) merged.nullFill = override.nullFill;
34224
+ if (override.overflow) merged.overflow = override.overflow;
34225
+ return merged;
34226
+ };
34227
+ /**
34228
+ * One record as its positional line, with no delimiter, no quoting and no terminator. The values
34229
+ * are the record's fields as the STAGED line holds them, each plan field reading the one it was
34230
+ * compiled against; a value the array does not reach is written as a null, which is what a
34231
+ * staged line does when its last fields carry nothing.
34232
+ */
34233
+ this.composeLine = (values, plan) => {
34234
+ Affirm_default(values, "Invalid record values");
34235
+ Affirm_default(plan, "Invalid fixed-width write plan");
34236
+ let line = "";
34237
+ for (const field of plan.fields)
34238
+ line += this.writeField(values[field.index], field);
34239
+ Affirm_default(line.length === plan.recordLength, `A fixed-width record composed to ${line.length} characters, not the declared recordLength of ${plan.recordLength}`);
34240
+ return line;
34241
+ };
34242
+ /**
34243
+ * One value as one field: its text padded out to the field's width on the side `align` names.
34244
+ *
34245
+ * A null is the full width in `nullFill`, and an EMPTY STRING is a null — the staged encoding
34246
+ * keeps the two apart and a positional line cannot. A value too long for its field stops the
34247
+ * run; see `overflowed`.
34248
+ */
34249
+ this.writeField = (value, field) => {
34250
+ if (value === null || value === void 0) return field.nullText;
34251
+ const text = typeof value === "string" ? value : String(value);
34252
+ if (text.length === 0) return field.nullText;
34253
+ if (text.length > field.width) return this.overflowed(text, field);
34254
+ return field.align === "right" ? text.padStart(field.width, field.pad) : text.padEnd(field.width, field.pad);
34255
+ };
34256
+ /**
34257
+ * A value longer than its field. It fails the run by default: a truncated account number is a
34258
+ * plausible-looking account number, and silent corruption in a feed nobody re-reads is the one
34259
+ * failure mode a positional writer must not have.
34260
+ *
34261
+ * Where the layout genuinely specifies a cut, the end that survives follows `align`, which is
34262
+ * what a COBOL MOVE does: a left-aligned value keeps its head, a right-aligned one keeps its
34263
+ * low-order tail.
34264
+ */
34265
+ this.overflowed = (text, field) => {
34266
+ Affirm_default(
34267
+ field.overflow === "truncate",
34268
+ `Value "${text}" is ${text.length} characters, longer than the ${field.width} of field "${field.name}" at columns ${field.start}-${field.end}. Widen the field, or set its "overflow" to "truncate" if the layout really specifies a cut.`
34269
+ );
34270
+ return field.align === "right" ? text.slice(text.length - field.width) : text.slice(0, field.width);
34271
+ };
34272
+ /**
34273
+ * What a record carries after its last field, BEFORE the newline the destination writes. Every
34274
+ * destination driver ends each transformed line with one `\n`, the last line included, so an
34275
+ * `lf` output needs nothing here and a `crlf` one needs the `\r` that turns that newline into a
34276
+ * CRLF — which is exactly what a CRLF file looks like to anything that splits on `\n`.
34277
+ *
34278
+ * A real positional feed picks one terminator and holds it: the NASCO POD files are CRLF on
34279
+ * every line including the last, and `lf` is the default everywhere else.
34280
+ */
34281
+ this.lineSuffix = (output) => {
34282
+ Affirm_default(output, "Invalid consumer output");
34283
+ return output.lineEnding === "crlf" ? "\r" : "";
34284
+ };
34285
+ }
34286
+ };
34287
+ FixedWidthWriter = new FixedWidthWriterClass();
34288
+ FixedWidthWriter_default = FixedWidthWriter;
34289
+ }
34290
+ });
34291
+
33826
34292
  // ../../node_modules/pend/index.js
33827
34293
  var require_pend = __commonJS({
33828
34294
  "../../node_modules/pend/index.js"(exports2, module2) {
@@ -33881,7 +34347,7 @@ var require_pend = __commonJS({
33881
34347
  // ../../node_modules/yauzl/fd-slicer.js
33882
34348
  var require_fd_slicer = __commonJS({
33883
34349
  "../../node_modules/yauzl/fd-slicer.js"(exports2) {
33884
- var fs31 = require("fs");
34350
+ var fs33 = require("fs");
33885
34351
  var util = require("util");
33886
34352
  var stream = require("stream");
33887
34353
  var Readable10 = stream.Readable;
@@ -33901,7 +34367,7 @@ var require_fd_slicer = __commonJS({
33901
34367
  FdSlicer.prototype.read = function(buffer, offset, length, position, callback) {
33902
34368
  var self2 = this;
33903
34369
  self2.pend.go(function(cb) {
33904
- fs31.read(self2.fd, buffer, offset, length, position, function(err2, bytesRead, buffer2) {
34370
+ fs33.read(self2.fd, buffer, offset, length, position, function(err2, bytesRead, buffer2) {
33905
34371
  cb();
33906
34372
  callback(err2, bytesRead, buffer2);
33907
34373
  });
@@ -33918,7 +34384,7 @@ var require_fd_slicer = __commonJS({
33918
34384
  self2.refCount -= 1;
33919
34385
  if (self2.refCount < 0) throw new Error("invalid unref");
33920
34386
  if (self2.refCount > 0) return;
33921
- fs31.close(self2.fd, onCloseDone);
34387
+ fs33.close(self2.fd, onCloseDone);
33922
34388
  function onCloseDone(err2) {
33923
34389
  if (err2) {
33924
34390
  self2.emit("error", err2);
@@ -33950,7 +34416,7 @@ var require_fd_slicer = __commonJS({
33950
34416
  }
33951
34417
  self2.context.pend.go(function(cb) {
33952
34418
  var buffer = Buffer.allocUnsafe(toRead);
33953
- fs31.read(self2.context.fd, buffer, 0, toRead, self2.pos, function(err2, bytesRead) {
34419
+ fs33.read(self2.context.fd, buffer, 0, toRead, self2.pos, function(err2, bytesRead) {
33954
34420
  if (err2) {
33955
34421
  self2.destroy(err2);
33956
34422
  } else if (bytesRead === 0) {
@@ -34310,7 +34776,7 @@ var require_crc32 = __commonJS({
34310
34776
  // ../../node_modules/yauzl/index.js
34311
34777
  var require_yauzl = __commonJS({
34312
34778
  "../../node_modules/yauzl/index.js"(exports2) {
34313
- var fs31 = require("fs");
34779
+ var fs33 = require("fs");
34314
34780
  var zlib3 = require("zlib");
34315
34781
  var fd_slicer = require_fd_slicer();
34316
34782
  var util = require("util");
@@ -34379,10 +34845,10 @@ var require_yauzl = __commonJS({
34379
34845
  if (options2.validateEntrySizes == null) options2.validateEntrySizes = true;
34380
34846
  if (options2.strictFileNames == null) options2.strictFileNames = false;
34381
34847
  if (callback == null) callback = defaultCallback;
34382
- fs31.open(path22, "r", function(err2, fd) {
34848
+ fs33.open(path22, "r", function(err2, fd) {
34383
34849
  if (err2) return callback(err2);
34384
34850
  fromFd(fd, options2, function(err3, zipfile) {
34385
- if (err3) fs31.close(fd, defaultCallback);
34851
+ if (err3) fs33.close(fd, defaultCallback);
34386
34852
  callback(err3, zipfile);
34387
34853
  });
34388
34854
  });
@@ -34399,7 +34865,7 @@ var require_yauzl = __commonJS({
34399
34865
  if (options2.validateEntrySizes == null) options2.validateEntrySizes = true;
34400
34866
  if (options2.strictFileNames == null) options2.strictFileNames = false;
34401
34867
  if (callback == null) callback = defaultCallback;
34402
- fs31.fstat(fd, function(err2, stats) {
34868
+ fs33.fstat(fd, function(err2, stats) {
34403
34869
  if (err2) return callback(err2);
34404
34870
  var reader = new fd_slicer.FdSlicer(fd);
34405
34871
  fromRandomAccessReader(reader, stats.size, options2, callback);
@@ -38478,7 +38944,9 @@ var init_XMLParser = __esm({
38478
38944
  var init_src5 = __esm({
38479
38945
  "../../packages/parsing/src/index.ts"() {
38480
38946
  init_CSVParser();
38947
+ init_DatasetCodec();
38481
38948
  init_FixedWidthParser();
38949
+ init_FixedWidthWriter();
38482
38950
  init_ParseCompression();
38483
38951
  init_ParseHelper();
38484
38952
  init_XLSParser();
@@ -46114,8 +46582,8 @@ var init_LocalSourceDriver = __esm({
46114
46582
  Logger_default.log(`Matched ${allFileKeys.length} files, copying to locally and creating unified dataset.`);
46115
46583
  const firstPath = import_path10.default.join(this._path, allFileKeys[0]);
46116
46584
  const headerLine = (await DriverHelper_default.quickReadFile(firstPath, 1))[0];
46117
- const promises2 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, headerLine, i6));
46118
- const results = await Promise.all(promises2);
46585
+ const promises3 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, headerLine, i6));
46586
+ const results = await Promise.all(promises3);
46119
46587
  return results.flat();
46120
46588
  } else {
46121
46589
  return await this._get(request, "");
@@ -46135,8 +46603,8 @@ var init_LocalSourceDriver = __esm({
46135
46603
  Logger_default.log(`Matched ${allFileKeys.length} files, copying to locally and creating unified dataset.`);
46136
46604
  const firstPath = import_path10.default.join(this._path, allFileKeys[0]);
46137
46605
  const headerLine = (await DriverHelper_default.quickReadFile(firstPath, 1))[0];
46138
- const promises2 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, headerLine, i6));
46139
- const results = await Promise.all(promises2);
46606
+ const promises3 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, headerLine, i6));
46607
+ const results = await Promise.all(promises3);
46140
46608
  return results.flat();
46141
46609
  } else {
46142
46610
  return await this._get(request, "");
@@ -46564,8 +47032,8 @@ var init_S3SourceDriver = __esm({
46564
47032
  if (fileKey.includes("%")) {
46565
47033
  const allFileKeys = await this.listFiles(fileKey);
46566
47034
  Affirm_default(allFileKeys.length < 50, `Pattern ${fileKey} of producer requested to S3 matches more than 50 files (${allFileKeys.length}), this is more than the S3 allowed limit. Please refine your pattern, remove some files or use a separate bucket.`);
46567
- const promises2 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, i6));
46568
- const results = await Promise.all(promises2);
47035
+ const promises3 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, i6));
47036
+ const results = await Promise.all(promises3);
46569
47037
  return results.flat();
46570
47038
  } else {
46571
47039
  return await this._get(request);
@@ -46578,8 +47046,8 @@ var init_S3SourceDriver = __esm({
46578
47046
  const { fileKey } = request;
46579
47047
  if (fileKey.includes("%")) {
46580
47048
  const allFileKeys = await this.listFiles(fileKey);
46581
- const promises2 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, i6));
46582
- const results = await Promise.all(promises2);
47049
+ const promises3 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, i6));
47050
+ const results = await Promise.all(promises3);
46583
47051
  return results.flat();
46584
47052
  } else {
46585
47053
  return await this._get(request);
@@ -47526,7 +47994,7 @@ var init_ProducerManager = __esm({
47526
47994
  });
47527
47995
 
47528
47996
  // ../../packages/engines/src/transform/TypeCaster.ts
47529
- var import_dayjs, import_customParseFormat, import_utc, DAY_OF_YEAR_TOKEN, FORMAT_ONLY_TOKENS, DATE_PARSE_TOKENS, TYPE_DEFAULTS, TypeCasterClass, TypeCaster, TypeCaster_default;
47997
+ var import_dayjs, import_customParseFormat, import_utc, DAY_OF_YEAR_TOKEN, FORMAT_ONLY_TOKENS, DATE_PARSE_TOKENS, TRUE_VALUES, TYPE_DEFAULTS, TypeCasterClass, TypeCaster, TypeCaster_default;
47530
47998
  var init_TypeCaster = __esm({
47531
47999
  "../../packages/engines/src/transform/TypeCaster.ts"() {
47532
48000
  init_src();
@@ -47547,6 +48015,7 @@ var init_TypeCaster = __esm({
47547
48015
  { token: "mm", width: 2 },
47548
48016
  { token: "ss", width: 2 }
47549
48017
  ];
48018
+ TRUE_VALUES = /^(1|true|yes|y|t)$/i;
47550
48019
  TYPE_DEFAULTS = {
47551
48020
  string: "",
47552
48021
  number: 0,
@@ -47556,7 +48025,8 @@ var init_TypeCaster = __esm({
47556
48025
  TypeCasterClass = class {
47557
48026
  constructor() {
47558
48027
  /**
47559
- * Casts a value for a DECLARED dimension: the type cast, plus what a MISSING value means for it.
48028
+ * Casts a value for a DECLARED field: the type cast, plus what a MISSING value means for it. The
48029
+ * field is a producer dimension on the read path and a dataset column on the re-read path.
47560
48030
  *
47561
48031
  * Missing is null/undefined, and — for every type but `string` — the empty string, which is how
47562
48032
  * a NULL arrives from every source that stages through CSV (SQL, Delta Share, CSV files) and is
@@ -47588,6 +48058,7 @@ var init_TypeCaster = __esm({
47588
48058
  switch (type) {
47589
48059
  case "boolean": {
47590
48060
  if (typeof value === "boolean") return value;
48061
+ if (typeof value === "string") return TRUE_VALUES.test(value.trim());
47591
48062
  return Boolean(value);
47592
48063
  }
47593
48064
  case "datetime":
@@ -47726,6 +48197,7 @@ var init_TypeCaster = __esm({
47726
48197
  var LineParserClass, LineParser, LineParser_default;
47727
48198
  var init_LineParser = __esm({
47728
48199
  "../../packages/engines/src/parsing/LineParser.ts"() {
48200
+ init_src();
47729
48201
  init_TypeCaster();
47730
48202
  init_src5();
47731
48203
  LineParserClass = class {
@@ -47818,22 +48290,23 @@ var init_LineParser = __esm({
47818
48290
  return record;
47819
48291
  };
47820
48292
  /**
47821
- * Used ONLY to parse internal records (inside the .dataset) since I know they are already prepared in the correct way
48293
+ * The parser of one staged `.dataset`, compiled once from the schema that file was written with.
48294
+ * The schema is the ONLY description of the file - it has no header - and there is deliberately
48295
+ * no overload taking a consumer's field list, which cannot know about a shape a pass changed.
47822
48296
  */
47823
- this._internalParseCSV = (line, fields, delimiter) => {
47824
- const parts = CSVParser_default.parseRow(line, delimiter ?? ",");
47825
- const record = {};
47826
- for (const [index, field] of fields.entries()) {
47827
- const fieldKey = field.finalKey;
47828
- record[fieldKey] = field.dimension ? TypeCaster_default.castDimension(parts[index], field.dimension) : TypeCaster_default.cast(parts[index], "string");
47829
- }
47830
- return record;
47831
- };
47832
- /**
47833
- * Used ONLY to parse internal records (inside the .dataset) since I know they are already prepared in the correct way
47834
- */
47835
- this._internalParseJSON = (line) => {
47836
- return JSON.parse(line);
48297
+ this.datasetParser = (dsSchema) => {
48298
+ Affirm_default(dsSchema, "Invalid dataset schema");
48299
+ Affirm_default.hasItems(dsSchema.columns, "The dataset schema declares no column to read");
48300
+ if (dsSchema.encoding === "JSON")
48301
+ return (line) => JSON.parse(line);
48302
+ const { columns } = dsSchema;
48303
+ return (line) => {
48304
+ const parts = DatasetCodec_default.decodeRow(line);
48305
+ const record = {};
48306
+ for (let index = 0; index < columns.length; index++)
48307
+ record[columns[index].name] = TypeCaster_default.castDimension(parts[index], columns[index]);
48308
+ return record;
48309
+ };
47837
48310
  };
47838
48311
  }
47839
48312
  };
@@ -47950,7 +48423,7 @@ var init_ProducerEngine = __esm({
47950
48423
  let effectiveProducer = producer;
47951
48424
  let effectiveSource = source;
47952
48425
  if (compressionType) {
47953
- scope = { id: (0, import_crypto6.randomUUID)(), folder: `sample-${(0, import_crypto6.randomUUID)()}`, workersId: [] };
48426
+ scope = ExecutorScope_default.open({ id: (0, import_crypto6.randomUUID)(), folder: `sample-${(0, import_crypto6.randomUUID)()}`, workersId: [] });
47954
48427
  const driver = await DriverFactory_default.instantiateSource(source);
47955
48428
  const readyResult = await driver.ready({ producer, scope });
47956
48429
  const firstFile = readyResult.files[0].fullUri;
@@ -47960,7 +48433,7 @@ var init_ProducerEngine = __esm({
47960
48433
  ...producer,
47961
48434
  settings: {
47962
48435
  ...producer.settings,
47963
- fileKey: readyResult.files.length > 1 ? "%.dataset" : import_path14.default.basename(firstFile),
48436
+ fileKey: readyResult.files.length > 1 ? "%.extract" : import_path14.default.basename(firstFile),
47964
48437
  compressionType: void 0
47965
48438
  }
47966
48439
  };
@@ -51213,8 +51686,7 @@ var init_ConsumerManager = __esm({
51213
51686
  from: x2.owner
51214
51687
  },
51215
51688
  finalKey: x2.nameInProducer,
51216
- dimension: x2.dimension,
51217
- measure: x2.measure
51689
+ dimension: x2.dimension
51218
51690
  })));
51219
51691
  } else {
51220
51692
  const from = field.cField.from ?? (consumer.producers.length === 1 ? consumer.producers[0].name : null);
@@ -51226,8 +51698,7 @@ var init_ConsumerManager = __esm({
51226
51698
  from: col.owner
51227
51699
  },
51228
51700
  finalKey: col.nameInProducer,
51229
- dimension: col.dimension,
51230
- measure: col.measure
51701
+ dimension: col.dimension
51231
51702
  });
51232
51703
  });
51233
51704
  }
@@ -51242,13 +51713,12 @@ var init_ConsumerManager = __esm({
51242
51713
  from: field.cField.from ?? match.column.owner
51243
51714
  },
51244
51715
  finalKey: match.name,
51245
- dimension: match.column.dimension,
51246
- measure: match.column.measure
51716
+ dimension: match.column.dimension
51247
51717
  })));
51248
51718
  } else {
51249
51719
  const col = ConsumerManager.searchFieldInColumns(field.cField, availableColumns, consumer);
51250
51720
  Affirm_default(col, `Consumer "${consumer.name}" misconfiguration: the requested field "${field.cField.key}" is not found in any of the specified producers ("${consumer.producers.map((x2) => x2.name).join(", ")}")`);
51251
- expandedFields.push({ ...field, dimension: col.dimension, measure: col.measure });
51721
+ expandedFields.push({ ...field, dimension: col.dimension });
51252
51722
  }
51253
51723
  return expandedFields;
51254
51724
  };
@@ -52310,10 +52780,9 @@ var init_FieldLineage = __esm({
52310
52780
  this._fromProducer = (producer, field) => {
52311
52781
  const node = LineageEngine_default.nodeId("producer", producer.name);
52312
52782
  const dimension = (producer.dimensions ?? []).find((x2) => x2.name === field.key);
52313
- const measure = (producer.measures ?? []).find((x2) => x2.name === field.key);
52314
- const applied = dimension?.alias ? [`source column "${dimension.alias}"`] : measure ? ["measure"] : [];
52783
+ const applied = dimension?.alias ? [`source column "${dimension.alias}"`] : [];
52315
52784
  const step = { resource: producer.name, node, field: field.key, applied };
52316
- if (dimension || measure) return { path: [step] };
52785
+ if (dimension) return { path: [step] };
52317
52786
  if (field.key === "*") return { path: [{ ...step, applied: ["every column of this producer"] }] };
52318
52787
  if (Algo_default.isGlob(field.key)) return { path: [{ ...step, applied: ["every matching column"] }] };
52319
52788
  return { path: [step], unresolved: `"${field.key}" is not a column of producer "${producer.name}"` };
@@ -52359,11 +52828,8 @@ var init_FieldLineage = __esm({
52359
52828
  ...field.hidden ? ["hidden from the export"] : []
52360
52829
  ];
52361
52830
  };
52362
- /** Every column a producer exposes, dimensions then measures, in declared order. */
52363
- this._producerColumns = (producer) => [
52364
- ...(producer.dimensions ?? []).map((x2) => x2.name),
52365
- ...(producer.measures ?? []).map((x2) => x2.name)
52366
- ];
52831
+ /** Every column a producer exposes, in declared order. */
52832
+ this._producerColumns = (producer) => (producer.dimensions ?? []).map((x2) => x2.name);
52367
52833
  /** Whether a resource exposes a column by that name. A `*` consumer exposes whatever it is asked. */
52368
52834
  this._exposes = (env2, name, column) => {
52369
52835
  const producer = (env2.producers ?? []).find((x2) => x2.name === name);
@@ -52694,7 +53160,6 @@ var init_ResourceView = __esm({
52694
53160
  ],
52695
53161
  tables: [
52696
53162
  this._dimensionsTable(producer),
52697
- ...producer.measures?.length ? [this._measuresTable(producer)] : [],
52698
53163
  this._usedByTable(env2, producer),
52699
53164
  this._keyValueTable("Settings", settings)
52700
53165
  ],
@@ -52731,16 +53196,6 @@ var init_ResourceView = __esm({
52731
53196
  ]);
52732
53197
  return this._table("Dimensions", columns, rows);
52733
53198
  };
52734
- this._measuresTable = (producer) => {
52735
- const rows = (producer.measures ?? []).map((measure) => [
52736
- { value: measure.name, note: this._note(measure.description) },
52737
- { value: measure.sql }
52738
- ]);
52739
- return this._table("Measures", [
52740
- { key: "name", label: "Name" },
52741
- { key: "sql", label: "SQL" }
52742
- ], rows);
52743
- };
52744
53199
  /**
52745
53200
  * Which consumers read each column, the downstream half of field-level lineage. It is the answer
52746
53201
  * to "can this column go", which is a question a wide layout raises constantly and which nothing
@@ -54986,6 +55441,145 @@ var init_RunOverlay = __esm({
54986
55441
  }
54987
55442
  });
54988
55443
 
55444
+ // ../../packages/engines/src/parsing/ParquetExporter.ts
55445
+ var fs24, PARQUET_TYPES, TRUE_VALUES2, ParquetExporterClass, ParquetExporter, ParquetExporter_default;
55446
+ var init_ParquetExporter = __esm({
55447
+ "../../packages/engines/src/parsing/ParquetExporter.ts"() {
55448
+ fs24 = __toESM(require("fs"), 1);
55449
+ init_src();
55450
+ init_src3();
55451
+ init_src5();
55452
+ init_src2();
55453
+ PARQUET_TYPES = {
55454
+ string: "STRING",
55455
+ number: "DOUBLE",
55456
+ boolean: "BOOLEAN",
55457
+ datetime: "TIMESTAMP"
55458
+ };
55459
+ TRUE_VALUES2 = /^(1|true|yes|y|t)$/i;
55460
+ ParquetExporterClass = class {
55461
+ constructor() {
55462
+ /**
55463
+ * Writes `request.sourcePath` to `request.targetPath` as parquet, SNAPPY compressed, one typed
55464
+ * column per visible consumer field.
55465
+ *
55466
+ * Peak memory is bounded by one row group rather than by the dataset: the writer pulls the rows
55467
+ * a group at a time from the generator below, and nothing holds the file as a whole.
55468
+ */
55469
+ this.exportToParquet = async (request) => {
55470
+ Affirm_default(request, "Missing required parameter request");
55471
+ Affirm_default(request.sourcePath, "Missing required parameter request.sourcePath");
55472
+ Affirm_default(request.targetPath, "Missing required parameter request.targetPath");
55473
+ Affirm_default(request.dsSchema, "Missing required parameter request.dsSchema");
55474
+ Affirm_default(request.consumerName, "Missing required parameter request.consumerName");
55475
+ const columns = this._columns(request.dsSchema);
55476
+ Affirm_default.hasItems(columns, `Consumer "${request.consumerName}" has no visible field to export as parquet`);
55477
+ const { parquetWriteRows, fileWriter } = await import("hyparquet-writer");
55478
+ const rowGroupSize = this._rowGroupSize(columns.length);
55479
+ const startedMs = Date.now();
55480
+ await parquetWriteRows({
55481
+ writer: fileWriter(request.targetPath),
55482
+ columns,
55483
+ rows: this._readRecords(request, columns),
55484
+ rowGroupSize
55485
+ });
55486
+ const bytes = fs24.statSync(request.targetPath).size;
55487
+ Logger_default.info(`Wrote parquet "${request.targetPath}" for consumer "${request.consumerName}" (${columns.length} column(s), ${rowGroupSize} rows/group, ${bytes} bytes, ${Date.now() - startedMs}ms)`);
55488
+ };
55489
+ /**
55490
+ * The exported schema, taken from the dataset's own: one column per column ON the line, in line
55491
+ * order. Not from the consumer's fields, which cannot know about a shape a `pivot` changed.
55492
+ */
55493
+ this._columns = (dsSchema) => {
55494
+ return dsSchema.columns.map((column) => ({
55495
+ name: column.name,
55496
+ type: PARQUET_TYPES[column.type] ?? "STRING",
55497
+ nullable: true
55498
+ }));
55499
+ };
55500
+ /**
55501
+ * Rows per row group, derived from a budget of VALUES rather than a fixed row count: a row group
55502
+ * is buffered whole before it can be compressed, so what bounds the export's memory is cells, and
55503
+ * 100,000 rows means 400k values on a 4-column consumer and 130 million on a 1,300-field one.
55504
+ * Clamped at both ends — see the PARQUET_ constants for why each bound is there.
55505
+ */
55506
+ this._rowGroupSize = (columnCount) => {
55507
+ Affirm_default(columnCount > 0, "Cannot size a row group for zero columns");
55508
+ const { PARQUET_ROW_GROUP_CELLS, PARQUET_MIN_ROW_GROUP_ROWS, PARQUET_MAX_ROW_GROUP_ROWS } = Constants_default.defaults;
55509
+ const rows = Math.floor(PARQUET_ROW_GROUP_CELLS / columnCount);
55510
+ return Math.min(Math.max(rows, PARQUET_MIN_ROW_GROUP_ROWS), PARQUET_MAX_ROW_GROUP_ROWS);
55511
+ };
55512
+ /** A positional line read onto the column names by position — the file holds the visible columns only. */
55513
+ this._positionalValues = (line, columns) => {
55514
+ const parts = DatasetCodec_default.decodeRow(line);
55515
+ const values = {};
55516
+ for (const [index, column] of columns.entries())
55517
+ values[column.name] = parts[index];
55518
+ return values;
55519
+ };
55520
+ /**
55521
+ * One value as its column holds it. A missing value is a null in the column rather than the type's
55522
+ * zero, so a reader can still tell "no value" from a real `0`, `false` or empty string.
55523
+ *
55524
+ * Missing is null/undefined, plus — for every type but `string` — the empty string, matching
55525
+ * `TypeCaster.castDimension`: that is how a NULL arrives from every source that stages through
55526
+ * text. An empty string IS a value for a string column and is kept.
55527
+ */
55528
+ this._toParquetValue = (value, column, consumerName) => {
55529
+ if (value === null || value === void 0) return null;
55530
+ if (value === "" && column.type !== "STRING") return null;
55531
+ switch (column.type) {
55532
+ case "BOOLEAN":
55533
+ return typeof value === "boolean" ? value : TRUE_VALUES2.test(String(value).trim());
55534
+ case "DOUBLE":
55535
+ return typeof value === "number" ? value : Number(value);
55536
+ case "TIMESTAMP":
55537
+ return this._toDate(value, column.name, consumerName);
55538
+ default:
55539
+ return typeof value === "string" ? value : String(value);
55540
+ }
55541
+ };
55542
+ /**
55543
+ * A `datetime` column's value as a Date. An unparseable one FAILS the run, naming the field and
55544
+ * the value: a consumer whose transformation reformats a date into "DDMMYYYY" under a `datetime`
55545
+ * dimension is a configuration error, and a timestamp column silently full of 1970 hides it until
55546
+ * someone queries the lake.
55547
+ */
55548
+ this._toDate = (value, name, consumerName) => {
55549
+ const date2 = value instanceof Date ? value : new Date(value);
55550
+ if (isNaN(date2.getTime()))
55551
+ throw new Error(`Consumer "${consumerName}" cannot export field "${name}" as a parquet TIMESTAMP: "${String(value)}" is not a date. The field declares a "datetime" dimension, so either the value or that declaration is wrong.`);
55552
+ return date2;
55553
+ };
55554
+ }
55555
+ /**
55556
+ * The dataset's records, one at a time, coerced to what each column holds. An async generator, so
55557
+ * the writer pulls a row group's worth and no more — the file is never in memory as a whole.
55558
+ *
55559
+ * A parquet consumer stages JSON (see `DatasetSchema.encodingFor`), so the values arrive already
55560
+ * typed. The positional branch covers a dataset staged the other way, reading the row WITHOUT
55561
+ * casting since the cast is what loses a boolean (`Boolean('false')` is `true`).
55562
+ */
55563
+ async *_readRecords(request, columns) {
55564
+ const readStream = fs24.createReadStream(request.sourcePath);
55565
+ try {
55566
+ for await (const line of readLines(readStream)) {
55567
+ const values = request.dsSchema.encoding === "CSV" ? this._positionalValues(line, columns) : JSON.parse(line);
55568
+ const row = {};
55569
+ for (const column of columns)
55570
+ row[column.name] = this._toParquetValue(values[column.name], column, request.consumerName);
55571
+ yield row;
55572
+ }
55573
+ } finally {
55574
+ readStream.destroy();
55575
+ }
55576
+ }
55577
+ };
55578
+ ParquetExporter = new ParquetExporterClass();
55579
+ ParquetExporter_default = ParquetExporter;
55580
+ }
55581
+ });
55582
+
54989
55583
  // ../../packages/engines/src/producer/PushdownEngine.ts
54990
55584
  var MISSING_BY_TYPE, PushdownEngineClass, PushdownEngine, PushdownEngine_default;
54991
55585
  var init_PushdownEngine = __esm({
@@ -55818,6 +56412,7 @@ var init_src10 = __esm({
55818
56412
  init_RunOverlay();
55819
56413
  init_RunRegistry();
55820
56414
  init_LineParser();
56415
+ init_ParquetExporter();
55821
56416
  init_DiscoverMerger();
55822
56417
  init_ProducerEngine();
55823
56418
  init_ProducerManager();
@@ -55833,6 +56428,216 @@ var init_src10 = __esm({
55833
56428
  }
55834
56429
  });
55835
56430
 
56431
+ // ../../packages/executors/src/DatasetSchema.ts
56432
+ var DatasetSchemaClass, DatasetSchema, DatasetSchema_default;
56433
+ var init_DatasetSchema = __esm({
56434
+ "../../packages/executors/src/DatasetSchema.ts"() {
56435
+ init_src();
56436
+ init_src10();
56437
+ DatasetSchemaClass = class {
56438
+ constructor() {
56439
+ /**
56440
+ * How a consumer's records are staged on the way to the destination. A consumer declares exactly
56441
+ * one output (`maxItems: 1` in consumer-schema.json), so `outputs[0]` is THE output.
56442
+ */
56443
+ this.encodingFor = (consumer) => {
56444
+ Affirm_default(consumer, "Invalid consumer");
56445
+ Affirm_default.hasItems(consumer.outputs, `Consumer "${consumer.name}" declares no output`);
56446
+ switch (consumer.outputs[0].format) {
56447
+ case "JSON":
56448
+ case "API":
56449
+ return "JSON";
56450
+ case "PARQUET":
56451
+ return "JSON";
56452
+ case "TXT":
56453
+ return "CSV";
56454
+ case "FIXED":
56455
+ return "CSV";
56456
+ case "CSV":
56457
+ return "CSV";
56458
+ default:
56459
+ throw new Error(`Export format ${consumer.outputs[0].format} not implemented yet.`);
56460
+ }
56461
+ };
56462
+ /**
56463
+ * The schema the run's workers write with: one column per VISIBLE consumer field, in field
56464
+ * order. Hidden fields are not on the line at all.
56465
+ */
56466
+ this.compile = (consumer, fields) => {
56467
+ Affirm_default(consumer, "Invalid consumer");
56468
+ Affirm_default(fields, "Invalid consumer fields");
56469
+ const columns = fields.filter((x2) => !x2.cField.hidden).map(this._column);
56470
+ Affirm_default.hasItems(columns, `Consumer "${consumer.name}" has no visible field to write`);
56471
+ return {
56472
+ encoding: this.encodingFor(consumer),
56473
+ columns,
56474
+ rows: 0,
56475
+ writtenBy: "worker"
56476
+ };
56477
+ };
56478
+ /**
56479
+ * The same shape, as the named stage left it. Returned by every pass that rewrites the dataset
56480
+ * without changing its columns, so `rows` always describes the file as it is NOW.
56481
+ */
56482
+ this.rewritten = (dsSchema, writtenBy, rows) => {
56483
+ Affirm_default(dsSchema, "Invalid dataset schema");
56484
+ Affirm_default(rows >= 0, `Invalid row count ${rows} for a dataset written by "${writtenBy}"`);
56485
+ return { ...dsSchema, writtenBy, rows };
56486
+ };
56487
+ /**
56488
+ * The shape a `pivot` leaves behind: the row keys it grouped on, then one column per pivot value.
56489
+ * A pivoted column has no producer dimension behind it - it is an aggregate this pass invented -
56490
+ * so it declares `source: 'pivot'` and a `number` type.
56491
+ */
56492
+ this.forPivot = (dsSchema, rowKeys, pivotColumns, rows) => {
56493
+ Affirm_default(dsSchema, "Invalid dataset schema");
56494
+ Affirm_default.hasItems(rowKeys, "A pivot declares no row key");
56495
+ const keyColumns = rowKeys.map((key) => {
56496
+ const existing = dsSchema.columns.find((column) => column.name === key);
56497
+ Affirm_default(existing, `The pivot row key "${key}" is not a column of the dataset being pivoted`);
56498
+ return existing;
56499
+ });
56500
+ return {
56501
+ encoding: dsSchema.encoding,
56502
+ columns: [...keyColumns, ...pivotColumns.map((name) => ({ name, type: "number", source: "pivot" }))],
56503
+ rows,
56504
+ writtenBy: "pivot"
56505
+ };
56506
+ };
56507
+ /** The column names on a line, in line order - the header of a positional export. */
56508
+ this.names = (dsSchema) => {
56509
+ Affirm_default(dsSchema, "Invalid dataset schema");
56510
+ return dsSchema.columns.map((column) => column.name);
56511
+ };
56512
+ /**
56513
+ * One column, typed from the dimension behind it. `type`, `format` and `nullable` are the only
56514
+ * three properties a cast reads, so carrying those lets a reader cast a staged value back
56515
+ * without going near the consumer config.
56516
+ */
56517
+ this._column = (field) => {
56518
+ if (ConsumerManager_default.isLineCountField(field.cField))
56519
+ return { name: field.finalKey, type: "number", source: "lineCount" };
56520
+ const column = {
56521
+ name: field.finalKey,
56522
+ type: field.dimension?.type ?? "string"
56523
+ };
56524
+ if (field.dimension?.format) column.format = field.dimension.format;
56525
+ if (field.dimension?.nullable === false) column.nullable = false;
56526
+ return column;
56527
+ };
56528
+ }
56529
+ };
56530
+ DatasetSchema = new DatasetSchemaClass();
56531
+ DatasetSchema_default = DatasetSchema;
56532
+ }
56533
+ });
56534
+
56535
+ // ../../packages/executors/src/FixedBlockOrder.ts
56536
+ var fs25, FixedBlockOrderClass, FixedBlockOrder, FixedBlockOrder_default;
56537
+ var init_FixedBlockOrder = __esm({
56538
+ "../../packages/executors/src/FixedBlockOrder.ts"() {
56539
+ fs25 = __toESM(require("fs"));
56540
+ init_src();
56541
+ init_src5();
56542
+ init_src2();
56543
+ FixedBlockOrderClass = class {
56544
+ constructor() {
56545
+ /** The file one block's records are collected in. Named by the value's place in `order`, not by
56546
+ * the value itself: a discriminator value is data and may hold anything a path may not. */
56547
+ this.blockPath = (sourcePath, position) => `${sourcePath}.block_${position}`;
56548
+ /**
56549
+ * Routes the staged dataset into one file per layout and concatenates them in `layouts.order`,
56550
+ * returning the path of the block-ordered dataset the export then transforms.
56551
+ */
56552
+ this.reorder = async (request) => {
56553
+ Affirm_default(request, "Invalid block order request");
56554
+ Affirm_default(request.sourcePath, "Missing dataset to order into blocks");
56555
+ Affirm_default(request.layouts, "Invalid fixed-width layouts");
56556
+ Affirm_default.hasItems(request.layouts.order, "The fixed-width layouts declare no block order");
56557
+ const { sourcePath, layouts, consumerName } = request;
56558
+ const orderedPath = `${sourcePath}.blocks`;
56559
+ const counts = await this._route(request);
56560
+ await fs25.promises.unlink(sourcePath);
56561
+ await this._concat(sourcePath, layouts, orderedPath);
56562
+ Logger_default.info(`Consumer "${consumerName}" writes its FIXED record blocks as ${layouts.order.map((value, position) => `${value} x${counts[position]}`).join(", ")}`);
56563
+ return orderedPath;
56564
+ };
56565
+ /**
56566
+ * The routing pass: every line read once, appended to the file of the block its discriminator
56567
+ * names. One stream per layout, all open at once — `MAX_FIXED_LAYOUTS` is what bounds them, so
56568
+ * there is no eviction to do and a block's file is only ever written by its own stream.
56569
+ *
56570
+ * Returns the records each block took, in `order`, which is what the log line reports.
56571
+ */
56572
+ this._route = async (request) => {
56573
+ const { sourcePath, layouts } = request;
56574
+ const streams = layouts.order.map((_3, position) => fs25.createWriteStream(this.blockPath(sourcePath, position)));
56575
+ const positions = new Map(layouts.order.map((value, position) => [value, position]));
56576
+ const counts = layouts.order.map(() => 0);
56577
+ const readStream = fs25.createReadStream(sourcePath);
56578
+ try {
56579
+ for await (const line of readLines(readStream)) {
56580
+ const position = this._positionOf(line, layouts, positions);
56581
+ counts[position]++;
56582
+ const stream = streams[position];
56583
+ if (!stream.write(line + "\n"))
56584
+ await new Promise((resolve) => stream.once("drain", resolve));
56585
+ }
56586
+ } finally {
56587
+ for (const stream of streams)
56588
+ await this._closeStream(stream);
56589
+ readStream.destroy();
56590
+ }
56591
+ return counts;
56592
+ };
56593
+ /** The block a staged line belongs to. A record no layout covers stops the run; see `planFor`. */
56594
+ this._positionOf = (line, layouts, positions) => {
56595
+ const value = DatasetCodec_default.decodeRow(line)[layouts.index];
56596
+ const position = value === null || value === void 0 ? void 0 : positions.get(String(value));
56597
+ Affirm_default(
56598
+ position !== void 0,
56599
+ `No FIXED layout for the value ${value === null || value === void 0 ? "null" : `"${value}"`} of the discriminator field "${layouts.discriminator}". The layouts cover ${layouts.order.map((x2) => `"${x2}"`).join(", ")}.`
56600
+ );
56601
+ return position;
56602
+ };
56603
+ /** The blocks appended into one dataset, in `order`, each file removed as it is consumed. */
56604
+ this._concat = async (sourcePath, layouts, orderedPath) => {
56605
+ const writer = fs25.createWriteStream(orderedPath);
56606
+ try {
56607
+ for (let position = 0; position < layouts.order.length; position++) {
56608
+ const blockPath = this.blockPath(sourcePath, position);
56609
+ await this._append(blockPath, writer);
56610
+ await fs25.promises.unlink(blockPath);
56611
+ }
56612
+ } finally {
56613
+ await this._closeStream(writer);
56614
+ }
56615
+ };
56616
+ /** One block's bytes onto the end of the ordered dataset. Piped, so a block is never held in memory. */
56617
+ this._append = (blockPath, writer) => {
56618
+ return new Promise((resolve, reject) => {
56619
+ const reader = fs25.createReadStream(blockPath);
56620
+ reader.on("error", reject);
56621
+ reader.on("end", resolve);
56622
+ reader.pipe(writer, { end: false });
56623
+ });
56624
+ };
56625
+ /** Flushes and releases a stream's descriptor. Awaited: the read that follows must not race the flush. */
56626
+ this._closeStream = async (stream) => {
56627
+ if (!stream) return;
56628
+ stream.end();
56629
+ await new Promise((resolve, reject) => {
56630
+ stream.on("finish", resolve);
56631
+ stream.on("error", reject);
56632
+ });
56633
+ };
56634
+ }
56635
+ };
56636
+ FixedBlockOrder = new FixedBlockOrderClass();
56637
+ FixedBlockOrder_default = FixedBlockOrder;
56638
+ }
56639
+ });
56640
+
55836
56641
  // ../../packages/executors/src/OutputExecutor.ts
55837
56642
  var OutputExecutorClass, OutputExecutor, OutputExecutor_default;
55838
56643
  var init_OutputExecutor = __esm({
@@ -55841,35 +56646,25 @@ var init_OutputExecutor = __esm({
55841
56646
  init_src();
55842
56647
  init_src8();
55843
56648
  init_src4();
56649
+ init_src10();
55844
56650
  init_src5();
55845
56651
  init_src2();
56652
+ init_DatasetSchema();
56653
+ init_FixedBlockOrder();
55846
56654
  OutputExecutorClass = class {
55847
56655
  constructor() {
55848
- this._getInternalRecordFormat = (consumer) => {
55849
- const output = consumer.outputs[0];
55850
- switch (output.format) {
55851
- case "API":
55852
- return "JSON";
55853
- case "TXT":
55854
- return "CSV";
55855
- default:
55856
- return output.format;
55857
- }
55858
- };
55859
56656
  /**
55860
- * The output decisions a consumer makes once — the internal format, whether nulls are omitted,
55861
- * which fields are visible — lifted off the per-record path. `outputRecord` used to re-derive
55862
- * all three for every record, and `toCSV` allocated a filtered field array on top of that.
56657
+ * The write side of a staged dataset's schema: what to write, in what order, in which encoding.
56658
+ * Derived from the schema, so the writer and every reader share one column list.
55863
56659
  */
55864
- this.compileOutput = (consumer, fields) => {
56660
+ this.compileOutput = (consumer, dsSchema) => {
55865
56661
  Affirm_default(consumer, "Invalid consumer");
55866
- Affirm_default(fields, "Invalid consumer fields");
56662
+ Affirm_default(dsSchema, "Invalid dataset schema");
55867
56663
  return {
55868
- format: this._getInternalRecordFormat(consumer),
56664
+ format: dsSchema.encoding,
55869
56665
  delimiter: ",",
55870
- visibleKeys: fields.filter((x2) => !x2.cField.hidden).map((x2) => x2.finalKey),
55871
- hiddenKeys: fields.filter((x2) => x2.cField.hidden).map((x2) => x2.finalKey),
55872
- omitNull: consumer.outputs.some((x2) => x2.omitNull === true)
56666
+ visibleKeys: DatasetSchema_default.names(dsSchema),
56667
+ omitNull: consumer.outputs[0].omitNull === true
55873
56668
  };
55874
56669
  };
55875
56670
  this.outputRecord = (record, plan) => {
@@ -55882,26 +56677,38 @@ var init_OutputExecutor = __esm({
55882
56677
  throw new Error(`Export format ${plan.format} not implemented yet.`);
55883
56678
  }
55884
56679
  };
56680
+ /**
56681
+ * One positional line of the internal format. `DatasetCodec`, not `CSVParser`: a staged line has
56682
+ * to carry a null that is not an empty string and must never hold a newline.
56683
+ */
55885
56684
  this.toCSV = (record, plan) => {
55886
56685
  const { visibleKeys, delimiter } = plan;
55887
56686
  let row = "";
55888
56687
  for (let index = 0; index < visibleKeys.length; index++) {
55889
56688
  if (index > 0)
55890
56689
  row += delimiter;
55891
- row += CSVParser_default.escapeField(record[visibleKeys[index]]?.toString() ?? "");
56690
+ row += DatasetCodec_default.encodeField(record[visibleKeys[index]]);
55892
56691
  }
55893
56692
  return row;
55894
56693
  };
56694
+ /**
56695
+ * One JSON object per line, keyed in the CONSUMER's field order rather than the record's own.
56696
+ *
56697
+ * A record arrives keyed in the order its producer's dimensions were read, so two producers of
56698
+ * one consumer wrote the same record as two different lines — and `distinct` and
56699
+ * `no_duplicates`, which compare the line, saw two rows. Building from `visibleKeys` gives every
56700
+ * writer one order, and drops the hidden fields without deleting them out of the caller's record.
56701
+ */
55895
56702
  this.toJSON = (record, plan) => {
55896
- for (const key of plan.hiddenKeys)
55897
- delete record[key];
55898
- if (plan.omitNull) {
55899
- for (const key of Object.keys(record)) {
55900
- if (record[key] === null || record[key] === void 0)
55901
- delete record[key];
55902
- }
56703
+ const { visibleKeys, omitNull } = plan;
56704
+ const output = {};
56705
+ for (const key of visibleKeys) {
56706
+ const value = record[key];
56707
+ if (omitNull && (value === null || value === void 0))
56708
+ continue;
56709
+ output[key] = value === void 0 ? null : value;
55903
56710
  }
55904
- return JSON.stringify(record);
56711
+ return JSON.stringify(output);
55905
56712
  };
55906
56713
  /**
55907
56714
  * The exported name of one split file: the destination name with the file's label before the
@@ -55914,66 +56721,141 @@ var init_OutputExecutor = __esm({
55914
56721
  return `${pathName.substring(0, dot)}_${label}${pathName.substring(dot)}`;
55915
56722
  };
55916
56723
  /**
55917
- * Writes the run's result to every configured destination, one call per file the split pass
56724
+ * Writes the run's result to the consumer's destination, one call per file the split pass
55918
56725
  * produced. The files are passed in rather than read back out of the scope folder: the label a
55919
56726
  * file carries is what names it at the destination, and with no split configured that list is
55920
56727
  * the merged dataset alone, labelled "0" — the suffix an unsplit export has always had.
56728
+ *
56729
+ * One output per consumer (`maxItems: 1` in consumer-schema.json). This used to loop, but only
56730
+ * ever shipped the first entry, and could not have done more: `move` unlinks what it ships.
55921
56731
  */
55922
- this.exportResult = async (consumer, fields, splitFiles) => {
56732
+ this.exportResult = async (consumer, dsSchema, splitFiles) => {
55923
56733
  Affirm_default.hasItems(splitFiles, `Consumer "${consumer.name}" has no output file to export`);
55924
- const internalFormat = this._getInternalRecordFormat(consumer);
56734
+ Affirm_default.hasItems(consumer.outputs, `Consumer "${consumer.name}" declares no output to export to`);
56735
+ Affirm_default(dsSchema, `Consumer "${consumer.name}" has no dataset schema to export against`);
56736
+ const output = consumer.outputs[0];
56737
+ const internalFormat = dsSchema.encoding;
56738
+ const destination = Environment_default.getSource(output.exportDestination);
56739
+ Affirm_default(destination, `The export destination "${output.exportDestination}" of consumer "${consumer.name}" is not a source of this project`);
56740
+ const driver = await DriverFactory_default.instantiateDestination(destination);
56741
+ const destinationName = this._composeFileName(consumer, output, this._getExtension(output));
56742
+ Logger_default.info(`Exporting consumer "${consumer.name}" to "${output.exportDestination}" as ${output.format} (${destinationName}, ${splitFiles.length} file(s))`);
56743
+ const header = this._composeHeader(output, dsSchema);
56744
+ const fixedLayouts = output.format === "FIXED" && output.layouts ? FixedWidthWriter_default.compileLayouts(dsSchema, this._fixedPositions(consumer), output, consumer.name) : null;
56745
+ const fixedLine = output.format === "FIXED" ? this._fixedLineWriter(consumer, dsSchema, output, fixedLayouts) : null;
55925
56746
  const results = [];
55926
- for (const output of consumer.outputs) {
55927
- const destination = Environment_default.getSource(output.exportDestination);
55928
- const driver = await DriverFactory_default.instantiateDestination(destination);
55929
- const destinationName = this._composeFileName(consumer, output, this._getExtension(output));
55930
- Logger_default.info(`Exporting consumer "${consumer.name}" to "${output.exportDestination}" as ${output.format} (${destinationName}, ${splitFiles.length} file(s))`);
55931
- const header = this._composeHeader(output, fields);
55932
- for (const { path: startingPath, label } of splitFiles) {
55933
- const destinationPath = this.getCompletedPath(destinationName, label);
55934
- if (output.format === internalFormat) {
55935
- results.push(await driver.move(startingPath, destinationPath, header));
55936
- } else {
55937
- switch (output.format) {
55938
- case "CSV":
55939
- results.push(
55940
- await driver.transformAndMove(startingPath, (line) => {
55941
- const parsed = JSON.parse(line);
55942
- return Object.keys(parsed).map((x2) => `"${parsed[x2]}"`).join(",");
55943
- }, destinationPath, header)
55944
- );
55945
- break;
55946
- case "TXT": {
55947
- const delimiter = this._getDelimiter(output);
55948
- results.push(
55949
- await driver.transformAndMove(
55950
- startingPath,
55951
- (line) => CSVParser_default.stringifyRow(CSVParser_default.parseRow(line, ","), delimiter),
55952
- destinationPath,
55953
- header
55954
- )
55955
- );
55956
- break;
55957
- }
55958
- case "API":
55959
- case "JSON":
55960
- results.push(
55961
- await driver.transformAndMove(startingPath, (line) => {
55962
- const parts = CSVParser_default.parseRow(line, ",");
55963
- const value = {};
55964
- for (const [index, field] of fields.entries())
55965
- value[field.finalKey] = parts[index];
55966
- return JSON.stringify(value);
55967
- }, destinationPath)
55968
- );
55969
- break;
55970
- default:
55971
- throw new Error(`Export result to format ${output.format} not implemented yet.`);
55972
- }
56747
+ for (const { path: startingPath, label } of splitFiles) {
56748
+ const destinationPath = this.getCompletedPath(destinationName, label);
56749
+ if (output.format === internalFormat || output.format === "API") {
56750
+ results.push(await driver.move(startingPath, destinationPath, header));
56751
+ continue;
56752
+ }
56753
+ switch (output.format) {
56754
+ case "TXT": {
56755
+ const delimiter = this._getDelimiter(output);
56756
+ results.push(
56757
+ await driver.transformAndMove(
56758
+ startingPath,
56759
+ (line) => DatasetCodec_default.redelimit(line, delimiter),
56760
+ destinationPath,
56761
+ header
56762
+ )
56763
+ );
56764
+ break;
56765
+ }
56766
+ case "FIXED": {
56767
+ const fixedPath = fixedLayouts ? await FixedBlockOrder_default.reorder({ sourcePath: startingPath, layouts: fixedLayouts, consumerName: consumer.name }) : startingPath;
56768
+ results.push(await driver.transformAndMove(fixedPath, fixedLine, destinationPath));
56769
+ break;
55973
56770
  }
56771
+ case "PARQUET":
56772
+ results.push(await this._exportParquet({
56773
+ sourcePath: startingPath,
56774
+ destinationPath,
56775
+ dsSchema,
56776
+ consumerName: consumer.name,
56777
+ driver
56778
+ }));
56779
+ break;
56780
+ default:
56781
+ throw new Error(`Export result to format ${output.format} not implemented yet.`);
55974
56782
  }
55975
- return this._unifingResult(results);
55976
56783
  }
56784
+ return this._unifingResult(results);
56785
+ };
56786
+ /**
56787
+ * A consumer's FIXED output resolved without running it: the layouts the export would compile,
56788
+ * and every problem with them. `compile` prints what it resolves and fails on what it reports,
56789
+ * and a run resolves it before reading a producer — so a width that does not add up is caught
56790
+ * where it was written rather than after the whole dataset has been built.
56791
+ *
56792
+ * Null for a consumer that writes anything else: there is no layout to resolve.
56793
+ */
56794
+ this.reviewFixedOutput = (consumer, dsSchema) => {
56795
+ Affirm_default(consumer, "Invalid consumer");
56796
+ const output = consumer.outputs?.[0];
56797
+ if (output?.format !== "FIXED") return null;
56798
+ const schema = dsSchema ?? DatasetSchema_default.compile(consumer, ConsumerManager_default.getExpandedFields(consumer));
56799
+ return FixedWidthWriter_default.review(schema, this._fixedPositions(consumer), output, consumer.name);
56800
+ };
56801
+ /**
56802
+ * The line function a FIXED export hands its destination driver: one staged record in, the
56803
+ * positional line it becomes out. The layout is resolved here, once, and closed over — so the
56804
+ * per-record cost is a decode of the staged line and a pad per field, with no config lookup.
56805
+ *
56806
+ * With `layouts` declared the record's own discriminator picks its plan, which is the only
56807
+ * per-record branch on the path; the records are already grouped into their blocks by then.
56808
+ *
56809
+ * No header is passed alongside it: a positional file has no room for one, and the field names
56810
+ * would not sit on the columns they describe.
56811
+ */
56812
+ this._fixedLineWriter = (consumer, dsSchema, output, layouts) => {
56813
+ const suffix = FixedWidthWriter_default.lineSuffix(output);
56814
+ if (layouts) {
56815
+ for (const value of layouts.order)
56816
+ Logger_default.info(`Consumer "${consumer.name}" writes its FIXED "${value}" record as ${layouts.plans.get(value).recordLength} characters over ${layouts.plans.get(value).fields.length} field(s)`);
56817
+ return (line) => {
56818
+ const values = DatasetCodec_default.decodeRow(line);
56819
+ return FixedWidthWriter_default.composeLine(values, FixedWidthWriter_default.planFor(values, layouts)) + suffix;
56820
+ };
56821
+ }
56822
+ const plan = FixedWidthWriter_default.compilePlan(dsSchema.columns, this._fixedPositions(consumer), output, `the FIXED output of consumer "${consumer.name}"`);
56823
+ Logger_default.info(`Consumer "${consumer.name}" writes a FIXED record of ${plan.recordLength} characters over ${plan.fields.length} field(s)`);
56824
+ return (line) => FixedWidthWriter_default.composeLine(DatasetCodec_default.decodeRow(line), plan) + suffix;
56825
+ };
56826
+ /**
56827
+ * Every visible field's `position` block, keyed by the name its column carries on the staged
56828
+ * line. Read from the EXPANDED fields rather than the consumer's own, so a glob key brings the
56829
+ * `position` it was declared with to each of the columns it stands for, and a field the dataset
56830
+ * has no `position` for is left absent for `compilePlan` to name.
56831
+ */
56832
+ this._fixedPositions = (consumer) => {
56833
+ const positions = {};
56834
+ for (const field of ConsumerManager_default.getExpandedFields(consumer)) {
56835
+ if (field.cField.hidden) continue;
56836
+ positions[field.finalKey] = field.cField.position;
56837
+ }
56838
+ return positions;
56839
+ };
56840
+ /**
56841
+ * Ships one split file as parquet. A parquet file is a columnar container whose footer can only
56842
+ * be written once the last row group is encoded, so there is no per-line mapping to hand a
56843
+ * driver: the file is built in the scope folder beside the dataset it is read from, then handed
56844
+ * over as bytes with NO header — a parquet file carries its own schema, and `move` is
56845
+ * byte-faithful in all three destination drivers.
56846
+ *
56847
+ * `move` unlinks what it shipped, so a successful export leaves no staged file behind. A failed
56848
+ * one leaves it in the scope folder, which the run's cleanup removes with everything else.
56849
+ */
56850
+ this._exportParquet = async (plan) => {
56851
+ const parquetPath = `${plan.sourcePath}.parquet`;
56852
+ await ParquetExporter_default.exportToParquet({
56853
+ sourcePath: plan.sourcePath,
56854
+ targetPath: parquetPath,
56855
+ dsSchema: plan.dsSchema,
56856
+ consumerName: plan.consumerName
56857
+ });
56858
+ return plan.driver.move(parquetPath, plan.destinationPath);
55977
56859
  };
55978
56860
  /**
55979
56861
  * Folds the per-file upload results into the one the run reports.
@@ -55992,24 +56874,32 @@ var init_OutputExecutor = __esm({
55992
56874
  /**
55993
56875
  * The header line of a positional export: the visible field names, in column order, quoted and
55994
56876
  * delimited exactly as the rows under them — so the header parses with the same reader the data
55995
- * does. Null for a JSON/API output, where a record already carries its own field names, and for
55996
- * an output that turns the header off.
56877
+ * does. Null for a JSON/API output, where a record already carries its own field names, for a
56878
+ * FIXED one, where a name does not fit the column it describes and the receiver reads by
56879
+ * position anyway, and for an output that turns the header off.
55997
56880
  *
55998
- * The names are the exported ones (`finalKey`, i.e. the alias where one is set), since a reader
55999
- * of the file has nothing else to match a column against.
56881
+ * The names come from the dataset's own schema, so a `pivot` is described by the header.
56000
56882
  */
56001
- this._composeHeader = (output, fields) => {
56883
+ this._composeHeader = (output, dsSchema) => {
56002
56884
  if (output.header === false) return null;
56003
56885
  if (output.format !== "CSV" && output.format !== "TXT") return null;
56004
- const names = fields.filter((x2) => !x2.cField.hidden).map((x2) => x2.finalKey);
56005
- return CSVParser_default.stringifyRow(names, this._getDelimiter(output));
56886
+ return CSVParser_default.stringifyRow(DatasetSchema_default.names(dsSchema), this._getDelimiter(output));
56006
56887
  };
56007
56888
  /** The delimiter the exported file is written with. Only TXT configures it; CSV is a comma. */
56008
56889
  this._getDelimiter = (output) => {
56009
56890
  return output.format === "TXT" ? output.delimiter ?? " " : ",";
56010
56891
  };
56011
56892
  this._getExtension = (output) => {
56012
- return output.format === "CSV" ? "csv" : output.format === "JSON" ? "jsonl" : "txt";
56893
+ switch (output.format) {
56894
+ case "CSV":
56895
+ return "csv";
56896
+ case "JSON":
56897
+ return "jsonl";
56898
+ case "PARQUET":
56899
+ return "parquet";
56900
+ default:
56901
+ return "txt";
56902
+ }
56013
56903
  };
56014
56904
  this._composeFileName = (consumer, output, extension, executionId) => {
56015
56905
  if (output.exportName && output.exportName.trim().length > 0) {
@@ -56041,7 +56931,7 @@ var init_ConsumerExecutor = __esm({
56041
56931
  init_src();
56042
56932
  init_src10();
56043
56933
  init_OutputExecutor();
56044
- init_src10();
56934
+ init_DatasetSchema();
56045
56935
  init_src2();
56046
56936
  LIMIT_SCAN_BYTES = 64 * 1024;
56047
56937
  NEWLINE_BYTE = 10;
@@ -56165,13 +57055,20 @@ var init_ConsumerExecutor = __esm({
56165
57055
  }
56166
57056
  return output;
56167
57057
  };
56168
- this.processLineCount = async (consumer, datasetPath, executorResults) => {
56169
- const internalRecordFormat = OutputExecutor_default._getInternalRecordFormat(consumer);
56170
- const internalFields = ConsumerManager_default.getExpandedFields(consumer);
56171
- const outputPlan = OutputExecutor_default.compileOutput(consumer, internalFields);
56172
- const lineCountFields = ConsumerManager_default.getLineCountFieldsFromExpanded(internalFields, true);
56173
- if (lineCountFields.length === 0)
56174
- return 0;
57058
+ /**
57059
+ * Rewrites every `lineCount` field of the merged dataset so it names the line of the SOURCE file
57060
+ * the row came from, by adding each chunk's `rowOffset` to the counter its worker wrote.
57061
+ *
57062
+ * The shape is unchanged, so the schema comes back with a fresh `writtenBy` and the row count
57063
+ * this pass actually saw - which also checks the file still holds what the workers reported.
57064
+ */
57065
+ this.processLineCount = async (consumer, dsSchema, datasetPath, executorResults) => {
57066
+ Affirm_default(dsSchema, "Invalid dataset schema");
57067
+ const lineCountColumns = dsSchema.columns.filter((column) => column.source === "lineCount");
57068
+ if (lineCountColumns.length === 0)
57069
+ return dsSchema;
57070
+ const parseLine = LineParser_default.datasetParser(dsSchema);
57071
+ const outputPlan = OutputExecutor_default.compileOutput(consumer, dsSchema);
56175
57072
  const tempWorkPath = datasetPath + "_tmp";
56176
57073
  const reader = import_fs21.default.createReadStream(datasetPath);
56177
57074
  const writer = import_fs21.default.createWriteStream(tempWorkPath);
@@ -56193,11 +57090,11 @@ var init_ConsumerExecutor = __esm({
56193
57090
  Affirm_default(currentWorker, `Unable to resolve worker metadata while processing lineCount for ${datasetPath}`);
56194
57091
  const rowOffset = currentWorker.rowOffset ?? 0;
56195
57092
  if (rowOffset > 0) {
56196
- const record = this._parseLine(line, internalRecordFormat, internalFields);
56197
- for (const field of lineCountFields) {
56198
- const currentValue = Number(record[field.finalKey]);
57093
+ const record = parseLine(line);
57094
+ for (const column of lineCountColumns) {
57095
+ const currentValue = Number(record[column.name]);
56199
57096
  Affirm_default(!Number.isNaN(currentValue), `Invalid lineCount value found while processing ${datasetPath}`);
56200
- record[field.finalKey] = currentValue + rowOffset;
57097
+ record[column.name] = currentValue + rowOffset;
56201
57098
  }
56202
57099
  if (!writer.write(OutputExecutor_default.outputRecord(record, outputPlan) + "\n"))
56203
57100
  await waitForDrain();
@@ -56220,11 +57117,11 @@ var init_ConsumerExecutor = __esm({
56220
57117
  }
56221
57118
  await import_promises16.default.unlink(datasetPath);
56222
57119
  await import_promises16.default.rename(tempWorkPath, datasetPath);
56223
- return processedLineCount;
57120
+ return DatasetSchema_default.rewritten(dsSchema, "lineCount", processedLineCount);
56224
57121
  };
56225
57122
  /**
56226
- * Cuts the merged dataset down to the run's record limit, and reports the row count it left
56227
- * behind — or `null` when the dataset was already within the limit and nothing was touched.
57123
+ * Cuts the merged dataset down to the run's record limit, and reports the shape it left behind.
57124
+ * The schema comes back unchanged when the dataset was already within the limit.
56228
57125
  *
56229
57126
  * Every worker stops at the limit within its OWN chunk, which is the right thing for the read
56230
57127
  * (no thread reads further than the run could need) but leaves up to `limit` rows per chunk in
@@ -56234,7 +57131,8 @@ var init_ConsumerExecutor = __esm({
56234
57131
  * at then needs no arithmetic on line contents, so no line ending or multi-byte character can
56235
57132
  * put the cut in the middle of a row. Only the bytes up to the limit are ever read.
56236
57133
  */
56237
- this.processLimit = async (datasetPath, limit) => {
57134
+ this.processLimit = async (dsSchema, datasetPath, limit) => {
57135
+ Affirm_default(dsSchema, "Invalid dataset schema");
56238
57136
  Affirm_default(datasetPath, "Invalid dataset path");
56239
57137
  Affirm_default(limit > 0, `Invalid record limit ${limit}`);
56240
57138
  const handle = await import_promises16.default.open(datasetPath, "r+");
@@ -56245,7 +57143,7 @@ var init_ConsumerExecutor = __esm({
56245
57143
  while (newlines < limit) {
56246
57144
  const { bytesRead } = await handle.read(buffer, 0, LIMIT_SCAN_BYTES, offset);
56247
57145
  if (bytesRead === 0)
56248
- return null;
57146
+ return dsSchema;
56249
57147
  for (let index = 0; index < bytesRead; index++) {
56250
57148
  offset++;
56251
57149
  if (buffer[index] !== NEWLINE_BYTE) continue;
@@ -56255,14 +57153,15 @@ var init_ConsumerExecutor = __esm({
56255
57153
  }
56256
57154
  const { size } = await handle.stat();
56257
57155
  if (offset >= size)
56258
- return null;
57156
+ return dsSchema;
56259
57157
  await handle.truncate(offset);
56260
- return limit;
57158
+ return DatasetSchema_default.rewritten(dsSchema, "limit", limit);
56261
57159
  } finally {
56262
57160
  await handle.close();
56263
57161
  }
56264
57162
  };
56265
- this.processDistinct = async (datasetPath) => {
57163
+ this.processDistinct = async (dsSchema, datasetPath) => {
57164
+ Affirm_default(dsSchema, "Invalid dataset schema");
56266
57165
  const reader = import_fs21.default.createReadStream(datasetPath);
56267
57166
  const tempWorkPath = datasetPath + "_tmp";
56268
57167
  const writer = import_fs21.default.createWriteStream(tempWorkPath);
@@ -56291,18 +57190,18 @@ var init_ConsumerExecutor = __esm({
56291
57190
  }
56292
57191
  await import_promises16.default.unlink(datasetPath);
56293
57192
  await import_promises16.default.rename(tempWorkPath, datasetPath);
56294
- return newLineCount;
57193
+ return DatasetSchema_default.rewritten(dsSchema, "distinct", newLineCount);
56295
57194
  };
56296
- this.processDistinctOn = async (consumer, datasetPath) => {
57195
+ this.processDistinctOn = async (consumer, dsSchema, datasetPath) => {
57196
+ Affirm_default(dsSchema, "Invalid dataset schema");
56297
57197
  const reader = import_fs21.default.createReadStream(datasetPath);
56298
57198
  const { distinctOn } = consumer.options;
56299
57199
  const { keys, resolution } = distinctOn;
56300
57200
  const { strategy, orderBy, direction = "asc" } = resolution;
56301
- const internalRecordFormat = OutputExecutor_default._getInternalRecordFormat(consumer);
56302
- const internalFields = ConsumerManager_default.getExpandedFields(consumer);
57201
+ const parseLine = LineParser_default.datasetParser(dsSchema);
56303
57202
  const winners = /* @__PURE__ */ new Map();
56304
57203
  for await (const line of readLines(reader)) {
56305
- const record = internalRecordFormat === "CSV" ? LineParser_default._internalParseCSV(line, internalFields) : LineParser_default._internalParseJSON(line);
57204
+ const record = parseLine(line);
56306
57205
  const compositeKey = keys.map((k7) => String(record[k7] ?? "")).join("|");
56307
57206
  const existing = winners.get(compositeKey);
56308
57207
  if (!existing) {
@@ -56334,20 +57233,27 @@ var init_ConsumerExecutor = __esm({
56334
57233
  }
56335
57234
  await import_promises16.default.unlink(datasetPath);
56336
57235
  await import_promises16.default.rename(tempWorkPath, datasetPath);
56337
- return winners.size;
57236
+ return DatasetSchema_default.rewritten(dsSchema, "distinctOn", winners.size);
56338
57237
  };
56339
- this.processPivot = async (consumer, datasetPath) => {
57238
+ /**
57239
+ * Groups the dataset by `rowKeys` and turns one column's values into columns of their own.
57240
+ *
57241
+ * This is the one pass that changes the SHAPE of the file, which is why it returns the schema it
57242
+ * wrote rather than a row count: the header, the split pass, the dataset validation and the
57243
+ * parquet schema all read the result.
57244
+ */
57245
+ this.processPivot = async (consumer, dsSchema, datasetPath) => {
57246
+ Affirm_default(dsSchema, "Invalid dataset schema");
56340
57247
  const { pivot } = consumer.options;
56341
57248
  const { rowKeys, pivotColumn, valueColumn, aggregation, columnPrefix = "" } = pivot;
56342
- const internalRecordFormat = OutputExecutor_default._getInternalRecordFormat(consumer);
56343
- const internalFields = ConsumerManager_default.getExpandedFields(consumer);
57249
+ const parseLine = LineParser_default.datasetParser(dsSchema);
56344
57250
  let pivotValues = pivot.pivotValues;
56345
57251
  if (!pivotValues) {
56346
57252
  pivotValues = [];
56347
57253
  const discoverySet = /* @__PURE__ */ new Set();
56348
57254
  const discoverReader = import_fs21.default.createReadStream(datasetPath);
56349
57255
  for await (const line of readLines(discoverReader)) {
56350
- const record = this._parseLine(line, internalRecordFormat, internalFields);
57256
+ const record = parseLine(line);
56351
57257
  const val = String(record[pivotColumn] ?? "");
56352
57258
  if (!discoverySet.has(val)) {
56353
57259
  discoverySet.add(val);
@@ -56364,7 +57270,7 @@ var init_ConsumerExecutor = __esm({
56364
57270
  const groups = /* @__PURE__ */ new Map();
56365
57271
  const reader = import_fs21.default.createReadStream(datasetPath);
56366
57272
  for await (const line of readLines(reader)) {
56367
- const record = this._parseLine(line, internalRecordFormat, internalFields);
57273
+ const record = parseLine(line);
56368
57274
  const compositeKey = rowKeys.map((k7) => String(record[k7] ?? "")).join("|");
56369
57275
  const pivotVal = String(record[pivotColumn] ?? "");
56370
57276
  const numericVal = Number(record[valueColumn]) || 0;
@@ -56383,11 +57289,8 @@ var init_ConsumerExecutor = __esm({
56383
57289
  cell.min = Math.min(cell.min, numericVal);
56384
57290
  cell.max = Math.max(cell.max, numericVal);
56385
57291
  }
56386
- const pivotedFields = [
56387
- ...rowKeys.map((k7) => ({ cField: { key: k7 }, finalKey: k7 })),
56388
- ...pivotValues.map((pv) => ({ cField: { key: columnPrefix + pv }, finalKey: columnPrefix + pv }))
56389
- ];
56390
- const outputPlan = OutputExecutor_default.compileOutput(consumer, pivotedFields);
57292
+ const pivotedDsSchema = DatasetSchema_default.forPivot(dsSchema, rowKeys, pivotValues.map((pv) => columnPrefix + pv), groups.size);
57293
+ const outputPlan = OutputExecutor_default.compileOutput(consumer, pivotedDsSchema);
56391
57294
  const tempWorkPath = datasetPath + "_tmp";
56392
57295
  const writer = import_fs21.default.createWriteStream(tempWorkPath);
56393
57296
  const waitForDrain = () => new Promise((resolve) => writer.once("drain", resolve));
@@ -56437,10 +57340,8 @@ var init_ConsumerExecutor = __esm({
56437
57340
  }
56438
57341
  await import_promises16.default.unlink(datasetPath);
56439
57342
  await import_promises16.default.rename(tempWorkPath, datasetPath);
56440
- return outputCount;
56441
- };
56442
- this._parseLine = (line, format4, fields) => {
56443
- return format4 === "CSV" ? LineParser_default._internalParseCSV(line, fields) : LineParser_default._internalParseJSON(line);
57343
+ Affirm_default(outputCount === pivotedDsSchema.rows, `Pivot wrote ${outputCount} row(s) but declared ${pivotedDsSchema.rows}`);
57344
+ return pivotedDsSchema;
56444
57345
  };
56445
57346
  /**
56446
57347
  * Determines if the new record should replace the existing record based on the resolution strategy
@@ -56467,11 +57368,11 @@ var init_ConsumerExecutor = __esm({
56467
57368
  return false;
56468
57369
  }
56469
57370
  };
56470
- this.processDatasetValidation = async (consumer, datasetPath) => {
57371
+ this.processDatasetValidation = async (consumer, dsSchema, datasetPath) => {
57372
+ Affirm_default(dsSchema, "Invalid dataset schema");
56471
57373
  const validations = consumer.validate;
56472
57374
  if (!validations || validations.length === 0) return [];
56473
- const internalRecordFormat = OutputExecutor_default._getInternalRecordFormat(consumer);
56474
- const internalFields = ConsumerManager_default.getExpandedFields(consumer);
57375
+ const parseLine = LineParser_default.datasetParser(dsSchema);
56475
57376
  let rowCount = 0;
56476
57377
  const seenRows = /* @__PURE__ */ new Set();
56477
57378
  const fieldValueSets = /* @__PURE__ */ new Map();
@@ -56492,7 +57393,7 @@ var init_ConsumerExecutor = __esm({
56492
57393
  seenRows.add(line);
56493
57394
  }
56494
57395
  if (uniqueFieldKeys.length > 0) {
56495
- const record = internalRecordFormat === "CSV" ? LineParser_default._internalParseCSV(line, internalFields) : LineParser_default._internalParseJSON(line);
57396
+ const record = parseLine(line);
56496
57397
  for (const fieldKey of uniqueFieldKeys) {
56497
57398
  const valueSet = fieldValueSets.get(fieldKey);
56498
57399
  const val = String(record[fieldKey] ?? "");
@@ -56629,14 +57530,13 @@ var init_ExecutorPerformance = __esm({
56629
57530
  });
56630
57531
 
56631
57532
  // ../../packages/executors/src/ExecutorWriter.ts
56632
- var fs26, ExecutorWriter, ExecutorWriter_default;
57533
+ var fs28, ExecutorWriter, ExecutorWriter_default;
56633
57534
  var init_ExecutorWriter = __esm({
56634
57535
  "../../packages/executors/src/ExecutorWriter.ts"() {
56635
- fs26 = __toESM(require("fs"));
57536
+ fs28 = __toESM(require("fs"));
56636
57537
  init_src();
56637
57538
  init_src3();
56638
57539
  init_src10();
56639
- init_OutputExecutor();
56640
57540
  ExecutorWriter = class {
56641
57541
  constructor() {
56642
57542
  /**
@@ -56666,7 +57566,8 @@ var init_ExecutorWriter = __esm({
56666
57566
  Affirm_default(request, "Invalid split request");
56667
57567
  Affirm_default(request.sourcePath, "Missing dataset to split");
56668
57568
  Affirm_default(request.consumer, "Invalid consumer");
56669
- const { scope, sourcePath, consumer, fields } = request;
57569
+ const { scope, sourcePath, consumer, dsSchema } = request;
57570
+ Affirm_default(dsSchema, "Missing dataset schema to split against");
56670
57571
  const keys = scope.split?.by ?? [];
56671
57572
  const maxFileBytes = scope.split?.maxFileSizeGb > 0 ? scope.split.maxFileSizeGb * this.BYTES_PER_GB : null;
56672
57573
  if (keys.length === 0 && !maxFileBytes)
@@ -56681,21 +57582,21 @@ var init_ExecutorWriter = __esm({
56681
57582
  usedLabels: /* @__PURE__ */ new Set(),
56682
57583
  fileCount: 0
56683
57584
  };
56684
- await this._route(pass, fields);
57585
+ await this._route(pass, dsSchema);
56685
57586
  if (pass.buckets.size === 0)
56686
57587
  return [{ path: sourcePath, label: this.UNSPLIT_LABEL }];
56687
- await fs26.promises.unlink(sourcePath);
57588
+ await fs28.promises.unlink(sourcePath);
56688
57589
  return this._listFiles(pass);
56689
57590
  };
56690
57591
  /**
56691
57592
  * The pass itself: one line read, one line written, for every line of the dataset.
56692
57593
  */
56693
- this._route = async (pass, fields) => {
56694
- const internalFormat = OutputExecutor_default._getInternalRecordFormat(pass.consumer);
56695
- const readStream = fs26.createReadStream(pass.sourcePath);
57594
+ this._route = async (pass, dsSchema) => {
57595
+ const parseLine = pass.keys.length === 0 ? null : LineParser_default.datasetParser(dsSchema);
57596
+ const readStream = fs28.createReadStream(pass.sourcePath);
56696
57597
  try {
56697
57598
  for await (const line of readLines(readStream)) {
56698
- const bucket = this._bucketFor(line, pass, internalFormat, fields);
57599
+ const bucket = this._bucketFor(line, pass, parseLine);
56699
57600
  const lineBytes = Buffer.byteLength(line) + 1;
56700
57601
  if (this._needsRotation(bucket, pass, lineBytes))
56701
57602
  await this._rotate(bucket, pass);
@@ -56715,8 +57616,8 @@ var init_ExecutorWriter = __esm({
56715
57616
  * own: a shared catch-all would silently merge two different things, and that is invisible until
56716
57617
  * it is wrong.
56717
57618
  */
56718
- this._bucketFor = (line, pass, internalFormat, fields) => {
56719
- const bucketKey = pass.keys.length === 0 ? "" : this._bucketKey(line, pass.keys, internalFormat, fields);
57619
+ this._bucketFor = (line, pass, parseLine) => {
57620
+ const bucketKey = parseLine ? this._bucketKey(line, pass.keys, parseLine) : "";
56720
57621
  const existing = pass.buckets.get(bucketKey);
56721
57622
  if (existing) return existing;
56722
57623
  const bucket = {
@@ -56730,8 +57631,8 @@ var init_ExecutorWriter = __esm({
56730
57631
  return bucket;
56731
57632
  };
56732
57633
  /** The composite value a line buckets on, read off the parsed record. */
56733
- this._bucketKey = (line, keys, internalFormat, fields) => {
56734
- const record = internalFormat === "CSV" ? LineParser_default._internalParseCSV(line, fields) : LineParser_default._internalParseJSON(line);
57634
+ this._bucketKey = (line, keys, parseLine) => {
57635
+ const record = parseLine(line);
56735
57636
  return keys.map((key) => {
56736
57637
  const value = record[key];
56737
57638
  if (value === null || value === void 0) return "_null";
@@ -56796,7 +57697,7 @@ var init_ExecutorWriter = __esm({
56796
57697
  }
56797
57698
  if (pass.openStreams.length >= Constants_default.defaults.MAX_OPEN_SPLIT_STREAMS)
56798
57699
  await this._closeStream(pass.openStreams.shift());
56799
- bucket.stream = fs26.createWriteStream(this.getCompletedPath(pass.sourcePath, this._composeLabel(bucket, pass)), { flags: "a" });
57700
+ bucket.stream = fs28.createWriteStream(this.getCompletedPath(pass.sourcePath, this._composeLabel(bucket, pass)), { flags: "a" });
56800
57701
  pass.openStreams.push(bucket);
56801
57702
  return bucket.stream;
56802
57703
  };
@@ -57085,7 +57986,6 @@ var init_LookupResolver = __esm({
57085
57986
  init_src4();
57086
57987
  init_src10();
57087
57988
  init_src2();
57088
- init_OutputExecutor();
57089
57989
  LookupResolverClass = class {
57090
57990
  constructor() {
57091
57991
  // Reuse dicts across sibling consumers within one process; invalidated when the config changes.
@@ -57108,28 +58008,35 @@ var init_LookupResolver = __esm({
57108
58008
  Affirm_default(!stack.includes(ref.consumer), `code_lookup cycle detected: ${[...stack, ref.consumer].join(" -> ")}`);
57109
58009
  const lookupConsumer = Environment_default.getConsumer(ref.consumer);
57110
58010
  Affirm_default(lookupConsumer, `code_lookup references unknown consumer "${ref.consumer}"`);
57111
- let capturePath = captureByConsumer.get(ref.consumer);
57112
- if (!capturePath) {
57113
- capturePath = this.getCapturePath(scope, ref.consumer);
58011
+ let capture = captureByConsumer.get(ref.consumer);
58012
+ if (!capture) {
58013
+ const capturePath = this.getCapturePath(scope, ref.consumer);
57114
58014
  ExecutorScope_default.ensurePath(capturePath);
57115
- await this.runLookupConsumer(lookupConsumer, capturePath, stack, request);
57116
- captureByConsumer.set(ref.consumer, capturePath);
58015
+ capture = {
58016
+ path: capturePath,
58017
+ dsSchema: await this.runLookupConsumer(lookupConsumer, capturePath, stack, request)
58018
+ };
58019
+ captureByConsumer.set(ref.consumer, capture);
57117
58020
  }
57118
- this.cache.set(id, await this.buildDict(lookupConsumer, capturePath, ref));
58021
+ this.cache.set(id, await this.buildDict(capture, ref));
57119
58022
  }
57120
58023
  ExecutorScope_default.ensurePath(outPath);
57121
58024
  await import_promises17.default.writeFile(outPath, JSON.stringify(this.cache.get(id)));
57122
58025
  manifest.push({ id, consumer: ref.consumer, key: ref.key, value: ref.value, path: import_path22.default.resolve(outPath) });
57123
58026
  }
57124
- for (const capturePath of captureByConsumer.values())
57125
- await import_promises17.default.rm(capturePath, { force: true });
58027
+ for (const capture of captureByConsumer.values())
58028
+ await import_promises17.default.rm(capture.path, { force: true });
57126
58029
  return manifest;
57127
58030
  };
57128
- // Runs the source consumer through the real pipeline, capturing its merged output before cleanup.
58031
+ /**
58032
+ * Runs the source consumer through the real pipeline, capturing its merged output before
58033
+ * cleanup and returning the shape that run produced. The capture is a verbatim copy of the
58034
+ * dataset, so its schema is the ONLY description of it.
58035
+ */
57129
58036
  this.runLookupConsumer = async (lookupConsumer, capturePath, stack, request) => {
57130
58037
  const { default: ExecutorOrchestrator2 } = await Promise.resolve().then(() => (init_ExecutorOrchestrator(), ExecutorOrchestrator_exports));
57131
58038
  Logger_default.log(`[lookup] Materializing consumer "${lookupConsumer.name}" (path: ${stack.join(" -> ")})`);
57132
- await ExecutorOrchestrator2.launch({
58039
+ const result = await ExecutorOrchestrator2.launch({
57133
58040
  consumer: lookupConsumer,
57134
58041
  details: request.details,
57135
58042
  captureMainDatasetTo: capturePath,
@@ -57137,22 +58044,23 @@ var init_LookupResolver = __esm({
57137
58044
  _lookupStack: stack,
57138
58045
  syntheticProducerInputs: request.syntheticProducerInputs
57139
58046
  });
58047
+ Affirm_default(result.dsSchema, `code_lookup source consumer "${lookupConsumer.name}" reported no dataset schema`);
58048
+ return result.dsSchema;
57140
58049
  };
57141
58050
  // Projects the captured dataset to a { code: value } dict, bounded by MAX_LOOKUP_ROWS.
57142
- this.buildDict = async (lookupConsumer, capturePath, ref) => {
57143
- const format4 = OutputExecutor_default._getInternalRecordFormat(lookupConsumer);
57144
- const visibleFields = ConsumerManager_default.getExpandedFields(lookupConsumer).filter((f7) => !f7.cField.hidden);
57145
- const finalKeys = visibleFields.map((f7) => f7.finalKey);
57146
- Affirm_default(finalKeys.includes(ref.key), `code_lookup key column "${ref.key}" is not in the output of consumer "${ref.consumer}"`);
57147
- Affirm_default(finalKeys.includes(ref.value), `code_lookup value column "${ref.value}" is not in the output of consumer "${ref.consumer}"`);
58051
+ this.buildDict = async (capture, ref) => {
58052
+ const columnNames = capture.dsSchema.columns.map((column) => column.name);
58053
+ Affirm_default(columnNames.includes(ref.key), `code_lookup key column "${ref.key}" is not in the output of consumer "${ref.consumer}"`);
58054
+ Affirm_default(columnNames.includes(ref.value), `code_lookup value column "${ref.value}" is not in the output of consumer "${ref.consumer}"`);
58055
+ const parseLine = LineParser_default.datasetParser(capture.dsSchema);
57148
58056
  const dict = {};
57149
58057
  let rowsRead = 0;
57150
58058
  let warnedDup = false;
57151
- for await (const line of readLines(import_fs23.default.createReadStream(capturePath))) {
58059
+ for await (const line of readLines(import_fs23.default.createReadStream(capture.path))) {
57152
58060
  if (!line.length) continue;
57153
58061
  rowsRead++;
57154
58062
  Affirm_default(rowsRead <= Constants_default.defaults.MAX_LOOKUP_ROWS, `code_lookup table from consumer "${ref.consumer}" exceeds MAX_LOOKUP_ROWS (${Constants_default.defaults.MAX_LOOKUP_ROWS})`);
57155
- const record = this.parseLine(line, format4, visibleFields);
58063
+ const record = parseLine(line);
57156
58064
  const code = String(record[ref.key]);
57157
58065
  if (code in dict && !warnedDup) {
57158
58066
  Logger_default.warn(`code_lookup: duplicate key(s) in consumer "${ref.consumer}" for "${ref.key}"; last value wins`);
@@ -57163,10 +58071,6 @@ var init_LookupResolver = __esm({
57163
58071
  Logger_default.log(`[lookup] Built table "${ref.consumer}" (${ref.key}->${ref.value}): ${Object.keys(dict).length} entr(ies) from ${rowsRead} row(s)`);
57164
58072
  return dict;
57165
58073
  };
57166
- this.parseLine = (line, format4, visibleFields) => {
57167
- if (format4 === "CSV") return LineParser_default._internalParseCSV(line, visibleFields, ",");
57168
- return JSON.parse(line);
57169
- };
57170
58074
  this.getCapturePath = (scope, consumerName) => {
57171
58075
  const dir = import_path22.default.dirname(ExecutorScope_default.getLookupPath(scope, "x"));
57172
58076
  return import_path22.default.join(dir, `${consumerName}.capture`);
@@ -57200,7 +58104,6 @@ var init_ConsumerInputResolver = __esm({
57200
58104
  init_src();
57201
58105
  init_src3();
57202
58106
  init_src4();
57203
- init_src10();
57204
58107
  init_src2();
57205
58108
  ConsumerInputResolverClass = class {
57206
58109
  constructor() {
@@ -57225,23 +58128,29 @@ var init_ConsumerInputResolver = __esm({
57225
58128
  Affirm_default(upstream, `consumer-as-input references unknown consumer "${name}"`);
57226
58129
  const capturePath = ExecutorScope_default.getConsumerInputPath(scope, name);
57227
58130
  ExecutorScope_default.ensurePath(capturePath);
57228
- await this.runUpstreamConsumer(upstream, capturePath, stack, request);
58131
+ const dsSchema = await this.runUpstreamConsumer(upstream, capturePath, stack, request);
57229
58132
  const { size } = await import_promises18.default.stat(capturePath);
57230
- this.cache.set(name, { consumer: name, path: import_path23.default.resolve(capturePath), fileSize: size });
58133
+ this.cache.set(name, { consumer: name, path: import_path23.default.resolve(capturePath), fileSize: size, dsSchema });
57231
58134
  }
57232
58135
  manifest.set(name, this.cache.get(name));
57233
58136
  }
57234
58137
  return manifest;
57235
58138
  };
57236
- // Builds the in-memory producer the orchestrator reads the JSONL capture through. Its dimensions
57237
- // are the upstream consumer's visible output fields, matching the keys written into the capture.
57238
- this.buildSyntheticProducer = (upstreamConsumer) => {
57239
- Affirm_default(upstreamConsumer, "Invalid upstream consumer");
57240
- const dimensions = ConsumerManager_default.getExpandedFields(upstreamConsumer).filter((field) => !field.cField.hidden).map((field) => ({ name: field.finalKey, type: field.dimension?.type ?? "string" }));
58139
+ /**
58140
+ * Builds the in-memory producer the orchestrator reads the JSONL capture through. Its dimensions
58141
+ * are the columns the upstream run REPORTED writing, not the consumer's declared fields, which a
58142
+ * `pivot` leaves describing a shape no longer in the file.
58143
+ *
58144
+ * No `format` is carried over: the capture holds values the upstream run already rendered, so
58145
+ * re-parsing them with that dimension's input format would fail on its own output.
58146
+ */
58147
+ this.buildSyntheticProducer = (entry) => {
58148
+ Affirm_default(entry, "Invalid consumer input entry");
58149
+ Affirm_default(entry.dsSchema, `Consumer input "${entry.consumer}" carries no dataset schema`);
57241
58150
  return {
57242
- name: upstreamConsumer.name,
57243
- source: `__consumer_input__:${upstreamConsumer.name}`,
57244
- dimensions,
58151
+ name: entry.consumer,
58152
+ source: `__consumer_input__:${entry.consumer}`,
58153
+ dimensions: entry.dsSchema.columns.map((column) => ({ name: column.name, type: column.type })),
57245
58154
  settings: { fileType: "JSONL" },
57246
58155
  _version: 1
57247
58156
  };
@@ -57256,11 +58165,12 @@ var init_ConsumerInputResolver = __esm({
57256
58165
  }
57257
58166
  return names;
57258
58167
  };
57259
- // Runs the upstream consumer through the real pipeline, capturing its full output as JSONL.
58168
+ // Runs the upstream consumer through the real pipeline, capturing its output as JSONL and
58169
+ // reporting the shape that run produced.
57260
58170
  this.runUpstreamConsumer = async (upstream, capturePath, stack, request) => {
57261
58171
  const { default: ExecutorOrchestrator2 } = await Promise.resolve().then(() => (init_ExecutorOrchestrator(), ExecutorOrchestrator_exports));
57262
58172
  Logger_default.log(`[consumer-input] Materializing consumer "${upstream.name}" (path: ${stack.join(" -> ")})`);
57263
- await ExecutorOrchestrator2.launch({
58173
+ const result = await ExecutorOrchestrator2.launch({
57264
58174
  consumer: upstream,
57265
58175
  details: request.details,
57266
58176
  captureMainDatasetTo: capturePath,
@@ -57269,6 +58179,8 @@ var init_ConsumerInputResolver = __esm({
57269
58179
  _consumerInputStack: stack,
57270
58180
  syntheticProducerInputs: request.syntheticProducerInputs
57271
58181
  });
58182
+ Affirm_default(result.dsSchema, `consumer-as-input source "${upstream.name}" reported no dataset schema`);
58183
+ return result.dsSchema;
57272
58184
  };
57273
58185
  this.invalidateCacheIfStale = () => {
57274
58186
  const generation = Environment_default._configHash ?? null;
@@ -57310,6 +58222,7 @@ var init_ExecutorOrchestrator = __esm({
57310
58222
  init_src();
57311
58223
  init_src10();
57312
58224
  init_ExecutorWriter();
58225
+ init_DatasetSchema();
57313
58226
  import_promises20 = require("stream/promises");
57314
58227
  init_src2();
57315
58228
  init_ExecutorProgress2();
@@ -57328,9 +58241,9 @@ var init_ExecutorOrchestrator = __esm({
57328
58241
  * spawn lazily, on first exec, and a thread that dies mid-run is replaced by another, so this is
57329
58242
  * a count of spawns and NOT a concurrency figure (see IWorkerCensus).
57330
58243
  *
57331
- * `workerData` carries the loaded environment, so it is cloned once per THREAD at spawn rather
57332
- * than read and validated from disk once per chunk. Only run-constant data belongs there; per-task
57333
- * data goes in the exec argument, which is cloned again for every chunk.
58244
+ * `workerData` carries the loaded environment and the run's dataset schema, so both are cloned
58245
+ * once per THREAD at spawn rather than per chunk. Only run-constant data belongs there; per-task
58246
+ * data goes in the exec argument, cloned per chunk.
57334
58247
  */
57335
58248
  this.createPool = (maxWorkers, census, threadData2) => {
57336
58249
  const options2 = {
@@ -57361,13 +58274,19 @@ var init_ExecutorOrchestrator = __esm({
57361
58274
  const tracker = new ExecutorPerformance_default();
57362
58275
  const _progress = new ExecutorProgress2_default(logProgress);
57363
58276
  const { usageId } = UsageManager_default.startUsage(consumer, details);
57364
- const scope = { id: usageId, folder: `${consumer.name}_${usageId}`, workersId: [], split: OrchestratorHelper_default.resolveSplit(consumer) };
58277
+ const scope = ExecutorScope_default.open({ id: usageId, folder: `${consumer.name}_${usageId}`, workersId: [], split: OrchestratorHelper_default.resolveSplit(consumer) });
57365
58278
  const expandedFields = ConsumerManager_default.getExpandedFields(consumer);
58279
+ let dsSchema = DatasetSchema_default.compile(consumer, expandedFields);
57366
58280
  const census = { created: 0, peak: 0 };
57367
58281
  try {
57368
58282
  const start = performance.now();
57369
58283
  const executorResults = [];
57370
58284
  Logger_default.info(`[${usageId}] Launching consumer "${consumer.name}" (invoked by: ${details.invokedBy}, user: ${details.user?.name ?? "unknown"}, producer(s): ${consumer.producers.length}${OrchestratorHelper_default.formatRequestOptions(options2)}) | ${OrchestratorHelper_default.formatMemoryUsage()}`);
58285
+ const fixedReview = request.skipExport ? null : OutputExecutor_default.reviewFixedOutput(consumer, dsSchema);
58286
+ Affirm_default(
58287
+ !fixedReview || fixedReview.errors.length === 0,
58288
+ `Consumer "${consumer.name}" cannot write its FIXED output: ${fixedReview?.errors.join(" ")}`
58289
+ );
57371
58290
  const lookups = await LookupResolver_default.materialize(consumer, expandedFields, scope, request);
57372
58291
  const consumerInputs = await ConsumerInputResolver_default.materialize(consumer, scope, request);
57373
58292
  let counter = performance.now();
@@ -57397,6 +58316,7 @@ var init_ExecutorOrchestrator = __esm({
57397
58316
  executorResults.push(...await this.runWorkFiles({
57398
58317
  workFiles,
57399
58318
  consumer,
58319
+ dsSchema,
57400
58320
  scope,
57401
58321
  options: options2,
57402
58322
  lookups,
@@ -57411,58 +58331,26 @@ var init_ExecutorOrchestrator = __esm({
57411
58331
  Logger_default.log(`[${usageId}] All workers complete \u2014 ${executorResults.length} result(s), total input: ${Algo_default.sum(executorResults.map((x2) => x2.inputCount))}, total output: ${Algo_default.sum(executorResults.map((x2) => x2.outputCount))}`);
57412
58332
  _progress.update({ phase: "Reconciling work", progress: 0 });
57413
58333
  await this.reconcileExecutorThreadsResults(scope, executorResults, tracker);
58334
+ dsSchema = DatasetSchema_default.rewritten(dsSchema, "merge", Algo_default.sum(executorResults.map((x2) => x2.outputCount)));
57414
58335
  _progress.update({ phase: "Reconciling work", progress: 1 });
57415
58336
  Logger_default.log(`[${usageId}] Reconciled ${executorResults.length} worker result(s)`);
57416
58337
  _progress.update({ phase: "Post-processing", progress: 0 });
57417
- const postOperation = { totalOutputCount: null };
57418
- if (executorResults.length > 1) {
57419
- if (consumer.options?.distinct === true) {
57420
- Logger_default.log(`[${usageId}] Running unified distinct pass across merged workers`);
57421
- counter = performance.now();
57422
- const unifiedOutputCount = await ConsumerExecutor_default.processDistinct(ExecutorScope_default.getMainPath(scope));
57423
- tracker.measure("process-distinct:main", performance.now() - counter);
57424
- postOperation.totalOutputCount = unifiedOutputCount;
57425
- Logger_default.log(`[${usageId}] Distinct pass complete: ${unifiedOutputCount} unique rows in ${Math.round(performance.now() - counter)}ms`);
57426
- }
57427
- if (consumer.options?.distinctOn) {
57428
- Logger_default.log(`[${usageId}] Running unified distinctOn pass (${consumer.options.distinctOn})`);
57429
- counter = performance.now();
57430
- const unifiedOutputCount = await ConsumerExecutor_default.processDistinctOn(consumer, ExecutorScope_default.getMainPath(scope));
57431
- tracker.measure("process-distinct-on:main", performance.now() - counter);
57432
- postOperation.totalOutputCount = unifiedOutputCount;
57433
- Logger_default.log(`[${usageId}] DistinctOn pass complete: ${unifiedOutputCount} rows in ${Math.round(performance.now() - counter)}ms`);
57434
- }
57435
- if (consumer.options?.pivot) {
57436
- Logger_default.log(`[${usageId}] Running pivot operation`);
57437
- counter = performance.now();
57438
- const unifiedOutputCount = await ConsumerExecutor_default.processPivot(consumer, ExecutorScope_default.getMainPath(scope));
57439
- tracker.measure("process-pivot:main", performance.now() - counter);
57440
- postOperation.totalOutputCount = unifiedOutputCount;
57441
- Logger_default.log(`[${usageId}] Pivot complete: ${unifiedOutputCount} rows in ${Math.round(performance.now() - counter)}ms`);
57442
- }
57443
- if (ConsumerManager_default.hasLineCountFields(consumer, true)) {
57444
- Logger_default.log(`[${usageId}] Running unified lineCount pass across merged workers`);
57445
- counter = performance.now();
57446
- const processedLineCount = await ConsumerExecutor_default.processLineCount(consumer, ExecutorScope_default.getMainPath(scope), executorResults);
57447
- tracker.measure("process-line-count:main", performance.now() - counter);
57448
- Logger_default.log(`[${usageId}] LineCount pass complete: normalized ${processedLineCount} rows in ${Math.round(performance.now() - counter)}ms`);
57449
- }
57450
- }
58338
+ dsSchema = await this.runPostPasses({ consumer, dsSchema, scope, executorResults, tracker, usageId });
57451
58339
  if (options2?.limit > 0) {
57452
58340
  counter = performance.now();
57453
- const limitedOutputCount = await ConsumerExecutor_default.processLimit(ExecutorScope_default.getMainPath(scope), options2.limit);
58341
+ const limited = await ConsumerExecutor_default.processLimit(dsSchema, ExecutorScope_default.getMainPath(scope), options2.limit);
57454
58342
  tracker.measure("process-limit:main", performance.now() - counter);
57455
- if (Algo_default.hasVal(limitedOutputCount)) {
57456
- postOperation.totalOutputCount = limitedOutputCount;
57457
- Logger_default.log(`[${usageId}] Record limit applied: dataset cut to ${limitedOutputCount} row(s) in ${Math.round(performance.now() - counter)}ms`);
58343
+ if (limited.writtenBy === "limit") {
58344
+ Logger_default.log(`[${usageId}] Record limit applied: dataset cut to ${limited.rows} row(s) in ${Math.round(performance.now() - counter)}ms`);
57458
58345
  } else {
57459
58346
  Logger_default.log(`[${usageId}] Record limit of ${options2.limit} not reached \u2014 dataset left whole`);
57460
58347
  }
58348
+ dsSchema = limited;
57461
58349
  }
57462
58350
  if (consumer.validate && consumer.validate.length > 0) {
57463
58351
  Logger_default.log(`[${usageId}] Running dataset-level validations`);
57464
58352
  counter = performance.now();
57465
- const validationResults = await ConsumerExecutor_default.processDatasetValidation(consumer, ExecutorScope_default.getMainPath(scope));
58353
+ const validationResults = await ConsumerExecutor_default.processDatasetValidation(consumer, dsSchema, ExecutorScope_default.getMainPath(scope));
57466
58354
  tracker.measure("dataset-validation", performance.now() - counter);
57467
58355
  for (const result of validationResults) {
57468
58356
  if (result.onFail === "fail") {
@@ -57476,7 +58364,7 @@ var init_ExecutorOrchestrator = __esm({
57476
58364
  Logger_default.log(`[${usageId}] Dataset validations complete in ${Math.round(performance.now() - counter)}ms`);
57477
58365
  }
57478
58366
  if (request.captureMainDatasetTo) {
57479
- await this.captureDataset(consumer, expandedFields, ExecutorScope_default.getMainPath(scope), request.captureMainDatasetTo, request.captureFormat);
58367
+ await this.captureDataset(dsSchema, ExecutorScope_default.getMainPath(scope), request.captureMainDatasetTo, request.captureFormat);
57480
58368
  Logger_default.log(`[${usageId}] Captured dataset to ${request.captureMainDatasetTo}${request.captureFormat ? ` (as ${request.captureFormat})` : ""}`);
57481
58369
  }
57482
58370
  if (request.skipExport) {
@@ -57485,8 +58373,8 @@ var init_ExecutorOrchestrator = __esm({
57485
58373
  const materializeResult = OrchestratorHelper_default.computeFinalResult(tracker, executorResults, usageId, null);
57486
58374
  materializeResult.elapsedMS = performance.now() - start;
57487
58375
  materializeResult.peakWorkers = census.peak;
57488
- if (Algo_default.hasVal(postOperation.totalOutputCount))
57489
- materializeResult.outputCount = postOperation.totalOutputCount;
58376
+ materializeResult.outputCount = dsSchema.rows;
58377
+ materializeResult.dsSchema = dsSchema;
57490
58378
  UsageManager_default.endUsage(usageId, materializeResult.outputCount, materializeResult);
57491
58379
  await Logger_default.flush();
57492
58380
  return materializeResult;
@@ -57496,7 +58384,7 @@ var init_ExecutorOrchestrator = __esm({
57496
58384
  scope,
57497
58385
  sourcePath: ExecutorScope_default.getMainPath(scope),
57498
58386
  consumer,
57499
- fields: expandedFields
58387
+ dsSchema
57500
58388
  });
57501
58389
  tracker.measure("split-output", performance.now() - counter);
57502
58390
  if (scope.split)
@@ -57505,7 +58393,7 @@ var init_ExecutorOrchestrator = __esm({
57505
58393
  _progress.update({ phase: "Exporting result", progress: 0 });
57506
58394
  counter = performance.now();
57507
58395
  Logger_default.info(`[${usageId}] Exporting results to ${consumer.outputs.length} output(s)`);
57508
- const exportRes = await OutputExecutor_default.exportResult(consumer, expandedFields, splitFiles);
58396
+ const exportRes = await OutputExecutor_default.exportResult(consumer, dsSchema, splitFiles);
57509
58397
  tracker.measure("export-result", performance.now() - counter);
57510
58398
  Affirm_default(exportRes.res, `Consumer "${consumer.name}" failed to write its output to "${consumer.outputs.map((x2) => x2.exportDestination).join(", ")}" \u2014 see the logged driver error for the cause`);
57511
58399
  Logger_default.info(`[${usageId}] Export complete in ${Math.round(performance.now() - counter)}ms (key: ${exportRes.key})`);
@@ -57522,8 +58410,8 @@ var init_ExecutorOrchestrator = __esm({
57522
58410
  const finalResult = OrchestratorHelper_default.computeFinalResult(tracker, executorResults, usageId, exportRes.key);
57523
58411
  finalResult.elapsedMS = performance.now() - start;
57524
58412
  finalResult.peakWorkers = census.peak;
57525
- if (Algo_default.hasVal(postOperation.totalOutputCount))
57526
- finalResult.outputCount = postOperation.totalOutputCount;
58413
+ finalResult.outputCount = dsSchema.rows;
58414
+ finalResult.dsSchema = dsSchema;
57527
58415
  UsageManager_default.endUsage(usageId, finalResult.outputCount, finalResult);
57528
58416
  Logger_default.info(`[${usageId}] Consumer "${consumer.name}" completed: ${finalResult.outputCount} rows, ${finalResult.workerCount} chunk(s) on ${census.peak} thread(s), ${Math.round(finalResult.elapsedMS)}ms`);
57529
58417
  await Logger_default.flush();
@@ -57601,7 +58489,10 @@ var init_ExecutorOrchestrator = __esm({
57601
58489
  `Invalid worker pool size ${poolSize} \u2014 it must come from getParallelWorkerCount(), never from the work list`
57602
58490
  );
57603
58491
  let phase = performance.now();
57604
- const pool = this.createPool(poolSize, census, { environment: Environment_default.createSnapshot() });
58492
+ const pool = this.createPool(poolSize, census, {
58493
+ environment: Environment_default.createSnapshot(),
58494
+ dsSchema: request.dsSchema
58495
+ });
57605
58496
  tracker.measure("pool-create", performance.now() - phase);
57606
58497
  const context = {
57607
58498
  consumer: request.consumer,
@@ -57824,24 +58715,23 @@ var init_ExecutorOrchestrator = __esm({
57824
58715
  };
57825
58716
  /**
57826
58717
  * Copies the merged main.dataset out of the run scope for a materialization run. With
57827
- * captureFormat 'JSONL' the output is normalized to newline-delimited JSON objects keyed by
57828
- * each visible field's finalKey — self-describing, so a synthetic producer can read it without a
58718
+ * captureFormat 'JSONL' the output is normalized to newline-delimited JSON objects keyed by the
58719
+ * dataset's own column names — self-describing, so a synthetic producer can read it without a
57829
58720
  * header row. main.dataset in JSON internal format is already JSONL (verbatim copy); CSV internal
57830
58721
  * format has no header row, so each row is reprojected. Without a captureFormat it's a verbatim copy.
57831
58722
  */
57832
- this.captureDataset = async (consumer, fields, sourcePath, targetPath, captureFormat) => {
58723
+ this.captureDataset = async (dsSchema, sourcePath, targetPath, captureFormat) => {
57833
58724
  await import_promises19.default.mkdir(import_path24.default.dirname(targetPath), { recursive: true });
57834
- const internalFormat = OutputExecutor_default._getInternalRecordFormat(consumer);
57835
- if (captureFormat !== "JSONL" || internalFormat !== "CSV") {
58725
+ if (captureFormat !== "JSONL" || dsSchema.encoding !== "CSV") {
57836
58726
  await import_promises19.default.copyFile(sourcePath, targetPath);
57837
58727
  return;
57838
58728
  }
57839
- const visibleFields = fields.filter((field) => !field.cField.hidden);
58729
+ const parseLine = LineParser_default.datasetParser(dsSchema);
57840
58730
  const output = import_fs25.default.createWriteStream(targetPath);
57841
58731
  try {
57842
58732
  for await (const line of readLines(import_fs25.default.createReadStream(sourcePath))) {
57843
58733
  if (!line.length) continue;
57844
- const record = LineParser_default._internalParseCSV(line, visibleFields, ",");
58734
+ const record = parseLine(line);
57845
58735
  if (!output.write(JSON.stringify(record) + "\n"))
57846
58736
  await new Promise((resolve) => output.once("drain", resolve));
57847
58737
  }
@@ -57854,7 +58744,7 @@ var init_ExecutorOrchestrator = __esm({
57854
58744
  }
57855
58745
  };
57856
58746
  /**
57857
- * Grabs the remote datasets and creates an internal .dataset to work on
58747
+ * Grabs the remote datasets and stages each one as a local `.extract` to work on
57858
58748
  */
57859
58749
  this.readySourceFiles = async (args) => {
57860
58750
  const { consumer, scope, consumerInputs, syntheticInputs, options: options2, onProgress } = args;
@@ -57864,7 +58754,7 @@ var init_ExecutorOrchestrator = __esm({
57864
58754
  const reportProducer = (fraction) => onProgress?.((i6 + fraction) / consumer.producers.length);
57865
58755
  const consumerInput = consumerInputs.get(cProd.name);
57866
58756
  if (consumerInput) {
57867
- const prod2 = ConsumerInputResolver_default.buildSyntheticProducer(Environment_default.getConsumer(cProd.name));
58757
+ const prod2 = ConsumerInputResolver_default.buildSyntheticProducer(consumerInput);
57868
58758
  const response2 = { files: [{ fullUri: consumerInput.path, fileSize: consumerInput.fileSize }] };
57869
58759
  Logger_default.log(`[${scope.id}] Producer "${cProd.name}" is a consumer input: ${consumerInput.fileSize} bytes (${i6 + 1}/${consumer.producers.length})`);
57870
58760
  results.push({ prod: prod2, cProd, response: response2 });
@@ -58013,6 +58903,49 @@ var init_ExecutorOrchestrator = __esm({
58013
58903
  }
58014
58904
  return mainPath;
58015
58905
  };
58906
+ /**
58907
+ * The passes that work across lines, run on the merged dataset. Returns the schema the last
58908
+ * pass to write the file left behind — its `rows` is the run's row count.
58909
+ *
58910
+ * `lineCount` goes FIRST: it walks each chunk's `outputCount` in order to find the worker a line
58911
+ * came from, which only holds while the file still has sum(outputCount) lines in worker order.
58912
+ */
58913
+ this.runPostPasses = async (request) => {
58914
+ const { consumer, scope, executorResults, tracker, usageId } = request;
58915
+ const datasetPath = ExecutorScope_default.getMainPath(scope);
58916
+ const isMerged = executorResults.length > 1;
58917
+ let dsSchema = request.dsSchema;
58918
+ let counter = 0;
58919
+ if (isMerged && ConsumerManager_default.hasLineCountFields(consumer, true)) {
58920
+ Logger_default.log(`[${usageId}] Running unified lineCount pass across merged workers`);
58921
+ counter = performance.now();
58922
+ dsSchema = await ConsumerExecutor_default.processLineCount(consumer, dsSchema, datasetPath, executorResults);
58923
+ tracker.measure("process-line-count:main", performance.now() - counter);
58924
+ Logger_default.log(`[${usageId}] LineCount pass complete: normalized ${dsSchema.rows} rows in ${Math.round(performance.now() - counter)}ms`);
58925
+ }
58926
+ if (isMerged && consumer.options?.distinct === true) {
58927
+ Logger_default.log(`[${usageId}] Running unified distinct pass across merged workers`);
58928
+ counter = performance.now();
58929
+ dsSchema = await ConsumerExecutor_default.processDistinct(dsSchema, datasetPath);
58930
+ tracker.measure("process-distinct:main", performance.now() - counter);
58931
+ Logger_default.log(`[${usageId}] Distinct pass complete: ${dsSchema.rows} unique rows in ${Math.round(performance.now() - counter)}ms`);
58932
+ }
58933
+ if (isMerged && consumer.options?.distinctOn) {
58934
+ Logger_default.log(`[${usageId}] Running unified distinctOn pass (${consumer.options.distinctOn})`);
58935
+ counter = performance.now();
58936
+ dsSchema = await ConsumerExecutor_default.processDistinctOn(consumer, dsSchema, datasetPath);
58937
+ tracker.measure("process-distinct-on:main", performance.now() - counter);
58938
+ Logger_default.log(`[${usageId}] DistinctOn pass complete: ${dsSchema.rows} rows in ${Math.round(performance.now() - counter)}ms`);
58939
+ }
58940
+ if (consumer.options?.pivot) {
58941
+ Logger_default.log(`[${usageId}] Running pivot operation`);
58942
+ counter = performance.now();
58943
+ dsSchema = await ConsumerExecutor_default.processPivot(consumer, dsSchema, datasetPath);
58944
+ tracker.measure("process-pivot:main", performance.now() - counter);
58945
+ Logger_default.log(`[${usageId}] Pivot complete: ${dsSchema.rows} rows in ${Math.round(performance.now() - counter)}ms`);
58946
+ }
58947
+ return dsSchema;
58948
+ };
58016
58949
  this.performCleanupOperations = async (scope, tracker) => {
58017
58950
  const start = performance.now();
58018
58951
  await ExecutorScope_default.clearScope(scope);
@@ -58034,6 +58967,7 @@ var init_ExecutorOrchestrator = __esm({
58034
58967
  // ../../packages/executors/src/index.ts
58035
58968
  init_src();
58036
58969
  init_ConsumerExecutor();
58970
+ init_DatasetSchema();
58037
58971
 
58038
58972
  // ../../packages/executors/src/Executor.ts
58039
58973
  var import_fs22 = __toESM(require("fs"));
@@ -58189,6 +59123,7 @@ var Executor = class {
58189
59123
  this.run = async (request) => {
58190
59124
  Affirm_default(request, "Invalid request");
58191
59125
  const { consumer, producer, prodDimensions, workerId, chunk, options: options2, scope, reportWork, lookups } = request;
59126
+ Affirm_default(request.dsSchema, `Worker ${workerId} was given no dataset schema to write with`);
58192
59127
  const recordLimit = this.canBoundChunk(consumer) ? options2?.limit : null;
58193
59128
  this.hydrateLookups(lookups);
58194
59129
  const counter = performance.now();
@@ -58212,7 +59147,8 @@ var Executor = class {
58212
59147
  const writer = this.openWriteStream(scope, workerId);
58213
59148
  const fields = ConsumerManager_default.getExpandedFields(consumer);
58214
59149
  const plan = RecordPlan_default.compile({ consumer, producer, fields, dimensions: prodDimensions });
58215
- const outputPlan = OutputExecutor_default.compileOutput(consumer, fields);
59150
+ let dsSchema = request.dsSchema;
59151
+ const outputPlan = OutputExecutor_default.compileOutput(consumer, dsSchema);
58216
59152
  const linePlan = ProducerExecutor_default.compileLine(prodDimensions, chunk);
58217
59153
  const recordSelector = ProducerManager_default.resolveRecordSelector(producer);
58218
59154
  const { isFirstChunk, start, end } = chunk;
@@ -58296,14 +59232,16 @@ var Executor = class {
58296
59232
  if (consumer.options?.distinct === true) {
58297
59233
  Logger_default.log(`[${workerId}] Running distinct pass`);
58298
59234
  perf = performance.now();
58299
- totalOutputCount = await ConsumerExecutor_default.processDistinct(result.resultUri);
59235
+ dsSchema = await ConsumerExecutor_default.processDistinct(dsSchema, result.resultUri);
59236
+ totalOutputCount = dsSchema.rows;
58300
59237
  this._performance.measure("process-distinct", performance.now() - perf);
58301
59238
  totalCycles++;
58302
59239
  }
58303
59240
  if (consumer.options?.distinctOn) {
58304
59241
  Logger_default.log(`[${workerId}] Running distinctOn pass (${consumer.options.distinctOn})`);
58305
59242
  perf = performance.now();
58306
- totalOutputCount = await ConsumerExecutor_default.processDistinctOn(consumer, result.resultUri);
59243
+ dsSchema = await ConsumerExecutor_default.processDistinctOn(consumer, dsSchema, result.resultUri);
59244
+ totalOutputCount = dsSchema.rows;
58307
59245
  this._performance.measure("process-distinct-on", performance.now() - perf);
58308
59246
  totalCycles++;
58309
59247
  }
@@ -58353,6 +59291,7 @@ var Executor_default = Executor;
58353
59291
  // ../../packages/executors/src/index.ts
58354
59292
  init_ExecutorOrchestrator();
58355
59293
  init_ExecutorPerformance();
59294
+ init_ExecutorWriter();
58356
59295
  init_OrchestratorHelper();
58357
59296
 
58358
59297
  // ../../packages/executors/src/workers/ExecutorWorkerRunner.ts
@@ -58361,6 +59300,8 @@ var import_dotenv = __toESM(require("dotenv"));
58361
59300
  var import_worker_threads = require("worker_threads");
58362
59301
  init_src();
58363
59302
  init_src4();
59303
+ init_src10();
59304
+ init_DatasetSchema();
58364
59305
  init_src2();
58365
59306
  import_dotenv.default.configDotenv();
58366
59307
  var MODULE_LOADED_AT = performance.timeOrigin + performance.now();
@@ -58411,6 +59352,7 @@ var runExecutorTask = async (workerData) => {
58411
59352
  chunk,
58412
59353
  options: options2,
58413
59354
  scope,
59355
+ dsSchema: resolveDsSchema(consumer),
58414
59356
  lookups,
58415
59357
  reportWork: (packet) => import_workerpool2.default.workerEmit(packet)
58416
59358
  });
@@ -58431,6 +59373,11 @@ var runExecutorTask = async (workerData) => {
58431
59373
  throw error;
58432
59374
  }
58433
59375
  };
59376
+ var resolveDsSchema = (consumer) => {
59377
+ const sent = import_worker_threads.workerData?.dsSchema;
59378
+ if (sent) return sent;
59379
+ return DatasetSchema_default.compile(consumer, ConsumerManager_default.getExpandedFields(consumer));
59380
+ };
58434
59381
  var registerExecutorWorker = () => {
58435
59382
  import_workerpool2.default.worker({
58436
59383
  executor: runExecutorTask
@@ -58507,8 +59454,7 @@ var SampleRunnerClass = class {
58507
59454
  captureMainDatasetTo: capturePath,
58508
59455
  captureFormat: "JSONL"
58509
59456
  });
58510
- const captured = await this._readCapture(capturePath, size);
58511
- const records = captured.map((record) => this._inFieldOrder(consumer, record));
59457
+ const records = await this._readCapture(capturePath, size);
58512
59458
  Logger_default.log(`[sample] Consumer "${consumer.name}": ${records.length} row(s) from ${result.inputCount} input line(s)`);
58513
59459
  return {
58514
59460
  kind: "consumer",
@@ -58556,28 +59502,6 @@ var SampleRunnerClass = class {
58556
59502
  return this._inputProducers(nested, seen);
58557
59503
  });
58558
59504
  };
58559
- /**
58560
- * One row's keys in the order the consumer DECLARES its fields.
58561
- *
58562
- * A record comes out of a worker keyed in the order the producer's dimensions were read, which is
58563
- * the source's layout and not the consumer's — so a consumer declaring country, plan, is_active
58564
- * over a producer that stores is_active first would be shown its own columns in someone else's
58565
- * order. Invisible in an exported JSON object; the whole shape of the table here.
58566
- *
58567
- * Anything the record carries that the field list does not name is appended rather than dropped:
58568
- * a column that turns up unexpectedly is exactly the one worth seeing.
58569
- */
58570
- this._inFieldOrder = (consumer, record) => {
58571
- const declared = ConsumerManager_default.getExpandedFields(consumer).filter((field) => !field.cField.hidden).map((field) => field.finalKey);
58572
- const ordered = {};
58573
- for (const key of declared) {
58574
- if (key in record) ordered[key] = record[key];
58575
- }
58576
- for (const key of Object.keys(record)) {
58577
- if (!(key in ordered)) ordered[key] = record[key];
58578
- }
58579
- return ordered;
58580
- };
58581
59505
  /** The captured dataset, which `captureFormat: 'JSONL'` guarantees is one JSON object per line. */
58582
59506
  this._readCapture = async (capturePath, size) => {
58583
59507
  if (!import_fs26.default.existsSync(capturePath)) return [];