@cosyte/astm 0.0.17 → 0.0.19

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
@@ -136,7 +136,8 @@ this file is maintained by hand (Changesets handles the version bump and publish
136
136
  `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS` fires where two escape alignments disagree about a **field**
137
137
  boundary, the reading taken keeps it, and the escape character that reading resumes on heads **no
138
138
  escape sequence at all**, which is exactly the character the competing alignment uses to close its
139
- own sequence. A gained field boundary shifts every later field one place. It is not tolerable, so
139
+ own sequence. (**That tail bound is widened later in this same release**, to any tail this reader
140
+ cannot interpret; the entry below carries the change and its measurement.) A gained field boundary shifts every later field one place. It is not tolerable, so
140
141
  `{ strict: true }` refuses such a record whatever profile is in force.
141
142
 
142
143
  **What the shift costs, measured on the canonical set.** `R|1|^^^687|28.6&F&|&U/L||||F` reads
@@ -173,23 +174,27 @@ this file is maintained by hand (Changesets handles the version bump and publish
173
174
  profile, and both are **`PRE-EXISTING`** (byte-identical on the base of this slice), so they are
174
175
  disclosed here as a separate open condition rather than covered: wiring this sink to another
175
176
  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
177
+ 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
178
  the escape character past the boundary heads a **recognized** sequence, the alignment taken
178
179
  interprets it while the competing one would leave it bare, so the bytes prefer the alignment taken
179
180
  (under a set naming the field separator `F`, `28.6&F&F&F&U/L` is that separator escaped, written and
180
181
  escaped again, entirely well formed, and refusing it is the over-refusal that sank a criterion
181
182
  measured for this same family); and where it heads an **unrecognized** sequence, the alignment taken
182
183
  still consumes it while the competing one would leave two escape characters bare, so the preference
183
- is stronger again. That second case still shifts the fields and is silent here. It is recorded as a
184
- residue, measured, not overlooked.
184
+ was read as stronger again, and this shipped silent there while the fields shifted just the same.
185
+ **That residue is closed later in this same release** (see the tail-bound entry below), which
186
+ corrects the reason rather than the code: the preference comparison is true and is not the question
187
+ this code asks.
185
188
 
186
189
  **Measured against the corpus constants committed with the test**, on the same
187
190
  strict-accepted-under-a-gate-legal-profile tier. Over `DECLARATION_ALPHABET`, `SPLITTING_ROLES`,
188
191
  `BODY_ALPHABET` and `TAIL_SUFFIXES` in `test/records/alignment-shifted-fields.test.ts`, **864
189
- tuples**: the code fires on **96**, moves **32** from accepted to refused under a profile built from
190
- the whole tolerable allow-list, and moves **0** back. It fires on **none** of the 96 escape-clean
191
- tuples, and cannot: firing requires an escape character heading no sequence, which this package
192
- already reports as a deviation of its own. The criterion measured and rejected on this same corpus
192
+ tuples**: as this code shipped it fired on **96**, moved **32** from accepted to refused under a
193
+ profile built from the whole tolerable allow-list, and moved **0** back. **The tail-bound widening
194
+ below takes those to 192 and 64 on the same corpus**, which is what the committed test asserts now.
195
+ It fires on **none** of the 96 escape-clean tuples, and cannot, before or after: firing requires an
196
+ escape character heading no sequence this reader can interpret, and this package already reports
197
+ each of those as a deviation of its own. The criterion measured and rejected on this same corpus
193
198
  would have refused 48 of them. The 32 that move are exactly the population the code above left
194
199
  silent through its recognized-body exclusion.
195
200
 
@@ -204,6 +209,286 @@ this file is maintained by hand (Changesets handles the version bump and publish
204
209
  `ShiftedFieldsSink` type, and a trailing optional sink parameter on `splitEscapeAware`,
205
210
  `tokenizeRecord` and `tokenizeHeader`.
206
211
 
212
+ - **A parse-path warning code for a field a competing escape alignment read short**
213
+ (`ASTM-FRAME-RESIDUALS`, defect 17b). `ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD` fires where two
214
+ escape alignments of the same bytes disagree about a **repeat** boundary, the reading taken keeps
215
+ it, and the escape character that reading resumes on heads **no sequence at all**. (**That tail
216
+ bound is widened later in this same release**; see the entry below.) It lands on the profile safety
217
+ gate, so `{ strict: true }` refuses a stream carrying it whatever profile is in force.
218
+
219
+ **Nothing shifts, and that is not the same as nothing being lost, which is the correction this
220
+ entry carries.** No field-indexed slot moves: the units slot and the result-status slot are read
221
+ out of the same field numbers under either alignment, which is exactly why the shift report above
222
+ cannot see this. What it reports is that **the field is read as more repeats than the competing
223
+ alignment gives it**, which is true wherever it fires. **Where that gained boundary is the FIRST
224
+ one in the field it reaches a modeled slot**, because a field's modeled value and its components
225
+ are taken from its first repeat alone: everything past the boundary stays on the wire, stays in
226
+ `repeats`, and leaves every modeled slot. Both costs are reachable on the **canonical** set, so no
227
+ unusual declaration is needed for either:
228
+ - **A value truncates.** `R|1|^^^687|28.6&S&\&U/L|U/L||||F` reads a value of `28.6^`, and `&U/L`
229
+ leaves the result entirely; the competing alignment reads one repeat carrying all of it.
230
+ - **A modeled identity empties, which is the half the shift report could not reach.**
231
+ `R|1|&F&\&687|28.6|U/L||||F` reads a Universal Test ID of one component holding a decoded
232
+ field separator, so the local code `687` is in no modeled slot at all and the identity comes back
233
+ as a LOINC candidate nobody wrote. `DOE&S&\&JANE^A` reads a last name and **no given or middle
234
+ name**. Before this code the only warnings on those streams were tolerable ones, so the widest
235
+ gate-legal profile plus `{ strict: true }` accepted a truncated value and an emptied test
236
+ identity.
237
+
238
+ **At a LATER boundary it fires and no modeled slot moves**, because the first repeat is then
239
+ identical under both readings, and what differs is the repeat structure after it. That is
240
+ **over-reporting relative to the modeled slots and never under-reporting**, it is measured on its
241
+ own sweep beside the main corpus rather than assumed, and it is stated on the code itself.
242
+ Narrowing the sink to the first boundary would change which streams a published package refuses
243
+ and wants its own measurement, so the bound is written down instead.
244
+
245
+ **Additive, and the split is unchanged.** Every decoded byte is identical, `repeats` still carries
246
+ every one of them, and the value read is the value that was always read: this reports the gained
247
+ boundary, it does not repair it. Picking the other alignment was rejected for the same reason as
248
+ before, on a published package.
249
+
250
+ **Two further bounds, both deliberate.** It is wired to the **repeat** separator only: a gained
251
+ **component** boundary reaches a modeled slot too, and differently, moving it one slot along rather
252
+ than dropping it, which is the entry below. And the tail is
253
+ weighed **one construct deep**, on the same test the shift report uses: where the escape character
254
+ past the boundary heads a **recognized** sequence the bytes prefer the reading taken and the two
255
+ repeats are the ones the sender wrote (`28.6&R&\&R&U/L` is the canonical repeat separator
256
+ escaped, written and escaped again, and refusing it is the over-refusal that sank a criterion
257
+ measured for this same family), and where it heads an **unrecognized** one the competing alignment
258
+ would leave two escape characters bare, so the preference was read as stronger again, and this
259
+ shipped silent there while the field truncated just the same. **That residue is closed later in
260
+ this same release** (see the tail-bound entry below).
261
+
262
+ **Measured against the corpus constants committed with the test**, on the same
263
+ strict-accepted-under-a-gate-legal-profile tier and the same **864 tuples** as the two measurements
264
+ before it, in `test/records/alignment-truncated-field.test.ts`: as this code shipped it fired on
265
+ **96**, moved **32** from accepted to refused under a profile built from the whole tolerable
266
+ allow-list, moved **0** back, and fired on **none** of the 96 escape-clean tuples. **The
267
+ tail-bound widening below takes the first two to 192 and 64 on the same corpus**, which is what the
268
+ committed test asserts now, and leaves the escape-clean zero where it is. The pair-count criterion measured
269
+ and rejected on this same corpus would have refused 48 of them, which is why the tail is what gets
270
+ weighed. **Unlike that rejected criterion's population, this one is not confined to a set naming a
271
+ mnemonic letter as a delimiter**: the canonical repeat separator reaches it, on 12 of the 108
272
+ canonical tuples swept. A second sweep beside it moves the contested boundary off the front of the
273
+ field and measures the same column: **96 fire, 32 move, 0 back, 0 escape-clean**, and on every one
274
+ of the 96 the first repeat is identical under both alignments, which is the bound on the claim
275
+ stated as a measurement rather than as prose.
276
+
277
+ **It does not survive a re-emit**, the same residue its siblings disclosed: the serializer rewrites
278
+ the preserved characters into recognized mnemonics, so a second-generation read is silent and is
279
+ correct about its own bytes. Catch it on the first read of the wire bytes.
280
+
281
+ This is a **narrowing on a published package**, on the strict path only: a lenient parse of the
282
+ same stream returns the same records, with the same values, and one more warning on them. New
283
+ public surface: `ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD`, the `alignmentTruncatedField` warning
284
+ factory, the `TruncatedFieldSink` type, and a further trailing optional sink parameter on
285
+ `splitEscapeAware`, `tokenizeRecord` and `tokenizeHeader`.
286
+
287
+ - **A parse-path warning code for components a competing escape alignment moved one slot along**
288
+ (`ASTM-FRAME-RESIDUALS`, defect 17c). `ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS` fires where two
289
+ escape alignments of the same bytes disagree about a **component** boundary, the reading taken
290
+ keeps it, and the escape character that reading resumes on heads **no sequence at all**. (**That
291
+ tail bound is widened later in this same release**; see the entry below.) It lands on the profile
292
+ safety gate, so `{ strict: true }` refuses a stream carrying it whatever profile is in force. With it the three roles a split is taken on are all wired; there is no fourth, because
293
+ nothing splits on the escape role.
294
+
295
+ **Nothing shifts between fields and nothing leaves the record: the slots MOVE.** That is the third
296
+ answer to the one question the two entries above ask, and it is why neither of them could carry it.
297
+ Components are modeled **inside** a field, so every component after the gained boundary sits one
298
+ place further right than the competing alignment puts it, and the slots that indexes into are named
299
+ things. Both costs are reachable on the **canonical** set, so no unusual declaration is needed:
300
+ - **A code system and a vendor's local code swap places.** `R|1|&F&^&GLU^L^687|28.6|U/L||||F`
301
+ reads a Universal Test ID of four components, so `L` is the coding scheme and `687` the local
302
+ code; the competing alignment reads three, and `687` is the **coding scheme**. A code-system
303
+ selector and a vendor's local code are not the same thing, so a consumer routing on either is
304
+ routing on the alignment.
305
+ - **A given name and a middle name shift along.** `P|1||MRN-0001||DOE&F&^&JANE^A||19700101|F`
306
+ reads a given name of `&JANE` and a middle name of `A`; under the competing alignment `A` is the
307
+ **given** name and there is no middle name at all.
308
+
309
+ Before this code the only warning on either stream was the tolerable
310
+ `ASTM_UNPAIRED_ESCAPE_CHARACTER`, so the widest gate-legal profile plus `{ strict: true }` accepted
311
+ both. **This closes a claim that had been recorded as false and shipped**: "a gained repeat or
312
+ component boundary divides one field and reaches nothing outside it, so it cannot move a modeled
313
+ slot". The first clause is true and the inference is not.
314
+
315
+ **Every gained boundary at or before the LAST MODELED COMPONENT INDEX moves those slots, not only
316
+ the first**, because the shift propagates from it to the end of the component list. That is the one
317
+ structural asymmetry with the repeat-separator code above, where a field is modeled out of its
318
+ first repeat alone and only the first gained boundary reaches a modeled slot. **Two bounds run the
319
+ other way and this fires inside both**, which is over-reporting relative to the named slots and
320
+ never under-reporting, measured rather than assumed: past the last modeled component index a model
321
+ reads a fixed number of components (a patient name three, a Universal Test ID four), so a contested
322
+ boundary further right shifts the components after it while every named slot reads the same under
323
+ both alignments; and inside a LATER repeat nothing modeled moves at all, because `components` is
324
+ the first repeat. Narrowing the sink to either would change which streams a published package
325
+ refuses and wants its own measurement, so both bounds are written down instead.
326
+
327
+ **Additive, and the split is unchanged.** Every decoded byte is identical and the components read
328
+ are the components that were always read: this reports the moved slots, it does not repair them.
329
+ Picking the other alignment was rejected for the same reason as before, on a published package, and
330
+ **withholding the moved slots stays a separate question**: declining to model a slot changes an
331
+ extracted value for every consumer already on the registry.
332
+
333
+ **The tail is weighed one construct deep**, on the same test the two codes above use. Where the
334
+ escape character past the boundary heads a **recognized** sequence the bytes prefer the reading
335
+ taken and the components are the ones the sender wrote (`&F&^&F&GLU` is a field separator escaped,
336
+ a component separator written, and a field separator escaped again, which raises no warning at all,
337
+ and refusing it is the over-refusal that sank a criterion measured for this same family); where it
338
+ heads an **unrecognized** one the competing alignment would leave two escape characters bare, so
339
+ the preference was read as stronger again, and this shipped silent there while the slots moved just
340
+ the same. **That residue is closed later in this same release** (see the tail-bound entry below).
341
+
342
+ **Measured against the corpus constants committed with the test**, on the same
343
+ strict-accepted-under-a-gate-legal-profile tier and the same **864 tuples** as the three
344
+ measurements before it, in `test/records/alignment-shifted-components.test.ts`: as this code
345
+ shipped it fired on **96**, moved **32** from accepted to refused under a profile built from the
346
+ whole tolerable allow-list, moved **0** back, and fired on **none** of the 96 escape-clean tuples.
347
+ **The tail-bound widening below takes the first two to 192 and 64 on the same corpus**, which is
348
+ what the committed test asserts now, and leaves the escape-clean zero where it is. The pair-count
349
+ criterion measured and rejected on this same corpus would have refused 48 of them, which is why the
350
+ tail is what gets weighed. Its column is **disjoint from both** earlier tail reports, asserted on
351
+ the shared corpus rather than reasoned from the wiring: the three partition the splitting roles.
352
+
353
+ **The index axes that corpus holds fixed are swept beside it, because a criterion measured only
354
+ there inherits its blind spot.** Across every component position of the first repeat the reading
355
+ taken reads exactly one component more than the competing alignment, so the harm is not bounded to
356
+ the first boundary. And the identical 864-tuple alphabet re-run with a clean first repeat in front
357
+ of the contested construct gives the same column and the same delta (**96 fire, 32 move, 0 back, 0
358
+ escape-clean**) while on **all 96** the modeled component list is identical under both alignments,
359
+ which states the over-reporting bound as a measurement rather than as prose.
360
+
361
+ **It does not survive a re-emit**, the same residue its siblings disclosed: the serializer rewrites
362
+ the preserved characters into recognized mnemonics, so a second-generation read is silent and is
363
+ correct about its own bytes. Catch it on the first read of the wire bytes.
364
+
365
+ This is a **narrowing on a published package**, on the strict path only: a lenient parse of the
366
+ same stream returns the same records, with the same values, and one more warning on them. New
367
+ public surface: `ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS`, the `alignmentShiftedComponents`
368
+ warning factory, the `ShiftedComponentsSink` type, and a further trailing optional sink parameter
369
+ on `splitEscapeAware`, `tokenizeRecord` and `tokenizeHeader`.
370
+
371
+ - **The three alignment tail reports now fire on a tail they cannot READ, not only on one they cannot
372
+ CONSUME** (`ASTM-FRAME-RESIDUALS`, defect 17, the disclosed residue). No new code and no rename:
373
+ `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS`, `ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD` and
374
+ `ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS` all keep their names, their claims and their roles. What
375
+ changes is the one predicate behind them: a contested boundary is now reported where the escape
376
+ character the reading taken resumes on heads **no sequence this reader can interpret**, which is
377
+ two cases rather than one.
378
+ - It heads **no sequence at all** and is kept as a bare literal
379
+ (`ASTM_UNPAIRED_ESCAPE_CHARACTER`). Reported before, reported now.
380
+ - It heads a sequence whose body is **not a recognized mnemonic**, so the triple is preserved
381
+ verbatim and never guessed at (`ASTM_UNKNOWN_ESCAPE_SEQUENCE`). **This is what was silent.**
382
+
383
+ **The correction is to the reason, which is why it is worth stating.** The recorded ground for the
384
+ silence was a comparison of preferences: in that case the competing alignment would leave **two**
385
+ escape characters bare while the reading taken carries one unreadable body, so the bytes prefer the
386
+ reading taken more strongly than in the case that already fired. That comparison is true. It is not
387
+ the question these codes ask. They report a **cost**, a modeled slot decided by a boundary the
388
+ bytes do not force, and the cost is the same one: on the canonical set
389
+ `R|1|^^^687|28.6&F&|&Z&U/L||||F` still reads nine fields against the competing alignment's eight
390
+ and still hands back a status of `final` the sender put in no slot; `28.6&S&\&Z&U/L` still reads a
391
+ value of `28.6^`; `&F&^&Z&GLU^L^687` still reads `687` as a vendor local code where the competing
392
+ alignment reads it as the **coding scheme**. Each raised only the tolerable
393
+ `ASTM_UNKNOWN_ESCAPE_SEQUENCE` before, so the widest gate-legal profile plus `{ strict: true }`
394
+ accepted all three. **Consuming a triple is not interpreting one.**
395
+
396
+ **Exactly one tail is still excluded, and it is the one that carries the whole over-refusal
397
+ argument.** Where the escape character heads a sequence this reader **recognizes**, the reading
398
+ taken interprets a construct and nothing is reported: under a set naming the field separator `F`,
399
+ `28.6&F&F&F&U/L` is that separator escaped, written, and escaped again, entirely well formed. That
400
+ is the **only** tail on which a stream's escaping can be clean at all, so it is the only exclusion
401
+ that could ever protect conformant traffic, and refusing it is the over-refusal that sank a
402
+ criterion measured and rejected for this same family. The widened predicate **cannot** refuse an
403
+ escape-clean stream: every position it now rejects already raises an escape deviation of its own.
404
+
405
+ **That remaining silence is a TRADE, and it is not a claim that nothing was lost there.** On the
406
+ excluded tail the gained boundary is exactly as real, and it is not merely under-reported: it is
407
+ `warnings: []`, so a lenient consumer sees nothing at all. On the canonical set,
408
+ `R|1|^^^687|28.6&F&|&F&U/L||||F` reads **nine** fields against the competing alignment's eight and
409
+ hands back a status of **`final`**; `28.6&S&\&S&U/L` reads a value of `28.6^` and `^U/L` leaves
410
+ every modeled slot; `&F&^&F&GLU^L^687` reads one component more, so `687` is a vendor local code
411
+ under one reading and the **coding scheme** under the other. All three are `PRE-EXISTING`, all
412
+ three are now pinned by tests rather than by prose, and **read the raw line when an escape
413
+ character sits next to a delimiter, whether or not anything fired.**
414
+
415
+ **Additive in behaviour terms and still a report, not a repair.** No split changed, every decoded
416
+ byte is identical, and no tolerable code was struck off and no code became tolerable. **What the
417
+ three codes CLAIM about their role did have to move, and that is recorded rather than glossed**:
418
+ see the entry below. It still does not survive a re-emit:
419
+ the serializer rewrites the preserved characters into recognized mnemonics, so a second-generation
420
+ read is silent and correct about its own bytes. Catch it on the first read of the wire bytes.
421
+
422
+ **Measured against the corpus constants committed with the tests**, on the same
423
+ strict-accepted-under-a-gate-legal-profile tier as every measurement in this family. On the shared
424
+ **864-tuple** corpus each of the three codes now fires on **192** where it fired on 96, moves
425
+ **64** from accepted to refused where it moved 32, moves **0** back, and fires on **0** of the 96
426
+ escape-clean tuples exactly as before. The two reported tails contribute equally, so neither half
427
+ of a column carries the other.
428
+
429
+ **The axis that corpus holds fixed is swept beside it**, because a criterion measured only there
430
+ inherits its blind spot: that corpus varies whether the tail is bare, recognized or unrecognized,
431
+ but uses a single character as the unrecognized body. `test/records/alignment-unrecognized-tail.test.ts`
432
+ crosses `DECLARATION_ALPHABET`, `SPLITTING_ROLES` and `BODY_ALPHABET` with the same alphabet as the
433
+ **tail** body: **3,456 tuples**, of which **2,304** fire, **528** move from accepted to refused,
434
+ **0** move back, and **384** are escape-clean with **0** of them refused. Every tuple's observed
435
+ disposition is asserted against a predicate derived from those alphabets rather than against a
436
+ count, and the harness is failed on purpose against a perturbed mnemonic set in both directions.
437
+
438
+ **One universal that sweep refuted, recorded because it is the obvious thing to write.** "The
439
+ reading taken reads exactly one more segment than the competing alignment" is **false**. Where the
440
+ tail sequence's body is the delimiter the split is being taken on, the reading taken keeps that
441
+ character inside an opaque atom while the competing alignment splits on it, so the two readings
442
+ produce the **same** number of segments in **different** places: under `H~\^&`, `28.6&F&~&~&U/L`
443
+ reads `28.6&F&` + `&~&U/L` against the competitor's `28.6&F&~&` + `&U/L`. That is 144 of the 2,304
444
+ firing tuples. What holds on all of them is that the two readings **disagree** and that both
445
+ consume every byte, so neither is forced.
446
+
447
+ This is a **narrowing on a published package**, on the strict path only: a lenient parse of the
448
+ same stream returns the same records, with the same values, and one more warning on them. **No new
449
+ public surface**: no code, type, factory or parameter was added, removed or renamed.
450
+
451
+ - **The three alignment tail reports now state the one class in which nothing moves index**
452
+ (`ASTM-FRAME-RESIDUALS`, defect 17, the claim correction the widening above forced). Widening the
453
+ predicate widened the firing population past the point where each code's own cost claim held
454
+ everywhere, and the claims did not move with it. They do now. Where a surface states the cost, it
455
+ names the exception: the three sink docs in `src/common/escapes.ts`, the three code entries, the
456
+ three factory leads and the three **runtime warning messages** in `src/common/warnings.ts`,
457
+ `src/profiles/safety.ts`, the `referenceCorpus` rationale string, `README.md`,
458
+ `docs-content/quickstart.md`, `docs-content/limitations.md` and
459
+ `docs-content/troubleshooting.md`. Where a surface only paraphrased a cost, the paraphrase was
460
+ **removed** rather than re-qualified, so there is one place to keep true: the `@param` text on
461
+ `splitEscapeAware` in `src/common/escapes.ts` and on `tokenizeRecord` in `src/records/tokenize.ts`
462
+ now defers to the sink docs instead of restating them.
463
+
464
+ **The class, named rather than left to be found.** Where the sequence past the contested boundary
465
+ carries **the splitting delimiter itself** as its body, the reading taken holds that character
466
+ inside an opaque atom while the competing alignment splits on it, so the two readings return the
467
+ **same number** of segments in **different places**. Nothing moves index, and what differs is the
468
+ contents. On the canonical set `R|1|^^^687|28.6&F&|&|&U/L||||F` reads **nine** fields under both,
469
+ with the status `F` in field 9 under both and units of `&|&U/L` against `&U/L`; `28.6&F&\&\&U/L`
470
+ reads **two** repeats under both; `DOE&F&^&^&JANE^A` reads **three** components under both, with
471
+ `A` the middle name under both. So "every later field is one place further right", "the field is
472
+ read as more repeats than the other reading gives it" and "every later component sits one place
473
+ further right" were each false on that class, in `dist/index.d.ts`, in the immutable docs tarball,
474
+ and in three runtime messages.
475
+
476
+ **The class is bounded, and the bound is measured against constants committed with the test, not
477
+ argued.** `test/records/alignment-unrecognized-tail.test.ts` crosses `DECLARATION_ALPHABET`,
478
+ `SPLITTING_ROLES`, `BODY_ALPHABET` and `TAIL_BODY_ALPHABET` with two further constants,
479
+ `PAYLOAD_PREFIXES` and `PAYLOAD_SUFFIXES`, so the carrier shape is not held fixed the way every
480
+ earlier corpus in this family held it: **55,296 tuples**, of which **36,864** fire and **2,304**
481
+ are the class. **0** of those lack `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` and **0** were
482
+ accepted before this release, because that tail body is a splitting delimiter in force. So the
483
+ three codes over-report relative to the indexes there and never under-report, and the class costs
484
+ no stream its disposition. The class is exactly "the tail body is the delimiter being split on",
485
+ with **0** counterexamples in either direction. Every figure is derived from those six constants
486
+ inside its own assertion; **never quote one against a different sha or a different corpus.**
487
+
488
+ **What holds on every firing tuple**, and it is what the claims now lead with: the two readings
489
+ **disagree**, and both **consume every byte**, so neither reading is forced. No code, message
490
+ identity, type, factory or parameter changed, and no stream's disposition moved.
491
+
207
492
  ### Fixed
208
493
 
209
494
  - **A header that is not short no longer reports that it is too short** (`ASTM-FRAME-RESIDUALS`,
@@ -527,10 +812,13 @@ this file is maintained by hand (Changesets handles the version bump and publish
527
812
  unchanged, still `test/fixtures/**` and `src/**.ts`, so a staged link outside it is still not
528
813
  looked at; narrowing what a scope admits is not widening the scope. That scope also bounds the
529
814
  gitlink half: a submodule staged at `test/fixtures/nested` is refused, while one at `src/nested` is
530
- a directory name that fails the `.ts` suffix and is not looked at. `R` (rename) and `C` (copy) are
531
- still **not** enumerated by `--staged` at all, so a staged rename that also appends PHI passes that
815
+ a directory name that fails the `.ts` suffix and is not looked at. **`R` (rename) and `C` (copy)
816
+ are still not enumerated by `--staged` at all, so a staged rename that also appends PHI passes that
532
817
  route; pre-existing, unchanged here, and admitting it needs the two-path record shape handled,
533
- which is a scope decision rather than this one. A tracked file **absent from the worktree** is
818
+ which is a scope decision rather than this one.** That last sentence was true when it was written
819
+ and is **superseded later in this same release**: the entry below closes it, and it turned out to
820
+ need no two-path record shape at all. It is kept rather than rewritten because the measurement it
821
+ records is what the later entry starts from. A tracked file **absent from the worktree** is
534
822
  still caught at `git add` time only.
535
823
 
536
824
  **And this scanner still has no rule that a sweep observing ZERO targets must refuse**, which is
@@ -555,6 +843,87 @@ this file is maintained by hand (Changesets handles the version bump and publish
555
843
  is not, which makes closing it a separate slice with its own argument rather than a rider on a PHI
556
844
  blindness fix. It is a different defect and it fails in the safe direction.
557
845
 
846
+ - **The PHI scanner's staged route no longer lets git decide what it is safe not to look at, so a
847
+ `git mv` into a fixture directory can no longer carry PHI past the pre-commit gate**
848
+ (`PHI-SCAN-RENAME-BLIND-AT-PRECOMMIT`). Development tooling only: `scripts/phi-scan.ts` ships in no
849
+ tarball and the package's public surface is unchanged.
850
+
851
+ Reproduced on `427af85` before any fix, on git 2.39.5, with the same synthetic name-bearing payload
852
+ the symlink cases use (an ASTM `P` record carrying a patient name, a mother's maiden name and a
853
+ birthdate, plus a dashed SSN and an email at a non-test domain). Four ways the enumeration could be
854
+ emptied before any of the refusals above were reached, each measured as its own throwaway index:
855
+ - **A staged rename.** `git mv test/fixtures/original.astm test/fixtures/renamed.astm` on a file
856
+ carrying the payload paired as a single record with **two** paths and a status letter of `R`, and
857
+ `--diff-filter=AMT` returned nothing for it, so `pnpm phi-scan --staged` printed `OK: no hits`
858
+ and exited **0**. The same move with the payload **edited into the destination in the same
859
+ staging** passed identically, because git still pairs it when enough of the old content survives.
860
+ No similarity score is quoted here: it moves with how much survives, so a number taken from one
861
+ fixture is wrong in the next, and what is load-bearing is that the record was paired at all.
862
+ - **A staged rename of a symbolic link.** `git mv` of a tracked link into `test/fixtures/` put a
863
+ mode-`120000` entry under a scan root. The mode refusal added by the previous entry already knew
864
+ how to refuse that; the pairing deleted the record before the mode could be read, and the run
865
+ exited **0**.
866
+ - **A gitlink the caller's config hid.** `diff.ignoreSubmodules = all` in the caller's git config
867
+ dropped a staged gitlink under `test/fixtures/` from the raw output entirely. The **same index**
868
+ refused at exit 2 without that config and reported `OK: no hits` at exit **0** with it.
869
+ - **An unmerged path.** An in-scope path recorded unmerged, with the payload on one side, was
870
+ returned by neither `AM` nor `AMT`, and the run printed `OK: no hits` and exited **0** over an
871
+ index it could not read: such a path is recorded at one or more of stages 1/2/3 and never at
872
+ stage 0, and stage 0 is exactly what the `:<path>` form this route reads with names, so there is
873
+ no one blob for it to read. **Nothing rests on what `git show` does with such a path**: the route
874
+ refuses before it would call it, and an earlier draft that pinned that exit code did not
875
+ reproduce across git versions.
876
+
877
+ **The remedy is entirely in the argv, and it is one rule: stop trusting the caller's git config,
878
+ and stop letting the filter decide what is safe not to look at.** The route now reads
879
+ `git diff --cached --raw -z --no-renames --ignore-submodules=none --diff-filter=AMTUB`.
880
+ `--no-renames` makes a paired record impossible, so the destination arrives as an ordinary
881
+ single-path `A` and the source as a `D` the filter drops; verified under `diff.renames` at
882
+ `true`, `copies` and `false` and under `diff.renameLimit=1`, every one yielding the same
883
+ single-path record, so the two-field record stride is now **structural** rather than conditional on
884
+ the caller's config. `--ignore-submodules=none` restores a record the config could delete.
885
+ `U` is in the filter **so it can be refused, not scanned**, with its own message: an unmerged
886
+ record's destination mode is `000000`, so the existing refusal would have described it with a
887
+ sentence about symbolic links and gitlinks that is false for it. That case builds the unmerged
888
+ index directly, with `git update-index --index-info`, rather than provoking it with a real merge:
889
+ whether a merge conflicts depends on the merge machinery and on the caller having a git identity,
890
+ and the index state is the thing under test. Staged as a real conflict it did not reproduce on
891
+ every git version. None of the four costs the record
892
+ stride anything, because each carries a single path.
893
+
894
+ **Be exact about what that changes.** The new enumeration is a **superset** of the old one, not a
895
+ strictly larger set: the two are **equal** whenever nothing is renamed, copied, unmerged or hidden
896
+ by that config, and larger only when something is. Nothing the old argv enumerated stops being
897
+ enumerated. The `--staged` path scope is **unchanged**, still `test/fixtures/**` and `src/**.ts`, so
898
+ an unmerged or renamed path outside it is still not looked at, and both of those are pinned.
899
+
900
+ **`B` is in the filter because `-B` is not inert, and the mechanism is sharper than "a `B` record
901
+ the filter drops".** A complete rewrite under `-B` prints a **single-path record whose status
902
+ letter is `M`**, with a break score, which the record regex parses happily, so a reader checking
903
+ the raw output concludes the record is an `M` and that `AMTU` therefore keeps it. It does not:
904
+ `--diff-filter` classifies a broken pair as `B` **whatever letter it prints**. Measured on one
905
+ index, over a wholly rewritten in-scope fixture carrying a dashed SSN: `-B --diff-filter=AMTU`
906
+ returns empty while `-B --diff-filter=B` and `-B --diff-filter=AMTUB` each return the same record,
907
+ and a copy of the scanner with `-B` injected exits **0** with `OK: no hits` on the superseded
908
+ filter and **1** with the hit on the shipped one. `B` costs the enumeration nothing today, since
909
+ git only breaks a pair when `-B` is given, so it is there to stop the flag ever being a silent
910
+ blindfold. `-M`, `-C` and `--find-copies-harder` each turn detection back on over the top of
911
+ `--no-renames` and empty the route again, which is measured beside it; none of the four may be
912
+ added.
913
+
914
+ **6 of the file's 41 cases are red on `427af85`**, and the premises each rests on are pinned beside
915
+ it, so a case cannot quietly stop testing anything. Refusal messages still name only the entry's own
916
+ repo-relative path and an engine-owned reason, never a link target or any stream content, and that
917
+ is asserted on the new refusal too.
918
+
919
+ **What this does not cover, stated narrowly.** The all-mode walk is untouched: it has no concept of
920
+ a rename, and nothing here widens or narrows what it enumerates. The scan roots' own paths are still
921
+ outside the `--staged` scope, so a staged blob, link or gitlink at exactly `test/fixtures`, `test`
922
+ or `src` is still not looked at by that route. Neither route reaches `test/**/*.test.ts`, where this
923
+ package keeps most of its inline fixtures, so those are still read by hand. And this scanner still
924
+ has no rule that a sweep observing zero targets must refuse, exactly as recorded above. Each of
925
+ those is a scope decision with its own argument, not a consequence of anything here.
926
+
558
927
  - **The frame encoder no longer writes a different character than the one it was handed**
559
928
  (`ASTM-FRAME-BYTE-RESIDUALS`, closing known defect 7). `composeAstmFrames` turned a record given
560
929
  as a `string` into bytes with `charCodeAt(i) & 0xff`, so any character above `U+00FF` was replaced
package/README.md CHANGED
@@ -1,7 +1,9 @@
1
- <picture>
2
- <source media="(prefers-color-scheme: dark)" srcset="https://cosyte.com/tile/cosyte-lockup-tile-on-dark-1200x300.png">
3
- <img alt="Cosyte: a plus mark set in two overlapping rounded squares, one solid and one outlined, beside the Cosyte wordmark" src="https://cosyte.com/tile/cosyte-lockup-tile-on-light-1200x300.png">
4
- </picture>
1
+ <a href="https://cosyte.com">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://cosyte.com/tile/cosyte-lockup-tile-on-dark-1200x300.png">
4
+ <img alt="Cosyte: a plus mark set in two overlapping rounded squares, one solid and one outlined, beside the Cosyte wordmark" src="https://cosyte.com/tile/cosyte-lockup-tile-on-light-1200x300.png">
5
+ </picture>
6
+ </a>
5
7
 
6
8
  # @cosyte/astm
7
9
 
@@ -222,9 +224,12 @@ mirror: catch it on the first read.
222
224
 
223
225
  **And where that gained boundary is a FIELD boundary, it moves a result's status, which is a
224
226
  different question and so a different code.** The two alignments resume one character apart, so they
225
- disagree about every byte after the boundary, not only about it. Where the escape character the
226
- reading taken resumes on heads no sequence at all, that reading bought the boundary with a byte it
227
- cannot read while the competing alignment is the one that can use it. On the field separator that
227
+ disagree about the bytes after the boundary, not only about it (they can resync later, and the
228
+ class where they do is named below). Where the escape character the
229
+ reading taken resumes on heads no sequence this reader can _interpret_ (none at all, or one whose
230
+ body is not a recognized mnemonic and is therefore preserved verbatim rather than read), that
231
+ reading bought the boundary with bytes it cannot read while the competing alignment is the one that
232
+ can use them. On the field separator that
228
233
  matters clinically: `R|1|^^^687|28.6&F&|&U/L||||F` reads **nine** fields under the alignment taken
229
234
  and **eight** under the other, so the sender's trailing `F` lands in field 9, the result status,
230
235
  under the first and in no field at all under the second. The parse hands back units of `&U/L` and a
@@ -235,12 +240,77 @@ tolerate**; before it existed the only warning on that stream was the tolerable
235
240
  unchanged: it reports the shift rather than repairing it. It is wired to the **field** separator
236
241
  only, because a gained repeat or component boundary divides one field and so moves no field-indexed
237
242
  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.
243
+ gained repeat or component boundary does reach a modeled slot, and each of those two has a code of
244
+ its own below. It stays silent in exactly one case,
245
+ where the trailing escape character heads a sequence this reader RECOGNIZES: that is the escape
246
+ mechanism working, and refusing it would refuse well-formed traffic. That is the only tail on which
247
+ a stream's escaping can be clean, which is why it is the only exclusion. **That silence is a trade
248
+ and not a claim that nothing was lost there**: the gained field boundary is exactly as real, and on
249
+ that tail it is `warnings: []`, so `R|1|^^^687|28.6&F&|&F&U/L||||F` reads nine fields against the
250
+ competing alignment's eight and hands back a status of `final` with nothing reported at all. Read
251
+ the raw line when an escape character sits next to a delimiter, whether or not anything fired.
252
+ **The shift has one measured exception, and it is named rather than left to be found**: where the
253
+ sequence past the boundary carries the field separator itself as its body, the reading taken holds
254
+ that character inside an opaque atom while the competing alignment splits on it, so the two readings
255
+ read the **same number** of fields in **different places**. `R|1|^^^687|28.6&F&|&|&U/L||||F` reads
256
+ nine fields under both, with the status `F` in field 9 under both, and what differs is the units.
257
+ The code still fires there, which is over-reporting relative to the field indexes and never
258
+ under-reporting, and that class costs no stream its disposition, because
259
+ `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE` has already refused it. It does not survive a re-emit
260
+ either: catch it on the first read.
261
+
262
+ **And where that gained boundary is a REPEAT boundary, nothing shifts and the field can still be
263
+ read short.** What the report says is that the field is read as more repeats than the competing
264
+ alignment gives it. A field's modeled value and components are taken from its **first repeat
265
+ alone**, so where the gained boundary is the **first** one everything past it stays on the wire,
266
+ 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^`
267
+ and drops `&U/L`, and `R|1|&F&\&687|28.6|U/L||||F` reads a Universal Test ID of one component
268
+ holding a decoded field separator, so the local code `687` is in no modeled slot and the identity
269
+ comes back as a LOINC candidate nobody wrote. A patient name loses its given and middle names the
270
+ same way. That raises `ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD`, which **no profile may tolerate**;
271
+ before it existed the only warnings on those streams were tolerable ones, so a strict parse under a
272
+ legal profile accepted a truncated value and an emptied test identity. The reading is unchanged: it
273
+ reports the gained boundary rather than repairing it. **At a LATER boundary it fires and no modeled
274
+ slot moves**, because the first repeat is then the same under both readings; that is over-reporting
275
+ relative to the modeled slots and never under-reporting, and it is measured rather than assumed. Its
276
+ tail bound is the shift report's, for the same reason, and so is its one exclusion:
277
+ `28.6&R&\&R&U/L` is the repeat separator escaped, written, and escaped again, and refusing it would
278
+ refuse a well-formed stream. **That silence is a trade, not a claim that nothing was lost**:
279
+ `28.6&S&\&S&U/L` still reads a value of `28.6^` with `warnings: []`. **The truncation has the same
280
+ one exception as the shift**: where the sequence past the boundary carries the repeat separator
281
+ itself, the two readings read the same number of repeats in different places, so the field is not
282
+ read as more repeats at all, and that class was already refused by
283
+ `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE`. It is wired to the **repeat** separator only; a gained
284
+ **component** boundary reaches a modeled slot differently, and has its own code below. It does not
285
+ survive a re-emit either: catch it on the first read.
286
+
287
+ **And where that gained boundary is a COMPONENT boundary, nothing leaves the record and the slots
288
+ MOVE.** Components are modeled inside a field, so every component after the gained boundary sits one
289
+ place further right than the competing alignment puts it. On the canonical set,
290
+ `R|1|&F&^&GLU^L^687|28.6|U/L||||F` reads a Universal Test ID of four components, so `L` is the coding
291
+ scheme and `687` the vendor's local code; the competing alignment reads three, and `687` is the
292
+ **coding scheme**. A code-system selector and a vendor's local code are not the same thing.
293
+ `P|1||MRN-0001||DOE&F&^&JANE^A||19700101|F` reads a given name of `&JANE` and a middle name of `A`,
294
+ where the competing alignment makes `A` the **given** name with no middle name at all. That raises
295
+ `ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS`, which **no profile may tolerate**; before it existed the
296
+ only warning on either stream was the tolerable `ASTM_UNPAIRED_ESCAPE_CHARACTER`, so a strict parse
297
+ under a legal profile accepted both. The reading is unchanged: it reports the moved slots rather than
298
+ repairing them. **Every gained boundary at or before the last modeled component index moves them,
299
+ not only the first**, because the shift propagates to the end of the component list, which is where
300
+ this differs from the repeat role. **Two bounds run the other way and it fires inside both**,
301
+ over-reporting and never under: past the last modeled index nothing named moves (a name models three
302
+ components, a Universal Test ID four), and inside a later repeat nothing modeled moves at all. Its
303
+ tail bound is the other two reports', for the same reason, and so is its one exclusion, **and that
304
+ silence is a trade rather than a claim that nothing was lost**: `&F&^&F&GLU^L^687` still reads one
305
+ component more than the competing alignment, with `warnings: []`. **A THIRD bound runs the other way
306
+ here, and unlike the two above it is about the bytes past the boundary rather than where the
307
+ boundary sits**: where the sequence past it carries the component separator itself, the two readings
308
+ read the same number of components in different places, so `DOE&F&^&^&JANE^A` reads three components
309
+ under both with `A` the middle name under both, and that class was already refused by
310
+ `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE`. What holds wherever any of the three codes fires is
311
+ that the two readings disagree and that both consume every byte, so neither is forced. It does not
312
+ survive a re-emit either: catch it on the first read. With this the three splitting roles are all
313
+ wired, and there is no fourth: nothing splits on the escape role.
244
314
 
245
315
  ### A header that names one character in two delimiter roles
246
316
 
@@ -337,9 +407,17 @@ outside it:
337
407
  where the leftmost alignment lets a delimiter split that a competing alignment would have held,
338
408
  gains a boundary instead of losing one and raises `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT`, also
339
409
  not tolerable. Where that gained boundary is a **field** boundary and the reading taken resumes on
340
- an escape character heading no sequence, every later field shifts one place and a result's units
410
+ an escape character heading no sequence it can interpret, every later field shifts one place and a
411
+ result's units
341
412
  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.
413
+ `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS`, not tolerable either. Where it is a **repeat** boundary
414
+ nothing shifts, but the field is read out of its first repeat alone, so a gained first boundary
415
+ truncates a value and costs a test identity or a patient name the components that sat after it:
416
+ that raises `ASTM_RECORD_ALIGNMENT_TRUNCATED_FIELD`, not tolerable either. Where it is a
417
+ **component** boundary nothing leaves the record and every component after it moves one slot
418
+ along, so a coding scheme, a vendor local code or a given name is read out of a position the other
419
+ alignment does not put it in: that raises `ASTM_RECORD_ALIGNMENT_SHIFTED_COMPONENTS`, not
420
+ tolerable either.
343
421
 
344
422
  All are accepted limits, for two different reasons: widening the field-separator check would mean
345
423
  deciding which set a record ought to have had, which is the same guess the parser declines to make