@cosyte/astm 0.0.17 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -173,7 +173,7 @@ this file is maintained by hand (Changesets handles the version bump and publish
173
173
  profile, and both are **`PRE-EXISTING`** (byte-identical on the base of this slice), so they are
174
174
  disclosed here as a separate open condition rather than covered: wiring this sink to another
175
175
  delimiter role is a different criterion and wants its own population measurement. On the repeat role
176
- what is lost is the value, also not reported here. And the tail is weighed **one construct deep**: where
176
+ what is lost is the value, not reported by this code and now reported by its own, below. And the tail is weighed **one construct deep**: where
177
177
  the escape character past the boundary heads a **recognized** sequence, the alignment taken
178
178
  interprets it while the competing one would leave it bare, so the bytes prefer the alignment taken
179
179
  (under a set naming the field separator `F`, `28.6&F&F&F&U/L` is that separator escaped, written and
@@ -204,6 +204,78 @@ this file is maintained by hand (Changesets handles the version bump and publish
204
204
  `ShiftedFieldsSink` type, and a trailing optional sink parameter on `splitEscapeAware`,
205
205
  `tokenizeRecord` and `tokenizeHeader`.
206
206
 
207
+ - **A parse-path warning code for a field a competing escape alignment read short**
208
+ (`ASTM-FRAME-RESIDUALS`, defect 17b). `ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD` fires where two
209
+ escape alignments of the same bytes disagree about a **repeat** boundary, the reading taken keeps
210
+ it, and the escape character that reading resumes on heads **no sequence at all**. It lands on the
211
+ profile safety gate, so `{ strict: true }` refuses a stream carrying it whatever profile is in
212
+ force.
213
+
214
+ **Nothing shifts, and that is not the same as nothing being lost, which is the correction this
215
+ entry carries.** No field-indexed slot moves: the units slot and the result-status slot are read
216
+ out of the same field numbers under either alignment, which is exactly why the shift report above
217
+ cannot see this. What it reports is that **the field is read as more repeats than the competing
218
+ alignment gives it**, which is true wherever it fires. **Where that gained boundary is the FIRST
219
+ one in the field it reaches a modeled slot**, because a field's modeled value and its components
220
+ are taken from its first repeat alone: everything past the boundary stays on the wire, stays in
221
+ `repeats`, and leaves every modeled slot. Both costs are reachable on the **canonical** set, so no
222
+ unusual declaration is needed for either:
223
+ - **A value truncates.** `R|1|^^^687|28.6&S&\&U/L|U/L||||F` reads a value of `28.6^`, and `&U/L`
224
+ leaves the result entirely; the competing alignment reads one repeat carrying all of it.
225
+ - **A modeled identity empties, which is the half the shift report could not reach.**
226
+ `R|1|&F&\&687|28.6|U/L||||F` reads a Universal Test ID of one component holding a decoded
227
+ field separator, so the local code `687` is in no modeled slot at all and the identity comes back
228
+ as a LOINC candidate nobody wrote. `DOE&S&\&JANE^A` reads a last name and **no given or middle
229
+ name**. Before this code the only warnings on those streams were tolerable ones, so the widest
230
+ gate-legal profile plus `{ strict: true }` accepted a truncated value and an emptied test
231
+ identity.
232
+
233
+ **At a LATER boundary it fires and no modeled slot moves**, because the first repeat is then
234
+ identical under both readings, and what differs is the repeat structure after it. That is
235
+ **over-reporting relative to the modeled slots and never under-reporting**, it is measured on its
236
+ own sweep beside the main corpus rather than assumed, and it is stated on the code itself.
237
+ Narrowing the sink to the first boundary would change which streams a published package refuses
238
+ and wants its own measurement, so the bound is written down instead.
239
+
240
+ **Additive, and the split is unchanged.** Every decoded byte is identical, `repeats` still carries
241
+ every one of them, and the value read is the value that was always read: this reports the gained
242
+ boundary, it does not repair it. Picking the other alignment was rejected for the same reason as
243
+ before, on a published package.
244
+
245
+ **Two further bounds, both deliberate.** It is wired to the **repeat** separator only: a gained
246
+ **component** boundary reaches a modeled slot too, and differently, moving it one slot along rather
247
+ than dropping it, and that stays the separate open condition disclosed above. And the tail is
248
+ weighed **one construct deep**, on the same test the shift report uses: where the escape character
249
+ past the boundary heads a **recognized** sequence the bytes prefer the reading taken and the two
250
+ repeats are the ones the sender wrote (`28.6&R&\&R&U/L` is the canonical repeat separator
251
+ escaped, written and escaped again, and refusing it is the over-refusal that sank a criterion
252
+ measured for this same family), and where it heads an **unrecognized** one the competing alignment
253
+ would leave two escape characters bare, so the preference is stronger again. That second case still
254
+ truncates and is silent here: recorded as a residue, measured, not overlooked.
255
+
256
+ **Measured against the corpus constants committed with the test**, on the same
257
+ strict-accepted-under-a-gate-legal-profile tier and the same **864 tuples** as the two measurements
258
+ before it, in `test/records/alignment-truncated-field.test.ts`: the code fires on **96**, moves
259
+ **32** from accepted to refused under a profile built from the whole tolerable allow-list, moves
260
+ **0** back, and fires on **none** of the 96 escape-clean tuples. The pair-count criterion measured
261
+ and rejected on this same corpus would have refused 48 of them, which is why the tail is what gets
262
+ weighed. **Unlike that rejected criterion's population, this one is not confined to a set naming a
263
+ mnemonic letter as a delimiter**: the canonical repeat separator reaches it, on 12 of the 108
264
+ canonical tuples swept. A second sweep beside it moves the contested boundary off the front of the
265
+ field and measures the same column: **96 fire, 32 move, 0 back, 0 escape-clean**, and on every one
266
+ of the 96 the first repeat is identical under both alignments, which is the bound on the claim
267
+ stated as a measurement rather than as prose.
268
+
269
+ **It does not survive a re-emit**, the same residue its siblings disclosed: the serializer rewrites
270
+ the preserved characters into recognized mnemonics, so a second-generation read is silent and is
271
+ correct about its own bytes. Catch it on the first read of the wire bytes.
272
+
273
+ This is a **narrowing on a published package**, on the strict path only: a lenient parse of the
274
+ same stream returns the same records, with the same values, and one more warning on them. New
275
+ public surface: `ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD`, the `alignmentTruncatedField` warning
276
+ factory, the `TruncatedFieldSink` type, and a further trailing optional sink parameter on
277
+ `splitEscapeAware`, `tokenizeRecord` and `tokenizeHeader`.
278
+
207
279
  ### Fixed
208
280
 
209
281
  - **A header that is not short no longer reports that it is too short** (`ASTM-FRAME-RESIDUALS`,
package/README.md CHANGED
@@ -235,12 +235,30 @@ tolerate**; before it existed the only warning on that stream was the tolerable
235
235
  unchanged: it reports the shift rather than repairing it. It is wired to the **field** separator
236
236
  only, because a gained repeat or component boundary divides one field and so moves no field-indexed
237
237
  slot. **That bound is a choice, not a consequence**: components are modeled inside a field, so a
238
- gained **component** boundary does move a modeled slot, and `R|1|&F&^&GLU^L^687|28.6|U/L||||F` reads
239
- `687` as a local code under one alignment and as the coding scheme under the other, with only a
240
- tolerable code raised. That is a separate open condition, not something this code covers. It is also
241
- silent where the trailing escape character heads a sequence of its own, recognized or not; that case
242
- still shifts the fields and is the recorded residue. It does not survive a re-emit either: catch it
243
- on the first read.
238
+ gained repeat or component boundary does reach a modeled slot. It is also silent where the trailing
239
+ escape character heads a sequence of its own, recognized or not; that case still shifts the fields
240
+ and is the recorded residue. It does not survive a re-emit either: catch it on the first read.
241
+
242
+ **And where that gained boundary is a REPEAT boundary, nothing shifts and the field can still be
243
+ read short.** What the report says is that the field is read as more repeats than the competing
244
+ alignment gives it. A field's modeled value and components are taken from its **first repeat
245
+ alone**, so where the gained boundary is the **first** one everything past it stays on the wire,
246
+ stays in `repeats`, and leaves every modeled slot. On the canonical set, `R|1|^^^687|28.6&S&\&U/L|U/L||||F` reads a value of `28.6^`
247
+ and drops `&U/L`, and `R|1|&F&\&687|28.6|U/L||||F` reads a Universal Test ID of one component
248
+ holding a decoded field separator, so the local code `687` is in no modeled slot and the identity
249
+ comes back as a LOINC candidate nobody wrote. A patient name loses its given and middle names the
250
+ same way. That raises `ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD`, which **no profile may tolerate**;
251
+ before it existed the only warnings on those streams were tolerable ones, so a strict parse under a
252
+ legal profile accepted a truncated value and an emptied test identity. The reading is unchanged: it
253
+ reports the gained boundary rather than repairing it. **At a LATER boundary it fires and no modeled
254
+ slot moves**, because the first repeat is then the same under both readings; that is over-reporting
255
+ relative to the modeled slots and never under-reporting, and it is measured rather than assumed. Its
256
+ tail bound is the shift report's, for the same reason: `28.6&R&\&R&U/L` is the repeat separator
257
+ escaped, written, and escaped again, and refusing it would refuse a well-formed stream. It is wired to the **repeat** separator only; a gained
258
+ **component** boundary reaches a modeled slot differently, moving it one slot along rather than
259
+ dropping it (`R|1|&F&^&GLU^L^687|28.6|U/L||||F` reads `687` as a local code under one alignment and
260
+ as the coding scheme under the other, with only a tolerable code raised), and that is a separate
261
+ open condition. It does not survive a re-emit either: catch it on the first read.
244
262
 
245
263
  ### A header that names one character in two delimiter roles
246
264
 
@@ -339,7 +357,10 @@ outside it:
339
357
  not tolerable. Where that gained boundary is a **field** boundary and the reading taken resumes on
340
358
  an escape character heading no sequence, every later field shifts one place and a result's units
341
359
  and status are read out of slots the other alignment does not put them in: that raises
342
- `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS`, not tolerable either.
360
+ `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS`, not tolerable either. Where it is a **repeat** boundary
361
+ nothing shifts, but the field is read out of its first repeat alone, so a gained first boundary
362
+ truncates a value and costs a test identity or a patient name the components that sat after it:
363
+ that raises `ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD`, not tolerable either.
343
364
 
344
365
  All are accepted limits, for two different reasons: widening the field-separator check would mean
345
366
  deciding which set a record ought to have had, which is the same guess the parser declines to make
package/dist/index.cjs CHANGED
@@ -329,6 +329,73 @@ var WARNING_CODES = {
329
329
  * silent and is correct about its own bytes. A clean re-read is not evidence.
330
330
  */
331
331
  ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS: "ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS",
332
+ /**
333
+ * Two escape alignments of the same bytes disagreed about a **repeat** boundary, the reading
334
+ * taken kept it, and the escape character that reading resumes on **heads no sequence at all**.
335
+ * Same contested position and same tail test as
336
+ * {@link WARNING_CODES.ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS}, on a different delimiter role, and
337
+ * what it costs is a different thing, which is why it is a different code.
338
+ *
339
+ * **Nothing shifts, and that is not the same as nothing being lost.** No field-indexed slot
340
+ * moves: the units and the result status are read out of the same field numbers under either
341
+ * alignment. What this reports is that **the field is read as more repeats than the competing
342
+ * alignment gives it**, which is true wherever it fires. What that costs depends on **which**
343
+ * boundary was gained, and the two cases are stated separately rather than folded into one claim.
344
+ *
345
+ * **Where it is the FIRST boundary in the field, a modeled slot is lost**, because a field's
346
+ * modeled value and components are taken from its **first repeat alone**. Everything past the
347
+ * boundary stays on the wire and stays in `repeats`, and leaves every modeled slot:
348
+ *
349
+ * - **A value truncates.** Under `H|F^&`, `R|1|^^^687|28.6&S&F&U/L||||F` reads the value field as
350
+ * the two repeats `28.6^` and `&U/L`, so a consumer reads `28.6^` and the rest of the value is
351
+ * gone. The competing alignment reads one repeat carrying all of it.
352
+ * - **A modeled component list is deleted rather than shifted.** The same boundary inside a
353
+ * Universal Test ID leaves the components of the first repeat only: under `H|F^&`,
354
+ * `R|1|&F&F&687|28.6|U/L||||F` reads a UTID of one component holding a decoded field separator,
355
+ * with no coding scheme and no local code, where the bytes carry `687`. A patient name loses
356
+ * its given and middle names the same way. That is the half of this condition the shift report
357
+ * could not cover, because components are modeled **inside** a field.
358
+ *
359
+ * Before this code the only warnings on those streams were **tolerable** ones
360
+ * ({@link WARNING_CODES.ASTM_NONSTANDARD_DELIMITERS} and, where the tail applies,
361
+ * {@link WARNING_CODES.ASTM_UNPAIRED_ESCAPE_CHARACTER}), so the widest gate-legal profile plus
362
+ * `{ strict: true }` accepted a truncated value and an emptied Universal Test ID.
363
+ *
364
+ * **Where it is a LATER boundary, no modeled slot moves and this still fires.** The first repeat
365
+ * is identical under both alignments, so the value, the components and every slot taken from them
366
+ * read the same either way (`R|1|^^^687|5.0F28.6&S&F&U/L||||F` under `H|F^&` reads a value of
367
+ * `5.0` under both), and what differs is the repeat structure after the first. It fires there
368
+ * deliberately: the boundary is still one the bytes do not force and a consumer reading `repeats`
369
+ * is still reading an alignment guess. Relative to the modeled slots that is **over-reporting,
370
+ * never under-reporting**. Narrowing it to the first boundary would change which streams a
371
+ * published package refuses and wants its own measurement, so the bound is written down instead.
372
+ *
373
+ * **It is a report, not a repair.** The split is unchanged, every decoded byte is identical,
374
+ * `repeats` still carries every one of them, and the value read is the value that was always
375
+ * read. Picking the other alignment would be a different guess with no more evidence behind it,
376
+ * on a published package.
377
+ *
378
+ * **Two further deliberate bounds, both stated rather than left to be found.**
379
+ * - **Only the repeat role.** The **component** role reaches a modeled slot too, and differently:
380
+ * there the components stay in the record and move one slot along, so a local code reads as a
381
+ * coding scheme and a given name as a middle name. That is measured, reported by nothing where
382
+ * the earlier body is a recognized mnemonic, and open. It is not covered here, because wiring a
383
+ * sink to another split is another criterion wanting its own population measurement.
384
+ * - **The tail is weighed one construct deep**, exactly as for the shift report. Where the escape
385
+ * character the reading resumes on heads a sequence this codec *recognizes*, the bytes prefer
386
+ * the reading taken and the repeats are the ones the sender wrote: under a set naming the
387
+ * repeat separator `F`, `28.6&F&F&F&U/L` is that separator escaped, written, and escaped again,
388
+ * and refusing it would be an over-refusal of a well-formed stream. Where it heads a sequence
389
+ * whose body is *unrecognized*, the competing alignment would leave **two** escape characters
390
+ * bare, so the preference is stronger again, and this stays silent there even though the gained
391
+ * boundary is real. That last case is the named residue: measured, not overlooked.
392
+ *
393
+ * **Catch it on the first read.** Emit rewrites the preserved sequences into recognized
394
+ * mnemonics, and those bytes carry the reading that was taken unambiguously, so a
395
+ * second-generation read is silent and is correct about its own bytes. A clean re-read is not
396
+ * evidence.
397
+ */
398
+ ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD: "ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD",
332
399
  /** An escape sequence body was not one of `&F&`/`&S&`/`&R&`/`&E&`: preserved verbatim. */
333
400
  ASTM_UNKNOWN_ESCAPE_SEQUENCE: "ASTM_UNKNOWN_ESCAPE_SEQUENCE",
334
401
  /**
@@ -488,6 +555,13 @@ function alignmentShiftedFields(position) {
488
555
  position
489
556
  };
490
557
  }
558
+ function alignmentTruncatedField(position) {
559
+ return {
560
+ code: WARNING_CODES.ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD,
561
+ message: "A repeat boundary here is one of two escape alignments the bytes carry, and the reading that took it resumes on an escape character heading no sequence, which the other alignment uses to close one. This field is read as more repeats than the other reading gives it, and its modeled value and components are taken from the first repeat alone, so where that boundary is the first one the rest of the field leaves every modeled slot. The reading was kept and every byte is preserved.",
562
+ position
563
+ };
564
+ }
491
565
  function delimitersRedeclared(position) {
492
566
  return {
493
567
  code: WARNING_CODES.ASTM_RECORD_DELIMITERS_REDECLARED,
@@ -1113,7 +1187,7 @@ var referenceCorpus = defineAstmProfile({
1113
1187
  tolerate: [
1114
1188
  {
1115
1189
  code: "ASTM_UNKNOWN_ESCAPE_SEQUENCE",
1116
- rationale: "The OSS reference stack (python-astm codec.py, firsthand-verified) treats '&' as literal data and never decodes escape sequences, so streams from that ecosystem carry non-standard '&\u2026&' bodies our escape-aware tokenizer flags. The body is preserved byte-for-byte in the value. NOT always non-clinical, and this is measured rather than assumed: where the body is a delimiter, that delimiter never became a field boundary, so `28.6&|&U/L` reads as one value with no units and no final status. That subset now also raises ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE, which this profile cannot tolerate, so tolerating this code no longer hides it. The same holds for the mirror case, where a sequence ends where another could have begun and a delimiter splits that the other alignment would have held: that raises ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT, which this profile cannot tolerate either. Where that gained boundary is a field boundary and the reading taken resumes on an escape character heading no sequence, every later field shifts and a result's units and status move with it, which raises ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS, untolerable here as well."
1190
+ rationale: "The OSS reference stack (python-astm codec.py, firsthand-verified) treats '&' as literal data and never decodes escape sequences, so streams from that ecosystem carry non-standard '&\u2026&' bodies our escape-aware tokenizer flags. The body is preserved byte-for-byte in the value. NOT always non-clinical, and this is measured rather than assumed: where the body is a delimiter, that delimiter never became a field boundary, so `28.6&|&U/L` reads as one value with no units and no final status. That subset now also raises ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE, which this profile cannot tolerate, so tolerating this code no longer hides it. The same holds for the mirror case, where a sequence ends where another could have begun and a delimiter splits that the other alignment would have held: that raises ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT, which this profile cannot tolerate either. Where that gained boundary is a field boundary and the reading taken resumes on an escape character heading no sequence, every later field shifts and a result's units and status move with it, which raises ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS, untolerable here as well. Where that same gained boundary is a repeat boundary, nothing shifts but the field is read out of its first repeat alone, so a value truncates and a test identity empties, which raises ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD, untolerable here too. Both of those carry the same tail bound and stay silent where the escape character past the boundary heads a sequence of its own, recognized or not, which is much of this corpus: tolerating this code does not quiet them, but their absence is not a statement that nothing was lost."
1117
1191
  }
1118
1192
  ]
1119
1193
  });
@@ -1187,7 +1261,7 @@ function isMnemonicBody(body) {
1187
1261
  function escapeBody(body, d) {
1188
1262
  return isMnemonicBody(body) ? d[MNEMONIC_ROLES[body]] : void 0;
1189
1263
  }
1190
- function splitEscapeAware(text, delimiter, escape, onAmbiguousAlignment, onShiftedFields) {
1264
+ function splitEscapeAware(text, delimiter, escape, onAmbiguousAlignment, onShiftedFields, onTruncatedField) {
1191
1265
  if (text.length === 0) return [""];
1192
1266
  const out = [];
1193
1267
  let current = "";
@@ -1201,6 +1275,7 @@ function splitEscapeAware(text, delimiter, escape, onAmbiguousAlignment, onShift
1201
1275
  }
1202
1276
  if (contested && !isEscapeSequenceAt(text, i + 4, escape)) {
1203
1277
  onShiftedFields?.(out.length);
1278
+ onTruncatedField?.(out.length);
1204
1279
  }
1205
1280
  current += text.slice(i, i + 3);
1206
1281
  i += 3;
@@ -1220,7 +1295,7 @@ function splitEscapeAware(text, delimiter, escape, onAmbiguousAlignment, onShift
1220
1295
  }
1221
1296
 
1222
1297
  // src/records/tokenize.ts
1223
- function tokenizeRecord(record, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter, onAmbiguousAlignment, onAlignmentShiftedFields) {
1298
+ function tokenizeRecord(record, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter, onAmbiguousAlignment, onAlignmentShiftedFields, onAlignmentTruncatedField) {
1224
1299
  const rawFields = splitEscapeAware(
1225
1300
  record,
1226
1301
  d.field,
@@ -1235,11 +1310,15 @@ function tokenizeRecord(record, d, onUnknownEscape, onUnpairedEscape, onSwallowe
1235
1310
  () => onUnknownEscape?.(fieldIndex),
1236
1311
  () => onUnpairedEscape?.(fieldIndex),
1237
1312
  () => onSwallowedDelimiter?.(fieldIndex),
1238
- () => onAmbiguousAlignment?.(fieldIndex)
1313
+ () => onAmbiguousAlignment?.(fieldIndex),
1314
+ // The repeat split runs INSIDE one field, so the index it hands back is a repeat index and is
1315
+ // discarded: what a caller needs is which field carries the contested boundary, and that is
1316
+ // the index this map is already iterating.
1317
+ () => onAlignmentTruncatedField?.(fieldIndex)
1239
1318
  )
1240
1319
  );
1241
1320
  }
1242
- function tokenizeHeader(record, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter, onAmbiguousAlignment, onAlignmentShiftedFields) {
1321
+ function tokenizeHeader(record, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter, onAmbiguousAlignment, onAlignmentShiftedFields, onAlignmentTruncatedField) {
1243
1322
  const defEnd = record.indexOf(d.field, 2);
1244
1323
  const definition = defEnd === -1 ? record.slice(2) : record.slice(2, defEnd);
1245
1324
  const head = [opaqueField(record.slice(0, Math.min(1, record.length))), opaqueField(definition)];
@@ -1251,17 +1330,27 @@ function tokenizeHeader(record, d, onUnknownEscape, onUnpairedEscape, onSwallowe
1251
1330
  (i) => onUnpairedEscape?.(i + 2),
1252
1331
  (i) => onSwallowedDelimiter?.(i + 2),
1253
1332
  (i) => onAmbiguousAlignment?.(i + 2),
1254
- (i) => onAlignmentShiftedFields?.(i + 2)
1333
+ (i) => onAlignmentShiftedFields?.(i + 2),
1334
+ (i) => onAlignmentTruncatedField?.(i + 2)
1255
1335
  );
1256
1336
  return [...head, ...data];
1257
1337
  }
1258
1338
  function opaqueField(raw) {
1259
1339
  return { raw, components: [raw], repeats: [[raw]] };
1260
1340
  }
1261
- function toField(raw, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter, onAmbiguousAlignment) {
1262
- const rawRepeats = splitEscapeAware(raw, d.repeat, d.escape, () => {
1263
- onAmbiguousAlignment();
1264
- });
1341
+ function toField(raw, d, onUnknownEscape, onUnpairedEscape, onSwallowedDelimiter, onAmbiguousAlignment, onAlignmentTruncatedField) {
1342
+ const rawRepeats = splitEscapeAware(
1343
+ raw,
1344
+ d.repeat,
1345
+ d.escape,
1346
+ () => {
1347
+ onAmbiguousAlignment();
1348
+ },
1349
+ void 0,
1350
+ () => {
1351
+ onAlignmentTruncatedField();
1352
+ }
1353
+ );
1265
1354
  const repeats = rawRepeats.map(
1266
1355
  (rep) => splitEscapeAware(rep, d.component, d.escape, () => {
1267
1356
  onAmbiguousAlignment();
@@ -1412,6 +1501,11 @@ function buildRecord(line2, recordIndex, d, warnings) {
1412
1501
  alignmentShiftedFields({ recordIndex, recordType: rawType, fieldIndex: fieldIndex + 1 })
1413
1502
  );
1414
1503
  };
1504
+ const onAlignmentTruncatedField = (fieldIndex) => {
1505
+ warnings.push(
1506
+ alignmentTruncatedField({ recordIndex, recordType: rawType, fieldIndex: fieldIndex + 1 })
1507
+ );
1508
+ };
1415
1509
  const fields = rawType === "H" ? tokenizeHeader(
1416
1510
  line2,
1417
1511
  d,
@@ -1419,7 +1513,8 @@ function buildRecord(line2, recordIndex, d, warnings) {
1419
1513
  onUnpairedEscape,
1420
1514
  onSwallowedDelimiter,
1421
1515
  onAmbiguousAlignment,
1422
- onAlignmentShiftedFields
1516
+ onAlignmentShiftedFields,
1517
+ onAlignmentTruncatedField
1423
1518
  ) : tokenizeRecord(
1424
1519
  line2,
1425
1520
  d,
@@ -1427,7 +1522,8 @@ function buildRecord(line2, recordIndex, d, warnings) {
1427
1522
  onUnpairedEscape,
1428
1523
  onSwallowedDelimiter,
1429
1524
  onAmbiguousAlignment,
1430
- onAlignmentShiftedFields
1525
+ onAlignmentShiftedFields,
1526
+ onAlignmentTruncatedField
1431
1527
  );
1432
1528
  if (fields.length === 1 && line2.slice(1).trim().length > 0) {
1433
1529
  warnings.push(fieldsUnseparated({ recordIndex, recordType: rawType }));
@@ -2640,7 +2736,7 @@ function concatBytes3(chunks) {
2640
2736
  }
2641
2737
 
2642
2738
  // src/index.ts
2643
- var VERSION = "0.0.17";
2739
+ var VERSION = "0.0.18";
2644
2740
 
2645
2741
  exports.ALL_ASTM_WARNING_CODES = ALL_ASTM_WARNING_CODES;
2646
2742
  exports.AMBIGUOUS_CODES = AMBIGUOUS_CODES;
@@ -2665,6 +2761,7 @@ exports.TOLERABLE_CODES = TOLERABLE_CODES;
2665
2761
  exports.VERSION = VERSION;
2666
2762
  exports.WARNING_CODES = WARNING_CODES;
2667
2763
  exports.alignmentShiftedFields = alignmentShiftedFields;
2764
+ exports.alignmentTruncatedField = alignmentTruncatedField;
2668
2765
  exports.ambiguousEscapeAlignment = ambiguousEscapeAlignment;
2669
2766
  exports.ambiguousMessageKind = ambiguousMessageKind;
2670
2767
  exports.ambiguousValueSplit = ambiguousValueSplit;