@cosyte/astm 0.0.15 → 0.0.17
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 +218 -0
- package/README.md +97 -11
- package/dist/index.cjs +316 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +736 -55
- package/dist/index.d.ts +736 -55
- package/dist/index.mjs +311 -42
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,8 +9,226 @@ this file is maintained by hand (Changesets handles the version bump and publish
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Two parse-path warning codes for two readings the parser could not defend, both landing on the
|
|
15
|
+
profile safety gate** (`ASTM-FRAME-RESIDUALS`, defects 4 and 11). Both are additive: no existing
|
|
16
|
+
warning was removed or renamed, no split changed, and every extracted value is byte-identical to
|
|
17
|
+
what it was. What changed is that each condition is now reported by a code **no profile may
|
|
18
|
+
tolerate**, so `{ strict: true }` refuses a stream carrying it whatever profile is in force.
|
|
19
|
+
- `ASTM_RECORD_DELIMITER_ROLE_COLLISION`: an `H` record declared one character in two of the
|
|
20
|
+
repeat, component and escape roles, so the boundary between those two roles is not in the bytes.
|
|
21
|
+
A declaration whose **field** separator is one of the other three was already refused (the
|
|
22
|
+
`ASTM_RECORD_UNDECLARED_DELIMITERS` fatal on the first header, `ASTM_RECORD_UNREADABLE_REDECLARATION`
|
|
23
|
+
on a later one), so this covers the three unordered pairs among the rest: repeat/component,
|
|
24
|
+
repeat/escape, component/escape. The declaration is still honored and no record is dropped.
|
|
25
|
+
Measured: under `H|^^&` the field `A^B^C^D` reads back as four repeats of one component each, so
|
|
26
|
+
a two-repeats-of-two-components reading is unrecoverable; under `H|\&&` the same character
|
|
27
|
+
splits (`A&B` is two components) or opens an atom (`A&F&B` is the single component `A|B`)
|
|
28
|
+
depending only on what follows it. Emit has always refused these sets
|
|
29
|
+
(`ASTM_EMIT_INVALID_DELIMITERS`), which is how the gap became visible from the outside.
|
|
30
|
+
- `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE`: an escape sequence whose body was **not** a
|
|
31
|
+
recognized mnemonic held a character that is one of the three splitting delimiters in force, so
|
|
32
|
+
the atom rule kept it out of the split and the record was read with one fewer division than its
|
|
33
|
+
bytes carry. Measured on the canonical set: `R|1|^^^687|28.6&|&U/L||||F` reads a value of
|
|
34
|
+
`28.6&|&U/L`, with **no units** and status `unspecified` rather than `final`. It fires alongside
|
|
35
|
+
`ASTM_UNKNOWN_ESCAPE_SEQUENCE`, which stays and stays tolerable, because it remains true and
|
|
36
|
+
benign of every unrecognized body that is not a delimiter in force.
|
|
37
|
+
|
|
38
|
+
**The measure that discriminates is strict-accepted-under-a-gate-legal-profile, and "silent" was
|
|
39
|
+
never available here.** Both conditions always raised a tolerable code already
|
|
40
|
+
(`ASTM_NONSTANDARD_DELIMITERS` for the first, since any colliding set is by definition
|
|
41
|
+
non-canonical; `ASTM_UNKNOWN_ESCAPE_SEQUENCE` for the second), so `warnings: []` was structurally
|
|
42
|
+
unreachable for either class and "it is never silent" was true and told you nothing. What it hid is
|
|
43
|
+
that a profile tolerating the weaker code left a strict parse accepting the reading.
|
|
44
|
+
|
|
45
|
+
Measured against the corpus constants committed with the tests, not against a sweep whose
|
|
46
|
+
parameters are not in the tree. Over `DELIMITER_ALPHABET` and `sweepStream` in
|
|
47
|
+
`test/records/delimiter-role-collision.test.ts` (the twelve characters `|` `\` `^` `&` `~` `:` `#`
|
|
48
|
+
`*` `!` `@` `$` `%`, a header plus a terminator), 15,972 of the 20,736 four-role tuples resolve;
|
|
49
|
+
4,092 of those name one character in two roles and 11,880 do not. Under a gate-legal profile
|
|
50
|
+
tolerating `ASTM_NONSTANDARD_DELIMITERS` and `{ strict: true }`, **4,092 of the 4,092 were accepted
|
|
51
|
+
before and 0 are now**, while all 11,880 non-colliding sets are accepted exactly as before. Over
|
|
52
|
+
`BODY_ALPHABET` and `bodyStream` in `test/records/swallowed-delimiter.test.ts` (the same twelve
|
|
53
|
+
characters as escape bodies in one spec-clean result record, read identically in all twelve cases),
|
|
54
|
+
**3 report the new code, enumerated as `|`, `\` and `^`**, the field, repeat and component roles of
|
|
55
|
+
the canonical set; the escape role `&` is deliberately not among them, because nothing splits on
|
|
56
|
+
it, and the other eight are not delimiters at all. Under `referenceCorpus` and `{ strict: true }`,
|
|
57
|
+
12 of 12 were accepted before and 9 are now.
|
|
58
|
+
|
|
59
|
+
**Two things these codes do not do, stated so they are not read into them.** Neither repairs a
|
|
60
|
+
reading: the atom rule is unchanged, because it is what keeps `&F&` one token under a set that
|
|
61
|
+
names `F` as a delimiter, and a colliding declaration is still honored rather than refused. And the
|
|
62
|
+
second does not survive a re-emit: the serializer rewrites the preserved sequence into recognized
|
|
63
|
+
mnemonics, and that stream says the same value unambiguously, so a second-generation read is silent
|
|
64
|
+
and is correct about its own bytes. The first read of the wire bytes is where the condition exists
|
|
65
|
+
to be caught, and that is where the refusal lands.
|
|
66
|
+
|
|
67
|
+
This is a **narrowing on a published package**, on the strict path only: a lenient parse of the
|
|
68
|
+
same stream returns the same records with one more warning on them. New public surface:
|
|
69
|
+
`hasCollidingRoles`, the `delimiterRoleCollision` and `delimiterSwallowedByEscape` warning
|
|
70
|
+
factories, the `SwallowedDelimiterSink` type, and a trailing optional sink parameter on
|
|
71
|
+
`decodeEscapes`, `tokenizeRecord` and `tokenizeHeader`.
|
|
72
|
+
|
|
73
|
+
- **A third parse-path warning code, for a boundary the reading may have GAINED**
|
|
74
|
+
(`ASTM-FRAME-RESIDUALS`, defect 15, the mirror of defect 11). `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT`
|
|
75
|
+
fires where the escape character that **closed** an unrecognized escape sequence could instead have
|
|
76
|
+
**opened** one whose body is the delimiter that split, so the same bytes carry two alignments that
|
|
77
|
+
disagree by exactly one field, repeat or component boundary. Measured on the canonical set:
|
|
78
|
+
`R|1|^^^687|28.6&Z&|&U/L||||F` reads a value of `28.6&Z&` and units of `&U/L` under the leftmost
|
|
79
|
+
alignment, and reads as one unsplit field carrying both under the other. It is not tolerable, so
|
|
80
|
+
`{ strict: true }` refuses such a record whatever profile is in force.
|
|
81
|
+
|
|
82
|
+
**The direction is why it needed a code rather than being left to its sibling.** That one reports a
|
|
83
|
+
boundary the reading lost; this one reports a boundary the reading may have gained, which is the
|
|
84
|
+
more dangerous half, because a gained boundary hands a consumer a value and a units string the
|
|
85
|
+
sender's bytes do not unambiguously carry. Both codes the condition raised before
|
|
86
|
+
(`ASTM_UNKNOWN_ESCAPE_SEQUENCE` and `ASTM_UNPAIRED_ESCAPE_CHARACTER`) are on the profile
|
|
87
|
+
allow-list, so a gate-legal profile naming them left a strict parse accepting the altered reading.
|
|
88
|
+
Both stay on the list and stay true of the cases that cost nothing.
|
|
89
|
+
|
|
90
|
+
**Additive, and the split is unchanged.** The leftmost alignment is still the one taken and every
|
|
91
|
+
decoded byte is identical. Picking the other alignment was considered and rejected: it is a
|
|
92
|
+
different guess with no more evidence behind it, and it would move values on a package that is
|
|
93
|
+
already published. Two exclusions are deliberate, and the first is **wider than the argument for
|
|
94
|
+
it**, which is stated rather than left to be found. A **recognized** mnemonic before the delimiter
|
|
95
|
+
raises nothing, because the reading taken interprets a construct (`&F&` is the sender escaping a
|
|
96
|
+
field separator, which is what the mechanism is for) while the competitor's body is a delimiter
|
|
97
|
+
character this codec usually cannot interpret, so its own vocabulary prefers the reading taken.
|
|
98
|
+
That is not the same as the reading taken being conformant, and two cases fall in the gap, both
|
|
99
|
+
measured and both left open as their own defect rather than closed by widening the test:
|
|
100
|
+
`28.6&F&|&U/L` reads a value of `28.6|` and units of `&U/L` under a bare escape character that this
|
|
101
|
+
package reports only as the tolerable `ASTM_UNPAIRED_ESCAPE_CHARACTER`, and a declared set naming a
|
|
102
|
+
mnemonic letter as a splitting delimiter leaves both alignments interpreting exactly one construct
|
|
103
|
+
with neither preferred. The criterion that would cover them is a different one (counting what each
|
|
104
|
+
alignment interprets), and swapping criteria moves which streams a published package refuses. The
|
|
105
|
+
second exclusion is a delimiter with no escape character two positions past it, which raises
|
|
106
|
+
nothing because there is no competing alignment. What does fire is a subset of the records that
|
|
107
|
+
already raise `ASTM_UNKNOWN_ESCAPE_SEQUENCE`.
|
|
108
|
+
|
|
109
|
+
**Measured against the corpus constants committed with the test**, on the same
|
|
110
|
+
strict-accepted-under-a-gate-legal-profile tier, because "silent" is structurally unreachable here
|
|
111
|
+
too. Over `ALIGNMENT_ALPHABET` and `neutralStream` in
|
|
112
|
+
`test/records/escape-alignment-ambiguity.test.ts` (twelve characters: the four mnemonics `F` `S`
|
|
113
|
+
`R` `E`, the three splitting roles `|` `\` `^`, the escape role `&`, and `~` `:` `#` `*`, swept in
|
|
114
|
+
both positions of the pair that decides this, carried on a comment record so no result-semantics
|
|
115
|
+
warning masks the count), of 144 tuples **24 raise the new code**, being the eight unrecognized
|
|
116
|
+
bodies against the three splitting roles. Under a profile built from the whole tolerable
|
|
117
|
+
allow-list, which is the widest a gate-legal profile can be, and `{ strict: true }`, **108 of 144
|
|
118
|
+
were accepted before and 93 are now**. The fire count and the acceptance delta are **not the same
|
|
119
|
+
number**: of the 24 that fire, 9 were already refused by
|
|
120
|
+
`ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE`, so 15 is what moved.
|
|
121
|
+
|
|
122
|
+
**It does not repair the reading and it does not survive a re-emit**, the same residue its sibling
|
|
123
|
+
disclosed: the serializer rewrites the preserved characters into recognized mnemonics, and that
|
|
124
|
+
stream carries the alignment that was taken with no competitor left in it, so a second-generation
|
|
125
|
+
read is silent and is correct about its own bytes. The first read of the wire bytes is where the
|
|
126
|
+
ambiguity exists to be caught.
|
|
127
|
+
|
|
128
|
+
This is a **narrowing on a published package**, on the strict path only: a lenient parse of the
|
|
129
|
+
same stream returns the same records with one more warning on them. New public surface:
|
|
130
|
+
`ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT`, the `ambiguousEscapeAlignment` warning factory, the
|
|
131
|
+
`AmbiguousAlignmentSink` type, and a trailing optional sink parameter on `splitEscapeAware`,
|
|
132
|
+
`tokenizeRecord` and `tokenizeHeader`.
|
|
133
|
+
|
|
134
|
+
- **A fourth parse-path warning code, for the first of these that moves a MODELED CLINICAL SLOT
|
|
135
|
+
rather than a boundary** (`ASTM-FRAME-RESIDUALS`, defect 17(a)).
|
|
136
|
+
`ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS` fires where two escape alignments disagree about a **field**
|
|
137
|
+
boundary, the reading taken keeps it, and the escape character that reading resumes on heads **no
|
|
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
|
|
140
|
+
`{ strict: true }` refuses such a record whatever profile is in force.
|
|
141
|
+
|
|
142
|
+
**What the shift costs, measured on the canonical set.** `R|1|^^^687|28.6&F&|&U/L||||F` reads
|
|
143
|
+
**nine** fields under the alignment taken and **eight** under the other, so the sender's trailing
|
|
144
|
+
`F` lands in field 9, the result status, under the first and in **no field at all** under the
|
|
145
|
+
second. The parse hands back units of `&U/L` and a status of **`final`**, and both are consequences
|
|
146
|
+
of the alignment rather than values the sender placed in those slots. A downstream system reading
|
|
147
|
+
`final` acts on a result these bytes do not say was finalised. The only warning on that stream
|
|
148
|
+
before this code was the tolerable `ASTM_UNPAIRED_ESCAPE_CHARACTER`, so the widest gate-legal
|
|
149
|
+
profile plus `{ strict: true }` accepted it. **The predecessor entry above lists that `final` among
|
|
150
|
+
the values read without saying it is fabricated; this is that correction.**
|
|
151
|
+
|
|
152
|
+
**It is a second question about the same position, not a widening of the code above.** That one
|
|
153
|
+
asks whether this codec's own vocabulary prefers the alignment taken _at_ the contested position,
|
|
154
|
+
and is silent where the earlier body is a recognized mnemonic. This one asks what the alignment
|
|
155
|
+
taken makes of the bytes _after_ the boundary, and does not consult the earlier body at all. The
|
|
156
|
+
earlier code's exclusions are untouched, and the two fire alongside each other where both apply.
|
|
157
|
+
|
|
158
|
+
**Additive, and the split is unchanged.** Every decoded byte is identical, including the status,
|
|
159
|
+
which is still read as `final`: this reports the shift, it does not repair it. Picking the other
|
|
160
|
+
alignment was rejected for the same reason as before, and withholding the shifted slots was
|
|
161
|
+
weighed and deferred, because declining to model a slot changes an extracted value on a published
|
|
162
|
+
package and wants its own measurement of the population it moves.
|
|
163
|
+
|
|
164
|
+
**Two bounds, both deliberate, and neither is a claim that nothing was lost outside it.** It is
|
|
165
|
+
wired to the **field** separator only, because a gained repeat or component boundary divides one
|
|
166
|
+
field and so moves no **field-indexed** slot: the units and the status stay put. **That is a choice
|
|
167
|
+
and not a consequence, and stating it as a consequence would be false.** Components are modeled
|
|
168
|
+
_inside_ a field, so a gained **component** boundary does move a modeled slot:
|
|
169
|
+
`R|1|&F&^&GLU^L^687|28.6|U/L||||F` reads a Universal Test ID coding scheme of `L` and local code of
|
|
170
|
+
`687` under the alignment taken, and `687` as the **coding scheme** under the other, while
|
|
171
|
+
`DOE&F&^&JANE^A` reads `&JANE` as a first name under one and `A` under the other. Both raise only
|
|
172
|
+
the tolerable `ASTM_UNPAIRED_ESCAPE_CHARACTER` and are strict-accepted under the widest gate-legal
|
|
173
|
+
profile, and both are **`PRE-EXISTING`** (byte-identical on the base of this slice), so they are
|
|
174
|
+
disclosed here as a separate open condition rather than covered: wiring this sink to another
|
|
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
|
|
177
|
+
the escape character past the boundary heads a **recognized** sequence, the alignment taken
|
|
178
|
+
interprets it while the competing one would leave it bare, so the bytes prefer the alignment taken
|
|
179
|
+
(under a set naming the field separator `F`, `28.6&F&F&F&U/L` is that separator escaped, written and
|
|
180
|
+
escaped again, entirely well formed, and refusing it is the over-refusal that sank a criterion
|
|
181
|
+
measured for this same family); and where it heads an **unrecognized** sequence, the alignment taken
|
|
182
|
+
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.
|
|
185
|
+
|
|
186
|
+
**Measured against the corpus constants committed with the test**, on the same
|
|
187
|
+
strict-accepted-under-a-gate-legal-profile tier. Over `DECLARATION_ALPHABET`, `SPLITTING_ROLES`,
|
|
188
|
+
`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
|
|
193
|
+
would have refused 48 of them. The 32 that move are exactly the population the code above left
|
|
194
|
+
silent through its recognized-body exclusion.
|
|
195
|
+
|
|
196
|
+
**It does not survive a re-emit**, the same residue its siblings disclosed: the serializer rewrites
|
|
197
|
+
the preserved characters into recognized mnemonics, so a second-generation read is silent and is
|
|
198
|
+
correct about its own bytes. The first read of the wire bytes is where this exists to be caught, and
|
|
199
|
+
a clean re-read is not evidence.
|
|
200
|
+
|
|
201
|
+
This is a **narrowing on a published package**, on the strict path only: a lenient parse of the same
|
|
202
|
+
stream returns the same records, with the same values, and one more warning on them. New public
|
|
203
|
+
surface: `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS`, the `alignmentShiftedFields` warning factory, the
|
|
204
|
+
`ShiftedFieldsSink` type, and a trailing optional sink parameter on `splitEscapeAware`,
|
|
205
|
+
`tokenizeRecord` and `tokenizeHeader`.
|
|
206
|
+
|
|
12
207
|
### Fixed
|
|
13
208
|
|
|
209
|
+
- **A header that is not short no longer reports that it is too short** (`ASTM-FRAME-RESIDUALS`,
|
|
210
|
+
defect 16). `readDelimiters` had four ways to fail and the first header's fatal described all of
|
|
211
|
+
them with one sentence. Three were accurate; the fourth was not. `H||^&` is a full-length header
|
|
212
|
+
whose delimiter-definition field is empty because its own field separator ends the definition where
|
|
213
|
+
it begins, and it raised `ASTM_RECORD_UNDECLARED_DELIMITERS` reading "Header record is too short to
|
|
214
|
+
declare the four delimiters", which sent a reader counting characters instead of looking at the
|
|
215
|
+
declaration. `readDelimiterDeclaration` is `readDelimiters` with the reason kept, and the fatal now
|
|
216
|
+
carries the message for the reason it hit.
|
|
217
|
+
|
|
218
|
+
**The fatal code is unchanged and no stream's disposition moved.** A consumer switching on
|
|
219
|
+
`ASTM_RECORD_UNDECLARED_DELIMITERS` sees exactly what it saw before; only the human-readable
|
|
220
|
+
sentence changed. Splitting the code in two was considered and rejected: it would move a published
|
|
221
|
+
code for a stream whose outcome is already right, which is a breaking change bought for a sentence.
|
|
222
|
+
|
|
223
|
+
The reader's field-separator-reused branch is **unreachable** and stays. Such a separator ends the
|
|
224
|
+
delimiter definition where it appears, so the under-three-characters rule answers first, every
|
|
225
|
+
time, and that is now measured rather than argued: over `FIELD_SEPARATOR_ALPHABET` and
|
|
226
|
+
`DEFINITION_POSITIONS` in `test/common/delimiter-declaration-faults.test.ts`, all 36 collision-shaped
|
|
227
|
+
headers classify as a truncated definition and none as a reuse. Deleting the branch would leave the
|
|
228
|
+
invariant unstated and a change to how the definition field is bounded silent. New public surface:
|
|
229
|
+
`readDelimiterDeclaration`, the `DelimiterDeclarationFault` type, and a `fault` on the failure arm
|
|
230
|
+
of the previously unused `DelimiterReadResult`.
|
|
231
|
+
|
|
14
232
|
- **The escape encoder protected the escape character it introduced but not the `E`/`F`/`S`/`R`
|
|
15
233
|
mnemonics, so a delimiter set naming one of those letters in another role altered the values it
|
|
16
234
|
emitted** (`ASTM-FRAME-RESIDUALS` follow-up, defect 12). `PRE-EXISTING`, byte-identical on
|
package/README.md
CHANGED
|
@@ -177,15 +177,93 @@ package's serializer emits, so a stream it produced never trips the code. The co
|
|
|
177
177
|
so a vendor profile can expect it on a feed that sends bare ampersands and still parse
|
|
178
178
|
`{ strict: true }`.
|
|
179
179
|
|
|
180
|
-
**One escape shape still costs a field boundary, and it
|
|
181
|
-
sequence is opaque by design, which is what keeps `&F&` one token under a set that
|
|
182
|
-
delimiter. So where the body is
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
`
|
|
186
|
-
the
|
|
187
|
-
|
|
188
|
-
|
|
180
|
+
**One escape shape still costs a field boundary, and it has a code of its own.** A real
|
|
181
|
+
three-character sequence is opaque by design, which is what keeps `&F&` one token under a set that
|
|
182
|
+
names `F` as a delimiter. So where the body is an **unrecognized** character that is itself a
|
|
183
|
+
delimiter in force (`&|&` under the canonical set)
|
|
184
|
+
that delimiter does not split, and every field after it shifts: `R|1|^^^687|28.6&|&U/L||||F` reads a
|
|
185
|
+
value of `28.6&|&U/L` with no units and status `unspecified`. That reading is unchanged, and
|
|
186
|
+
narrowing the atom to change it would break the guarantee the atom exists for. What such a record now
|
|
187
|
+
raises, alongside the tolerable `ASTM_UNKNOWN_ESCAPE_SEQUENCE`, is
|
|
188
|
+
`ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE`, which **no profile may tolerate**, so a
|
|
189
|
+
`{ strict: true }` parse refuses it even under the shipped `referenceCorpus`. The narrower code fires
|
|
190
|
+
only where the unrecognized body is one of the three splitting roles in force. Two exclusions are
|
|
191
|
+
deliberate: the escape role, because nothing splits on it, and every **recognized** mnemonic, because
|
|
192
|
+
`&F&` under a set naming `F` as the repeat delimiter is the sender escaping the field separator on
|
|
193
|
+
purpose, and reporting that would report the escape mechanism working as a defect.
|
|
194
|
+
|
|
195
|
+
Read it as a report, not a repair. It also does not survive a re-emit: the serializer rewrites the
|
|
196
|
+
preserved sequence into recognized mnemonics, and that stream says the same value unambiguously, so a
|
|
197
|
+
second-generation read is silent and is right about its own bytes. The first read of the wire bytes
|
|
198
|
+
is where the condition exists to be caught.
|
|
199
|
+
|
|
200
|
+
**The mirror of it costs a boundary in the other direction, and it also has a code of its own.**
|
|
201
|
+
Sequences are matched greedily and leftmost, so the escape character that closes one cannot also open
|
|
202
|
+
the next. Where it could have, the same bytes carry two alignments that disagree by one boundary:
|
|
203
|
+
`R|1|^^^687|28.6&Z&|&U/L||||F` reads a value of `28.6&Z&` and units of `&U/L` under the alignment
|
|
204
|
+
taken, and reads as a single unsplit field carrying both under the other. Every byte is preserved and
|
|
205
|
+
the leftmost reading is kept (picking the other one would be a different guess with no more evidence
|
|
206
|
+
behind it), but the boundary it hands you is a choice, so it raises
|
|
207
|
+
`ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT`, which **no profile may tolerate**. Both codes the condition
|
|
208
|
+
raised before (`ASTM_UNKNOWN_ESCAPE_SEQUENCE` and `ASTM_UNPAIRED_ESCAPE_CHARACTER`) are tolerable, so
|
|
209
|
+
a strict parse under a profile naming them used to accept it. Two exclusions again. A **recognized**
|
|
210
|
+
mnemonic before the delimiter is silent, because the reading taken interprets a construct (`&F&` is
|
|
211
|
+
the sender escaping a separator, which is what the mechanism is for) while the competitor's body is a
|
|
212
|
+
delimiter character the codec usually cannot interpret, so its own vocabulary prefers the reading
|
|
213
|
+
taken. That is **not** the same as the reading taken being conformant, and the exclusion is wider
|
|
214
|
+
than that argument: `28.6&F&|&U/L` reads a value of `28.6|` and units of `&U/L` while raising only
|
|
215
|
+
the tolerable `ASTM_UNPAIRED_ESCAPE_CHARACTER`, and a declared set naming a mnemonic letter as a
|
|
216
|
+
splitting delimiter makes both alignments interpret one construct each with neither preferred. The
|
|
217
|
+
first of those is covered by a second code, below; the second is measured and recorded as an open
|
|
218
|
+
residue. The other exclusion is a delimiter with no escape character two positions past it, which is
|
|
219
|
+
no competing alignment at all. What does fire is a subset of what already raises
|
|
220
|
+
`ASTM_UNKNOWN_ESCAPE_SEQUENCE`. It does not survive a re-emit either, for the same reason as its
|
|
221
|
+
mirror: catch it on the first read.
|
|
222
|
+
|
|
223
|
+
**And where that gained boundary is a FIELD boundary, it moves a result's status, which is a
|
|
224
|
+
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
|
|
228
|
+
matters clinically: `R|1|^^^687|28.6&F&|&U/L||||F` reads **nine** fields under the alignment taken
|
|
229
|
+
and **eight** under the other, so the sender's trailing `F` lands in field 9, the result status,
|
|
230
|
+
under the first and in no field at all under the second. The parse hands back units of `&U/L` and a
|
|
231
|
+
status of **`final`**, and both are consequences of the alignment rather than values the sender put
|
|
232
|
+
in those slots. That raises `ASTM_RECORD_ALIGNMENT_SHIFTED_FIELDS`, which **no profile may
|
|
233
|
+
tolerate**; before it existed the only warning on that stream was the tolerable
|
|
234
|
+
`ASTM_UNPAIRED_ESCAPE_CHARACTER`, so a strict parse under a legal profile accepted it. The reading is
|
|
235
|
+
unchanged: it reports the shift rather than repairing it. It is wired to the **field** separator
|
|
236
|
+
only, because a gained repeat or component boundary divides one field and so moves no field-indexed
|
|
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.
|
|
244
|
+
|
|
245
|
+
### A header that names one character in two delimiter roles
|
|
246
|
+
|
|
247
|
+
ASTM messages are self-describing: the `H` record declares the four delimiters and a conformant
|
|
248
|
+
reader follows them. A declaration whose **field** separator is also one of the other three is
|
|
249
|
+
refused outright, because the four roles would be indistinguishable. A declaration where two of the
|
|
250
|
+
_remaining_ three agree is still read, because the stream is readable and refusing it would drop
|
|
251
|
+
records the sender did send, but the boundary between those two roles is no longer in the bytes.
|
|
252
|
+
|
|
253
|
+
Under `H|^^&`, where the repeat and component roles are both `^`, a field a canonical sender would
|
|
254
|
+
have written as two repeats of two components reads back as four repeats of one component each.
|
|
255
|
+
Under `H|\&&`, where the component and escape roles are both `&`, the same character splits
|
|
256
|
+
(`A&B` is two components) or opens an escape atom (`A&F&B` is the single component `A|B`) depending
|
|
257
|
+
only on what follows it.
|
|
258
|
+
|
|
259
|
+
Such a header now raises `ASTM_RECORD_DELIMITER_ROLE_COLLISION`, at the header that put the set into
|
|
260
|
+
force, once rather than once per colliding pair. A later header restating the set already in force
|
|
261
|
+
warns nothing, on the same rule that governs the other delimiter warnings. The code is **not** tolerable. That matters
|
|
262
|
+
because every such set is by definition non-canonical, so the only warning it used to raise was
|
|
263
|
+
`ASTM_NONSTANDARD_DELIMITERS`, which is tolerable: a profile expecting an ordinary vendor set left a
|
|
264
|
+
`{ strict: true }` parse accepting a declaration whose own field tree cannot be recovered. Emit has
|
|
265
|
+
always refused these sets (`ASTM_EMIT_INVALID_DELIMITERS`), which is how the gap first showed: a
|
|
266
|
+
message that parsed clean threw when it was serialized back against its own declared delimiters.
|
|
189
267
|
|
|
190
268
|
### Several messages in one stream
|
|
191
269
|
|
|
@@ -251,9 +329,17 @@ outside it:
|
|
|
251
329
|
and the damage then varies. A mis-split component can cost a test identity while the value and
|
|
252
330
|
units survive. The escape role's worst case has **narrowed, not gone**: a bare escape character no
|
|
253
331
|
longer merges the rest of the record (it reads as a literal and raises
|
|
254
|
-
`ASTM_UNPAIRED_ESCAPE_CHARACTER`), but an `&X&` sequence whose body is
|
|
332
|
+
`ASTM_UNPAIRED_ESCAPE_CHARACTER`), but an `&X&` sequence whose body is an unrecognized character
|
|
333
|
+
that is itself a delimiter in force is an opaque
|
|
255
334
|
atom, so that delimiter does not split and the value, the units and the status can still go
|
|
256
|
-
together. That one
|
|
335
|
+
together. That one raises `ASTM_RECORD_DELIMITER_SWALLOWED_BY_ESCAPE`, which is not tolerable,
|
|
336
|
+
alongside the tolerable `ASTM_UNKNOWN_ESCAPE_SEQUENCE`. The split itself is unchanged. Its mirror,
|
|
337
|
+
where the leftmost alignment lets a delimiter split that a competing alignment would have held,
|
|
338
|
+
gains a boundary instead of losing one and raises `ASTM_RECORD_AMBIGUOUS_ESCAPE_ALIGNMENT`, also
|
|
339
|
+
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
|
|
341
|
+
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.
|
|
257
343
|
|
|
258
344
|
All are accepted limits, for two different reasons: widening the field-separator check would mean
|
|
259
345
|
deciding which set a record ought to have had, which is the same guess the parser declines to make
|