@forzalabs/remora 2.4.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 fs32 = 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
- fs32.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
- fs32.close(fd, noop);
8574
+ fs33.close(fd, noop);
8575
8575
  return;
8576
8576
  }
8577
- return fs32.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 fs32 = 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 = fs32.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
- fs32.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 = fs32.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
- fs32.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
- fs32.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
- fs32.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
- fs32.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
- fs32.access(src, fs32.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 = fs32.createReadStream(src);
9232
- var out = fs32.createWriteStream(dest);
9231
+ var inp = fs33.createReadStream(src);
9232
+ var out = fs33.createWriteStream(dest);
9233
9233
  out.on("finish", () => {
9234
- fs32.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 (!fs32.existsSync(dirPath)) {
9241
- fs32.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.4.0",
11812
+ cliVersion: "2.5.0",
11813
11813
  backendVersion: 1,
11814
11814
  backendPort: 5088,
11815
11815
  workerVersion: 2,
@@ -11934,6 +11934,13 @@ 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.
@@ -13176,7 +13183,7 @@ var init_Validator = __esm({
13176
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.`);
13177
13184
  for (const output of consumer.outputs) {
13178
13185
  const format4 = output.format.toUpperCase();
13179
- if (format4 === "CSV" || format4 === "JSON" || format4 === "PARQUET" || format4 === "TXT") {
13186
+ if (format4 === "CSV" || format4 === "FIXED" || format4 === "JSON" || format4 === "PARQUET" || format4 === "TXT") {
13180
13187
  if (!output.exportDestination)
13181
13188
  errors.push(`A static file output must have an export destination set (${format4})`);
13182
13189
  else if (!Environment_default.getSource(output.exportDestination))
@@ -13186,6 +13193,7 @@ var init_Validator = __esm({
13186
13193
  errors.push(...this.validateOmitNull(consumer));
13187
13194
  errors.push(...this.validateHeader(consumer));
13188
13195
  errors.push(...this.validateSplit(consumer));
13196
+ errors.push(...this.validateFixedOutput(consumer));
13189
13197
  const dimensionFields = consumer.fields.filter((x2) => x2.key !== "*" && !x2.fixed && !x2.copyFrom && !x2.lineCount);
13190
13198
  const dimensionKeys = dimensionFields.map((x2) => `${x2.from ?? "_default_"}::${x2.key}`);
13191
13199
  const duplicateDimensionKeys = dimensionKeys.filter((k7, i6) => dimensionKeys.indexOf(k7) !== i6);
@@ -13254,7 +13262,7 @@ var init_Validator = __esm({
13254
13262
  this.validateSplit = (consumer) => {
13255
13263
  const errors = [];
13256
13264
  const { split: split2 } = consumer;
13257
- const positionalFormats = ["CSV", "TXT", "PARQUET"];
13265
+ const positionalFormats = ["CSV", "FIXED", "TXT", "PARQUET"];
13258
13266
  if (!Algo_default.hasVal(split2)) return errors;
13259
13267
  if (Algo_default.hasVal(consumer.maximumFileSize))
13260
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".`);
@@ -13285,7 +13293,7 @@ var init_Validator = __esm({
13285
13293
  */
13286
13294
  this.validateOmitNull = (consumer) => {
13287
13295
  const errors = [];
13288
- const positionalFormats = ["CSV", "TXT", "PARQUET"];
13296
+ const positionalFormats = ["CSV", "FIXED", "TXT", "PARQUET"];
13289
13297
  for (const output of (consumer.outputs ?? []).filter((x2) => x2.omitNull === true)) {
13290
13298
  const format4 = output.format.toUpperCase();
13291
13299
  if (positionalFormats.includes(format4))
@@ -13305,10 +13313,49 @@ var init_Validator = __esm({
13305
13313
  for (const output of (consumer.outputs ?? []).filter((x2) => Algo_default.hasVal(x2.header))) {
13306
13314
  const format4 = output.format.toUpperCase();
13307
13315
  if (!headerFormats.includes(format4))
13308
- errors.push(`The ${format4} output of consumer "${consumer.name}" declares "header", which only applies to CSV/TXT output: a ${format4} ${format4 === "PARQUET" ? "file carries its own schema" : "record already carries its own field names"}.`);
13316
+ errors.push(`The ${format4} output of consumer "${consumer.name}" declares "header", which only applies to CSV/TXT output: ${this.headerlessReason(format4)}.`);
13309
13317
  }
13310
13318
  return errors;
13311
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
+ };
13312
13359
  /**
13313
13360
  * Checks a field whose key selects columns by pattern ("addr*Street"). A pattern that matches
13314
13361
  * nothing is always a mistake — a renamed dimension, or a producer that never had those columns —
@@ -14889,7 +14936,7 @@ A consumer takes the data from one or more producers and changes it's shape to t
14889
14936
  # RULES
14890
14937
  - If a field is not needed, do not add it e.g.
14891
14938
  - Only import a producer once
14892
- - 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",
14893
14940
  - Use "API" as the only valid output format.
14894
14941
  - The "from" must contain only the name of the producer
14895
14942
 
@@ -33941,6 +33988,307 @@ var init_FixedWidthParser = __esm({
33941
33988
  }
33942
33989
  });
33943
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
+
33944
34292
  // ../../node_modules/pend/index.js
33945
34293
  var require_pend = __commonJS({
33946
34294
  "../../node_modules/pend/index.js"(exports2, module2) {
@@ -33999,7 +34347,7 @@ var require_pend = __commonJS({
33999
34347
  // ../../node_modules/yauzl/fd-slicer.js
34000
34348
  var require_fd_slicer = __commonJS({
34001
34349
  "../../node_modules/yauzl/fd-slicer.js"(exports2) {
34002
- var fs32 = require("fs");
34350
+ var fs33 = require("fs");
34003
34351
  var util = require("util");
34004
34352
  var stream = require("stream");
34005
34353
  var Readable10 = stream.Readable;
@@ -34019,7 +34367,7 @@ var require_fd_slicer = __commonJS({
34019
34367
  FdSlicer.prototype.read = function(buffer, offset, length, position, callback) {
34020
34368
  var self2 = this;
34021
34369
  self2.pend.go(function(cb) {
34022
- fs32.read(self2.fd, buffer, offset, length, position, function(err2, bytesRead, buffer2) {
34370
+ fs33.read(self2.fd, buffer, offset, length, position, function(err2, bytesRead, buffer2) {
34023
34371
  cb();
34024
34372
  callback(err2, bytesRead, buffer2);
34025
34373
  });
@@ -34036,7 +34384,7 @@ var require_fd_slicer = __commonJS({
34036
34384
  self2.refCount -= 1;
34037
34385
  if (self2.refCount < 0) throw new Error("invalid unref");
34038
34386
  if (self2.refCount > 0) return;
34039
- fs32.close(self2.fd, onCloseDone);
34387
+ fs33.close(self2.fd, onCloseDone);
34040
34388
  function onCloseDone(err2) {
34041
34389
  if (err2) {
34042
34390
  self2.emit("error", err2);
@@ -34068,7 +34416,7 @@ var require_fd_slicer = __commonJS({
34068
34416
  }
34069
34417
  self2.context.pend.go(function(cb) {
34070
34418
  var buffer = Buffer.allocUnsafe(toRead);
34071
- fs32.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) {
34072
34420
  if (err2) {
34073
34421
  self2.destroy(err2);
34074
34422
  } else if (bytesRead === 0) {
@@ -34428,7 +34776,7 @@ var require_crc32 = __commonJS({
34428
34776
  // ../../node_modules/yauzl/index.js
34429
34777
  var require_yauzl = __commonJS({
34430
34778
  "../../node_modules/yauzl/index.js"(exports2) {
34431
- var fs32 = require("fs");
34779
+ var fs33 = require("fs");
34432
34780
  var zlib3 = require("zlib");
34433
34781
  var fd_slicer = require_fd_slicer();
34434
34782
  var util = require("util");
@@ -34497,10 +34845,10 @@ var require_yauzl = __commonJS({
34497
34845
  if (options2.validateEntrySizes == null) options2.validateEntrySizes = true;
34498
34846
  if (options2.strictFileNames == null) options2.strictFileNames = false;
34499
34847
  if (callback == null) callback = defaultCallback;
34500
- fs32.open(path22, "r", function(err2, fd) {
34848
+ fs33.open(path22, "r", function(err2, fd) {
34501
34849
  if (err2) return callback(err2);
34502
34850
  fromFd(fd, options2, function(err3, zipfile) {
34503
- if (err3) fs32.close(fd, defaultCallback);
34851
+ if (err3) fs33.close(fd, defaultCallback);
34504
34852
  callback(err3, zipfile);
34505
34853
  });
34506
34854
  });
@@ -34517,7 +34865,7 @@ var require_yauzl = __commonJS({
34517
34865
  if (options2.validateEntrySizes == null) options2.validateEntrySizes = true;
34518
34866
  if (options2.strictFileNames == null) options2.strictFileNames = false;
34519
34867
  if (callback == null) callback = defaultCallback;
34520
- fs32.fstat(fd, function(err2, stats) {
34868
+ fs33.fstat(fd, function(err2, stats) {
34521
34869
  if (err2) return callback(err2);
34522
34870
  var reader = new fd_slicer.FdSlicer(fd);
34523
34871
  fromRandomAccessReader(reader, stats.size, options2, callback);
@@ -38598,6 +38946,7 @@ var init_src5 = __esm({
38598
38946
  init_CSVParser();
38599
38947
  init_DatasetCodec();
38600
38948
  init_FixedWidthParser();
38949
+ init_FixedWidthWriter();
38601
38950
  init_ParseCompression();
38602
38951
  init_ParseHelper();
38603
38952
  init_XLSParser();
@@ -46233,8 +46582,8 @@ var init_LocalSourceDriver = __esm({
46233
46582
  Logger_default.log(`Matched ${allFileKeys.length} files, copying to locally and creating unified dataset.`);
46234
46583
  const firstPath = import_path10.default.join(this._path, allFileKeys[0]);
46235
46584
  const headerLine = (await DriverHelper_default.quickReadFile(firstPath, 1))[0];
46236
- const promises2 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, headerLine, i6));
46237
- 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);
46238
46587
  return results.flat();
46239
46588
  } else {
46240
46589
  return await this._get(request, "");
@@ -46254,8 +46603,8 @@ var init_LocalSourceDriver = __esm({
46254
46603
  Logger_default.log(`Matched ${allFileKeys.length} files, copying to locally and creating unified dataset.`);
46255
46604
  const firstPath = import_path10.default.join(this._path, allFileKeys[0]);
46256
46605
  const headerLine = (await DriverHelper_default.quickReadFile(firstPath, 1))[0];
46257
- const promises2 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, headerLine, i6));
46258
- 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);
46259
46608
  return results.flat();
46260
46609
  } else {
46261
46610
  return await this._get(request, "");
@@ -46683,8 +47032,8 @@ var init_S3SourceDriver = __esm({
46683
47032
  if (fileKey.includes("%")) {
46684
47033
  const allFileKeys = await this.listFiles(fileKey);
46685
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.`);
46686
- const promises2 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, i6));
46687
- 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);
46688
47037
  return results.flat();
46689
47038
  } else {
46690
47039
  return await this._get(request);
@@ -46697,8 +47046,8 @@ var init_S3SourceDriver = __esm({
46697
47046
  const { fileKey } = request;
46698
47047
  if (fileKey.includes("%")) {
46699
47048
  const allFileKeys = await this.listFiles(fileKey);
46700
- const promises2 = allFileKeys.map((x2, i6) => this._get({ ...request, fileKey: x2 }, i6));
46701
- 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);
46702
47051
  return results.flat();
46703
47052
  } else {
46704
47053
  return await this._get(request);
@@ -56102,6 +56451,8 @@ var init_DatasetSchema = __esm({
56102
56451
  return "JSON";
56103
56452
  case "TXT":
56104
56453
  return "CSV";
56454
+ case "FIXED":
56455
+ return "CSV";
56105
56456
  case "CSV":
56106
56457
  return "CSV";
56107
56458
  default:
@@ -56181,6 +56532,112 @@ var init_DatasetSchema = __esm({
56181
56532
  }
56182
56533
  });
56183
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
+
56184
56641
  // ../../packages/executors/src/OutputExecutor.ts
56185
56642
  var OutputExecutorClass, OutputExecutor, OutputExecutor_default;
56186
56643
  var init_OutputExecutor = __esm({
@@ -56193,6 +56650,7 @@ var init_OutputExecutor = __esm({
56193
56650
  init_src5();
56194
56651
  init_src2();
56195
56652
  init_DatasetSchema();
56653
+ init_FixedBlockOrder();
56196
56654
  OutputExecutorClass = class {
56197
56655
  constructor() {
56198
56656
  /**
@@ -56283,6 +56741,8 @@ var init_OutputExecutor = __esm({
56283
56741
  const destinationName = this._composeFileName(consumer, output, this._getExtension(output));
56284
56742
  Logger_default.info(`Exporting consumer "${consumer.name}" to "${output.exportDestination}" as ${output.format} (${destinationName}, ${splitFiles.length} file(s))`);
56285
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;
56286
56746
  const results = [];
56287
56747
  for (const { path: startingPath, label } of splitFiles) {
56288
56748
  const destinationPath = this.getCompletedPath(destinationName, label);
@@ -56303,6 +56763,11 @@ var init_OutputExecutor = __esm({
56303
56763
  );
56304
56764
  break;
56305
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;
56770
+ }
56306
56771
  case "PARQUET":
56307
56772
  results.push(await this._exportParquet({
56308
56773
  sourcePath: startingPath,
@@ -56318,6 +56783,60 @@ var init_OutputExecutor = __esm({
56318
56783
  }
56319
56784
  return this._unifingResult(results);
56320
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
+ };
56321
56840
  /**
56322
56841
  * Ships one split file as parquet. A parquet file is a columnar container whose footer can only
56323
56842
  * be written once the last row group is encoded, so there is no per-line mapping to hand a
@@ -56355,8 +56874,9 @@ var init_OutputExecutor = __esm({
56355
56874
  /**
56356
56875
  * The header line of a positional export: the visible field names, in column order, quoted and
56357
56876
  * delimited exactly as the rows under them — so the header parses with the same reader the data
56358
- * does. Null for a JSON/API output, where a record already carries its own field names, and for
56359
- * 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.
56360
56880
  *
56361
56881
  * The names come from the dataset's own schema, so a `pivot` is described by the header.
56362
56882
  */
@@ -57010,10 +57530,10 @@ var init_ExecutorPerformance = __esm({
57010
57530
  });
57011
57531
 
57012
57532
  // ../../packages/executors/src/ExecutorWriter.ts
57013
- var fs27, ExecutorWriter, ExecutorWriter_default;
57533
+ var fs28, ExecutorWriter, ExecutorWriter_default;
57014
57534
  var init_ExecutorWriter = __esm({
57015
57535
  "../../packages/executors/src/ExecutorWriter.ts"() {
57016
- fs27 = __toESM(require("fs"));
57536
+ fs28 = __toESM(require("fs"));
57017
57537
  init_src();
57018
57538
  init_src3();
57019
57539
  init_src10();
@@ -57065,7 +57585,7 @@ var init_ExecutorWriter = __esm({
57065
57585
  await this._route(pass, dsSchema);
57066
57586
  if (pass.buckets.size === 0)
57067
57587
  return [{ path: sourcePath, label: this.UNSPLIT_LABEL }];
57068
- await fs27.promises.unlink(sourcePath);
57588
+ await fs28.promises.unlink(sourcePath);
57069
57589
  return this._listFiles(pass);
57070
57590
  };
57071
57591
  /**
@@ -57073,7 +57593,7 @@ var init_ExecutorWriter = __esm({
57073
57593
  */
57074
57594
  this._route = async (pass, dsSchema) => {
57075
57595
  const parseLine = pass.keys.length === 0 ? null : LineParser_default.datasetParser(dsSchema);
57076
- const readStream = fs27.createReadStream(pass.sourcePath);
57596
+ const readStream = fs28.createReadStream(pass.sourcePath);
57077
57597
  try {
57078
57598
  for await (const line of readLines(readStream)) {
57079
57599
  const bucket = this._bucketFor(line, pass, parseLine);
@@ -57177,7 +57697,7 @@ var init_ExecutorWriter = __esm({
57177
57697
  }
57178
57698
  if (pass.openStreams.length >= Constants_default.defaults.MAX_OPEN_SPLIT_STREAMS)
57179
57699
  await this._closeStream(pass.openStreams.shift());
57180
- bucket.stream = fs27.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" });
57181
57701
  pass.openStreams.push(bucket);
57182
57702
  return bucket.stream;
57183
57703
  };
@@ -57762,6 +58282,11 @@ var init_ExecutorOrchestrator = __esm({
57762
58282
  const start = performance.now();
57763
58283
  const executorResults = [];
57764
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
+ );
57765
58290
  const lookups = await LookupResolver_default.materialize(consumer, expandedFields, scope, request);
57766
58291
  const consumerInputs = await ConsumerInputResolver_default.materialize(consumer, scope, request);
57767
58292
  let counter = performance.now();