@cosyte/astm 0.0.7 → 0.0.8
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 +67 -0
- package/README.md +20 -5
- package/dist/index.cjs +138 -134
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +136 -34
- package/dist/index.d.ts +136 -34
- package/dist/index.mjs +138 -134
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -51,7 +51,11 @@ interface AstmPosition {
|
|
|
51
51
|
* ```
|
|
52
52
|
*/
|
|
53
53
|
declare const WARNING_CODES: {
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* A record's type letter is not one of the modeled types: surfaced as an unsupported record,
|
|
56
|
+
* never dropped. Treat it as a possible lost message boundary, because a header the reader did
|
|
57
|
+
* not recognize as one does not open a new message.
|
|
58
|
+
*/
|
|
55
59
|
readonly ASTM_RECORD_UNKNOWN_TYPE: "ASTM_RECORD_UNKNOWN_TYPE";
|
|
56
60
|
/** The header declared delimiters other than the canonical `H|\^&`: tolerated, noted. */
|
|
57
61
|
readonly ASTM_NONSTANDARD_DELIMITERS: "ASTM_NONSTANDARD_DELIMITERS";
|
|
@@ -185,6 +189,11 @@ interface AstmRecordWarning {
|
|
|
185
189
|
* Build an `ASTM_RECORD_UNKNOWN_TYPE` warning. The record is still surfaced (as
|
|
186
190
|
* an unsupported record), never dropped.
|
|
187
191
|
*
|
|
192
|
+
* **This one is not cosmetic.** Message grouping decides where a message starts by
|
|
193
|
+
* reading each record's type letter, so an unrecognized letter may be a header the
|
|
194
|
+
* reader failed to see, and two messages then read as one. A profile is therefore
|
|
195
|
+
* not permitted to tolerate this code.
|
|
196
|
+
*
|
|
188
197
|
* @example
|
|
189
198
|
* ```ts
|
|
190
199
|
* import { unknownRecordType } from "@cosyte/astm";
|
|
@@ -986,10 +995,11 @@ declare function detectFraming(bytes: Uint8Array, options?: DetectFramingOptions
|
|
|
986
995
|
*
|
|
987
996
|
* 1. **A profile can never tolerate a safety-critical warning code**: a result
|
|
988
997
|
* value, abnormal flag, result status, reference range, units, patient/comment
|
|
989
|
-
* context, message-kind ambiguity, code system,
|
|
990
|
-
* warning (see `src/profiles/safety.ts`). Naming one is
|
|
991
|
-
* throw*, not a silent relaxation, so a profile can never
|
|
992
|
-
* "ok"
|
|
998
|
+
* context, message-kind ambiguity, code system, an unrecognized record type, or
|
|
999
|
+
* any frame/LTP integrity warning (see `src/profiles/safety.ts`). Naming one is
|
|
1000
|
+
* a *definition-time throw*, not a silent relaxation, so a profile can never
|
|
1001
|
+
* make a bad checksum "ok", a cancelled result read "final", or quiet the
|
|
1002
|
+
* warning that says two messages were read as one.
|
|
993
1003
|
* 2. **A tolerated deviation is downgraded, never dropped.** The parser still
|
|
994
1004
|
* records it (re-coded {@link WARNING_CODES.PROFILE_QUIRK_APPLIED}, flagged
|
|
995
1005
|
* `expected: true`, carrying the original `toleratedCode`) so nothing is
|
|
@@ -1124,7 +1134,7 @@ interface AstmProfile {
|
|
|
1124
1134
|
* const opts: DefineAstmProfileOptions = {
|
|
1125
1135
|
* name: "my-analyzer",
|
|
1126
1136
|
* transport: "raw",
|
|
1127
|
-
* tolerate: [{ code: "
|
|
1137
|
+
* tolerate: [{ code: "ASTM_NONSTANDARD_DELIMITERS", rationale: "declares its own set" }],
|
|
1128
1138
|
* };
|
|
1129
1139
|
* const p = defineAstmProfile(opts);
|
|
1130
1140
|
* ```
|
|
@@ -1685,6 +1695,11 @@ interface TerminatorRecord extends RecordBase {
|
|
|
1685
1695
|
* `H`/`P`/`O`/`R`/`C`/`Q`/`M`/`S`/`L` are all modeled; anything else is surfaced
|
|
1686
1696
|
* with its raw fields intact and flagged with an
|
|
1687
1697
|
* `ASTM_RECORD_UNKNOWN_TYPE` warning, never dropped.
|
|
1698
|
+
*
|
|
1699
|
+
* **One of these may be a header.** Message grouping decides where a message
|
|
1700
|
+
* starts by reading the type letter, so a header the reader could not recognize
|
|
1701
|
+
* arrives here instead, opens no message, and the messages either side of it are
|
|
1702
|
+
* grouped as one. Check `rawType` before trusting a split.
|
|
1688
1703
|
*/
|
|
1689
1704
|
interface UnsupportedRecord extends RecordBase {
|
|
1690
1705
|
readonly type: "unsupported";
|
|
@@ -2436,6 +2451,26 @@ declare function query(msg: AstmMessage): readonly QueryRecord[];
|
|
|
2436
2451
|
* is made this way because it is the boundary this parser already enforces for delimiters,
|
|
2437
2452
|
* and because it never drops a record.
|
|
2438
2453
|
*
|
|
2454
|
+
* **What that costs, and where the cost is paid.** Grouping reads a record's **type letter**,
|
|
2455
|
+
* so it is only as good as the reader's ability to recognize one. A header carrying a stray
|
|
2456
|
+
* leading byte reads as an unsupported record, opens no message, and the two messages either
|
|
2457
|
+
* side of it merge: a patient from the first is then paired with results from the second, which
|
|
2458
|
+
* is the precise pairing {@link messages} exists to prevent. The parser does report it, as an
|
|
2459
|
+
* `ASTM_RECORD_UNKNOWN_TYPE` warning against that record, and a strict parse refuses the stream
|
|
2460
|
+
* outright. The cost is not only misattribution: delimiters are re-read at each `H` too, so an
|
|
2461
|
+
* unrecognized header does not re-scope them either, and where the sets differ the merged tail is
|
|
2462
|
+
* tokenized with the previous header's delimiters and its fields are lost rather than merely
|
|
2463
|
+
* misfiled. Because that warning is the only report the merge produces, a profile is **not**
|
|
2464
|
+
* permitted to tolerate the code and quiet it; the safety gate refuses the code when a profile is
|
|
2465
|
+
* defined, and declines to downgrade the warning whatever profile is in force. Read an
|
|
2466
|
+
* `ASTM_RECORD_UNKNOWN_TYPE` on a stream you are about to group as "a message boundary may be
|
|
2467
|
+
* missing here", not as cosmetic noise.
|
|
2468
|
+
*
|
|
2469
|
+
* Recognizing a mangled header *as* a header is deliberately not attempted. Deciding that an
|
|
2470
|
+
* unsupported record was "really" an `H` means guessing at a byte the sender did not send, and
|
|
2471
|
+
* that guess would silently split a stream a different way; a warning that the reader lost its
|
|
2472
|
+
* place is the honest answer, and refusing the stream in strict mode is the safe one.
|
|
2473
|
+
*
|
|
2439
2474
|
* **What is deliberately not modeled.** ASTM's within-message record hierarchy, which `P`
|
|
2440
2475
|
* a given `R` files against when one message carries several patients, is not modeled
|
|
2441
2476
|
* here. The clauses that would ground it (the message-level structure diagram, and the `P`
|
|
@@ -2912,7 +2947,7 @@ declare function deepFreeze<T>(value: T): T;
|
|
|
2912
2947
|
* description: "Acme's inbound raw-TCP analyzer",
|
|
2913
2948
|
* transport: "raw",
|
|
2914
2949
|
* tolerate: [
|
|
2915
|
-
* { code: "
|
|
2950
|
+
* { code: "ASTM_NONSTANDARD_DELIMITERS", rationale: "declares its own delimiter set" },
|
|
2916
2951
|
* ],
|
|
2917
2952
|
* provenance: { source: "Acme host-interface manual", reference: "internal-2026" },
|
|
2918
2953
|
* });
|
|
@@ -2938,7 +2973,8 @@ declare function defineAstmProfile(opts: DefineAstmProfileOptions): AstmProfile;
|
|
|
2938
2973
|
* `tolerate` entry whose code is unknown, whose `rationale` is empty, or, the
|
|
2939
2974
|
* load-bearing safety rule, whose code is **safety-critical** (a result value,
|
|
2940
2975
|
* abnormal flag, result status, reference range, units, patient/comment context,
|
|
2941
|
-
* message-kind, code system, or any frame/LTP
|
|
2976
|
+
* message-kind, code system, an unrecognized record type, or any frame/LTP
|
|
2977
|
+
* integrity warning). Carries the
|
|
2942
2978
|
* offending profile's `name` when it is known, so a multi-profile build names the
|
|
2943
2979
|
* culprit.
|
|
2944
2980
|
*
|
|
@@ -3059,6 +3095,17 @@ declare function getDefaultAstmProfile(): AstmProfile | undefined;
|
|
|
3059
3095
|
* never reallocated). A warning that is already `expected` (e.g. re-processed) is
|
|
3060
3096
|
* passed through untouched.
|
|
3061
3097
|
*
|
|
3098
|
+
* **The safety gate is re-checked here, and that is not redundant.**
|
|
3099
|
+
* {@link defineAstmProfile} refuses a safety-critical code at definition time, but
|
|
3100
|
+
* {@link AstmProfile} is a plain interface: a hand-authored object literal
|
|
3101
|
+
* type-checks, and `parseAstmRecords(raw, { profile })` and `setDefaultAstmProfile`
|
|
3102
|
+
* both accept one without re-running the factory. A definition-time-only gate
|
|
3103
|
+
* therefore guards a door that has a second entrance. A safety-critical code is
|
|
3104
|
+
* **not downgraded here whatever the profile says**, so the original warning
|
|
3105
|
+
* survives and `strict` still escalates it. Silently declining to downgrade, rather
|
|
3106
|
+
* than throwing, is the fail-safe direction: the signal is preserved either way, and
|
|
3107
|
+
* a parse is not turned into an exception by a profile the caller built by hand.
|
|
3108
|
+
*
|
|
3062
3109
|
* @param profile - The active profile.
|
|
3063
3110
|
* @param warning - One accumulated warning.
|
|
3064
3111
|
* @returns The re-badged warning when tolerated, else the original.
|
|
@@ -3109,7 +3156,7 @@ declare function resolveProfileTransport(profile: AstmProfile | undefined): Astm
|
|
|
3109
3156
|
* The safety spine of the profile subsystem: the set of warning codes a profile is
|
|
3110
3157
|
* **forbidden** to tolerate. A vendor profile exists to quiet known, benign
|
|
3111
3158
|
* structural noise: never to hide a deviation that could change a clinical
|
|
3112
|
-
* reading or corrupt the wire.
|
|
3159
|
+
* reading, lose track of where one message ends, or corrupt the wire.
|
|
3113
3160
|
*
|
|
3114
3161
|
* **Design: default-deny.** Rather than enumerate the *forbidden* codes (a list you
|
|
3115
3162
|
* can forget to extend when a new warning ships, the exact incompleteness a
|
|
@@ -3121,43 +3168,93 @@ declare function resolveProfileTransport(profile: AstmProfile | undefined): Astm
|
|
|
3121
3168
|
* the allow-list below. Widening the allow-list can only ever be an explicit,
|
|
3122
3169
|
* argued act; forgetting to touch this file can never *weaken* the gate.
|
|
3123
3170
|
*
|
|
3124
|
-
* The
|
|
3125
|
-
*
|
|
3126
|
-
*
|
|
3127
|
-
*
|
|
3128
|
-
* profile
|
|
3171
|
+
* **The test for admitting a code has two parts, and the second one is easy to
|
|
3172
|
+
* miss.** A code may sit on the allow-list only while
|
|
3173
|
+
*
|
|
3174
|
+
* 1. it is structural or syntactic noise that **cannot alter, drop, or fabricate
|
|
3175
|
+
* an extracted value** (a profile re-badges a *warning*, it never re-parses, so
|
|
3176
|
+
* the underlying datum is surfaced identically with or without the profile);
|
|
3177
|
+
* **and**
|
|
3178
|
+
* 2. **nothing else in this package reads the condition the warning reports.**
|
|
3179
|
+
*
|
|
3180
|
+
* Part 2 is a claim about the whole library, not about the warning, and it is the
|
|
3181
|
+
* part that can stop being true without anyone touching this file. A code admitted
|
|
3182
|
+
* as benign stops being benign the moment a later feature starts making a decision
|
|
3183
|
+
* out of the very condition the warning is reporting, and adding that feature does
|
|
3184
|
+
* not force this file to be revisited. That is not hypothetical: it happened once
|
|
3185
|
+
* here, to `ASTM_RECORD_UNKNOWN_TYPE`, and the consequence was a wrong-patient
|
|
3186
|
+
* path.
|
|
3187
|
+
*
|
|
3188
|
+
* **A record's type letter acquired two load-bearing readers, and the code was
|
|
3189
|
+
* admitted before either existed.** Message grouping reads the letter to decide
|
|
3190
|
+
* where one message ends and the next begins, so an unrecognized header opens no
|
|
3191
|
+
* message and two messages are read as one, pairing a patient with results that
|
|
3192
|
+
* arrived under a different header. Message classification counts `Q` / `R` / `O`
|
|
3193
|
+
* records by the same letter, so an unrecognized `Q` costs a host-query request its
|
|
3194
|
+
* `Q`-dominates guarantee: it reads as a result set, and the
|
|
3195
|
+
* `ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND` warning that flags that contradiction goes
|
|
3196
|
+
* with it. In both cases `ASTM_RECORD_UNKNOWN_TYPE` is the only warning left on the
|
|
3197
|
+
* stream, while the allow-list still described it as harmless. Tolerating it
|
|
3198
|
+
* re-badged the single report that the reader had lost its place, and a strict parse
|
|
3199
|
+
* then accepted the stream. It is off the list for that reason, and its removal is
|
|
3200
|
+
* what part 2 above is written down to prevent a repeat of.
|
|
3201
|
+
*
|
|
3202
|
+
* **So the three that remain are recorded with the reading each one survives**, not
|
|
3203
|
+
* merely with the value it preserves. Each was re-derived against both readers of
|
|
3204
|
+
* record structure above, plus the single-message / single-patient guards:
|
|
3129
3205
|
*
|
|
3130
|
-
* - `ASTM_RECORD_UNKNOWN_TYPE`: an unmodeled record type, surfaced verbatim as an
|
|
3131
|
-
* `unsupported` record carrying no interpreted clinical field.
|
|
3132
3206
|
* - `ASTM_NONSTANDARD_DELIMITERS`: the header's own declared delimiters are read
|
|
3133
|
-
* and honored either way
|
|
3134
|
-
*
|
|
3135
|
-
*
|
|
3136
|
-
*
|
|
3137
|
-
*
|
|
3138
|
-
*
|
|
3139
|
-
*
|
|
3140
|
-
* is never
|
|
3207
|
+
* and honored either way, and this notes only that they differ from the canonical
|
|
3208
|
+
* set. Neither reader sees them: the stream is cut into records on a line break,
|
|
3209
|
+
* and a record's type letter is the first character of its line, both taken before
|
|
3210
|
+
* any delimiter-driven tokenization runs.
|
|
3211
|
+
* - `ASTM_UNKNOWN_ESCAPE_SEQUENCE`: an unrecognized escape body is **preserved
|
|
3212
|
+
* byte-for-byte** in the decoded value (the escape codec does not guess at one),
|
|
3213
|
+
* so the value is identical with or without the profile. Neither reader sees it
|
|
3214
|
+
* either: a decoded field value is never cut back into records, and the type
|
|
3215
|
+
* letter is read before decoding.
|
|
3216
|
+
* - `ASTM_RECORD_UNINTERPRETED_QUERY_STATUS`: a request-information status carried
|
|
3217
|
+
* verbatim as a leaf field on a `Q` record; the code set is paywalled and is not
|
|
3218
|
+
* interpreted, profile or not. Classification reads whether a `Q` record is
|
|
3219
|
+
* *present*, never what this field says, and grouping does not read `Q` at all.
|
|
3141
3220
|
*
|
|
3142
3221
|
* Everything a wrong value could hide from: a result value split ambiguity, an
|
|
3143
3222
|
* undefined abnormal flag or result status, an unparseable reference range, absent
|
|
3144
3223
|
* units, a mis-attached comment, a partial timestamp, a query-vs-result ambiguity,
|
|
3145
|
-
* a bad frame checksum / sequence gap / unterminated /
|
|
3146
|
-
* ambiguous transport, an unexpected protocol event, or a
|
|
3147
|
-
* forbidden.
|
|
3224
|
+
* an unrecognized record type, a bad frame checksum / sequence gap / unterminated /
|
|
3225
|
+
* oversize frame, an ambiguous transport, an unexpected protocol event, or a
|
|
3226
|
+
* rejected frame: is forbidden.
|
|
3227
|
+
*
|
|
3228
|
+
* **What this file cannot do for you.** Part 2 is a review obligation, not a
|
|
3229
|
+
* mechanical one. There is no honest automatic check for "nothing load-bearing
|
|
3230
|
+
* reads this", because deciding whether a future reader of record structure has put
|
|
3231
|
+
* an allow-listed code back in the path takes reading the reader. What is
|
|
3232
|
+
* mechanical is the direction of the failure: default-deny means a *new* code is
|
|
3233
|
+
* refused until argued in, so the residual risk is confined to the small, explicit
|
|
3234
|
+
* list below. Re-derive that list whenever something new starts reading record
|
|
3235
|
+
* structure.
|
|
3148
3236
|
*/
|
|
3149
3237
|
|
|
3150
3238
|
/**
|
|
3151
|
-
* The **only** warning codes a profile may list in its `tolerate` set: benign
|
|
3152
|
-
* structural
|
|
3153
|
-
* extracted value
|
|
3154
|
-
*
|
|
3155
|
-
*
|
|
3239
|
+
* The **only** warning codes a profile may list in its `tolerate` set: benign
|
|
3240
|
+
* structural or syntactic vendor noise that cannot alter, drop, or fabricate an
|
|
3241
|
+
* extracted value, **and** whose reported condition nothing else in this package
|
|
3242
|
+
* reads: not to decide where one message ends and the next begins, and not to
|
|
3243
|
+
* decide what kind of message it is. Frozen so it cannot be mutated at runtime to
|
|
3244
|
+
* smuggle a code in. Adding to this set is a deliberate, reviewable act, and an
|
|
3245
|
+
* addition has to satisfy both halves of that test, not just the first.
|
|
3246
|
+
*
|
|
3247
|
+
* `ASTM_RECORD_UNKNOWN_TYPE` is **not** on this list, and used to be. An
|
|
3248
|
+
* unrecognized record type may be a header the reader did not recognize as one, in
|
|
3249
|
+
* which case two messages have been read as one, or a `Q` it did not recognize, in
|
|
3250
|
+
* which case a host-query request reads as a result set. Tolerating it can quiet
|
|
3251
|
+
* the only report either happened.
|
|
3156
3252
|
*
|
|
3157
3253
|
* @example
|
|
3158
3254
|
* ```ts
|
|
3159
3255
|
* import { TOLERABLE_CODES } from "@cosyte/astm";
|
|
3160
3256
|
* TOLERABLE_CODES.has("ASTM_UNKNOWN_ESCAPE_SEQUENCE"); // true
|
|
3257
|
+
* TOLERABLE_CODES.has("ASTM_RECORD_UNKNOWN_TYPE"); // false
|
|
3161
3258
|
* TOLERABLE_CODES.has("ASTM_FRAME_BAD_CHECKSUM"); // false
|
|
3162
3259
|
* ```
|
|
3163
3260
|
*/
|
|
@@ -3182,6 +3279,7 @@ declare const ALL_ASTM_WARNING_CODES: ReadonlySet<AnyAstmWarningCode>;
|
|
|
3182
3279
|
* ```ts
|
|
3183
3280
|
* import { SAFETY_CRITICAL_CODES } from "@cosyte/astm";
|
|
3184
3281
|
* SAFETY_CRITICAL_CODES.has("ASTM_RECORD_UNDEFINED_RESULT_STATUS"); // true
|
|
3282
|
+
* SAFETY_CRITICAL_CODES.has("ASTM_RECORD_UNKNOWN_TYPE"); // true
|
|
3185
3283
|
* SAFETY_CRITICAL_CODES.has("ASTM_FRAME_BAD_CHECKSUM"); // true
|
|
3186
3284
|
* ```
|
|
3187
3285
|
*/
|
|
@@ -3199,6 +3297,7 @@ declare const SAFETY_CRITICAL_CODES: ReadonlySet<AnyAstmWarningCode>;
|
|
|
3199
3297
|
* import { isSafetyCriticalCode } from "@cosyte/astm";
|
|
3200
3298
|
* isSafetyCriticalCode("ASTM_UNKNOWN_ESCAPE_SEQUENCE"); // false (tolerable)
|
|
3201
3299
|
* isSafetyCriticalCode("ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT"); // true
|
|
3300
|
+
* isSafetyCriticalCode("ASTM_RECORD_UNKNOWN_TYPE"); // true (it can mean a lost message boundary)
|
|
3202
3301
|
* ```
|
|
3203
3302
|
*/
|
|
3204
3303
|
declare function isSafetyCriticalCode(code: string): boolean;
|
|
@@ -3255,8 +3354,11 @@ declare const defaultProfile: AstmProfile;
|
|
|
3255
3354
|
* only re-badges the *warning*: no value, flag, status, or identifier is altered
|
|
3256
3355
|
* or dropped. And by the safety gate in `defineAstmProfile`, it **cannot** tolerate
|
|
3257
3356
|
* a result value / flag / status / range / units, a patient or comment context, a
|
|
3258
|
-
* message-kind ambiguity, or any frame / LTP integrity
|
|
3259
|
-
* surface, profile or not.
|
|
3357
|
+
* message-kind ambiguity, an unrecognized record type, or any frame / LTP integrity
|
|
3358
|
+
* warning: those always surface, profile or not. The unrecognized-record-type case
|
|
3359
|
+
* matters because message grouping reads a record's type letter, so a letter the
|
|
3360
|
+
* reader does not recognize may be a header, and a header read as something else
|
|
3361
|
+
* joins two messages into one.
|
|
3260
3362
|
*
|
|
3261
3363
|
* **Honesty.** This is a *reference-corpus* profile, not a claim about any named
|
|
3262
3364
|
* analyzer vendor. No per-vendor ("cobas does X") behaviour is asserted; a named
|
package/dist/index.d.ts
CHANGED
|
@@ -51,7 +51,11 @@ interface AstmPosition {
|
|
|
51
51
|
* ```
|
|
52
52
|
*/
|
|
53
53
|
declare const WARNING_CODES: {
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* A record's type letter is not one of the modeled types: surfaced as an unsupported record,
|
|
56
|
+
* never dropped. Treat it as a possible lost message boundary, because a header the reader did
|
|
57
|
+
* not recognize as one does not open a new message.
|
|
58
|
+
*/
|
|
55
59
|
readonly ASTM_RECORD_UNKNOWN_TYPE: "ASTM_RECORD_UNKNOWN_TYPE";
|
|
56
60
|
/** The header declared delimiters other than the canonical `H|\^&`: tolerated, noted. */
|
|
57
61
|
readonly ASTM_NONSTANDARD_DELIMITERS: "ASTM_NONSTANDARD_DELIMITERS";
|
|
@@ -185,6 +189,11 @@ interface AstmRecordWarning {
|
|
|
185
189
|
* Build an `ASTM_RECORD_UNKNOWN_TYPE` warning. The record is still surfaced (as
|
|
186
190
|
* an unsupported record), never dropped.
|
|
187
191
|
*
|
|
192
|
+
* **This one is not cosmetic.** Message grouping decides where a message starts by
|
|
193
|
+
* reading each record's type letter, so an unrecognized letter may be a header the
|
|
194
|
+
* reader failed to see, and two messages then read as one. A profile is therefore
|
|
195
|
+
* not permitted to tolerate this code.
|
|
196
|
+
*
|
|
188
197
|
* @example
|
|
189
198
|
* ```ts
|
|
190
199
|
* import { unknownRecordType } from "@cosyte/astm";
|
|
@@ -986,10 +995,11 @@ declare function detectFraming(bytes: Uint8Array, options?: DetectFramingOptions
|
|
|
986
995
|
*
|
|
987
996
|
* 1. **A profile can never tolerate a safety-critical warning code**: a result
|
|
988
997
|
* value, abnormal flag, result status, reference range, units, patient/comment
|
|
989
|
-
* context, message-kind ambiguity, code system,
|
|
990
|
-
* warning (see `src/profiles/safety.ts`). Naming one is
|
|
991
|
-
* throw*, not a silent relaxation, so a profile can never
|
|
992
|
-
* "ok"
|
|
998
|
+
* context, message-kind ambiguity, code system, an unrecognized record type, or
|
|
999
|
+
* any frame/LTP integrity warning (see `src/profiles/safety.ts`). Naming one is
|
|
1000
|
+
* a *definition-time throw*, not a silent relaxation, so a profile can never
|
|
1001
|
+
* make a bad checksum "ok", a cancelled result read "final", or quiet the
|
|
1002
|
+
* warning that says two messages were read as one.
|
|
993
1003
|
* 2. **A tolerated deviation is downgraded, never dropped.** The parser still
|
|
994
1004
|
* records it (re-coded {@link WARNING_CODES.PROFILE_QUIRK_APPLIED}, flagged
|
|
995
1005
|
* `expected: true`, carrying the original `toleratedCode`) so nothing is
|
|
@@ -1124,7 +1134,7 @@ interface AstmProfile {
|
|
|
1124
1134
|
* const opts: DefineAstmProfileOptions = {
|
|
1125
1135
|
* name: "my-analyzer",
|
|
1126
1136
|
* transport: "raw",
|
|
1127
|
-
* tolerate: [{ code: "
|
|
1137
|
+
* tolerate: [{ code: "ASTM_NONSTANDARD_DELIMITERS", rationale: "declares its own set" }],
|
|
1128
1138
|
* };
|
|
1129
1139
|
* const p = defineAstmProfile(opts);
|
|
1130
1140
|
* ```
|
|
@@ -1685,6 +1695,11 @@ interface TerminatorRecord extends RecordBase {
|
|
|
1685
1695
|
* `H`/`P`/`O`/`R`/`C`/`Q`/`M`/`S`/`L` are all modeled; anything else is surfaced
|
|
1686
1696
|
* with its raw fields intact and flagged with an
|
|
1687
1697
|
* `ASTM_RECORD_UNKNOWN_TYPE` warning, never dropped.
|
|
1698
|
+
*
|
|
1699
|
+
* **One of these may be a header.** Message grouping decides where a message
|
|
1700
|
+
* starts by reading the type letter, so a header the reader could not recognize
|
|
1701
|
+
* arrives here instead, opens no message, and the messages either side of it are
|
|
1702
|
+
* grouped as one. Check `rawType` before trusting a split.
|
|
1688
1703
|
*/
|
|
1689
1704
|
interface UnsupportedRecord extends RecordBase {
|
|
1690
1705
|
readonly type: "unsupported";
|
|
@@ -2436,6 +2451,26 @@ declare function query(msg: AstmMessage): readonly QueryRecord[];
|
|
|
2436
2451
|
* is made this way because it is the boundary this parser already enforces for delimiters,
|
|
2437
2452
|
* and because it never drops a record.
|
|
2438
2453
|
*
|
|
2454
|
+
* **What that costs, and where the cost is paid.** Grouping reads a record's **type letter**,
|
|
2455
|
+
* so it is only as good as the reader's ability to recognize one. A header carrying a stray
|
|
2456
|
+
* leading byte reads as an unsupported record, opens no message, and the two messages either
|
|
2457
|
+
* side of it merge: a patient from the first is then paired with results from the second, which
|
|
2458
|
+
* is the precise pairing {@link messages} exists to prevent. The parser does report it, as an
|
|
2459
|
+
* `ASTM_RECORD_UNKNOWN_TYPE` warning against that record, and a strict parse refuses the stream
|
|
2460
|
+
* outright. The cost is not only misattribution: delimiters are re-read at each `H` too, so an
|
|
2461
|
+
* unrecognized header does not re-scope them either, and where the sets differ the merged tail is
|
|
2462
|
+
* tokenized with the previous header's delimiters and its fields are lost rather than merely
|
|
2463
|
+
* misfiled. Because that warning is the only report the merge produces, a profile is **not**
|
|
2464
|
+
* permitted to tolerate the code and quiet it; the safety gate refuses the code when a profile is
|
|
2465
|
+
* defined, and declines to downgrade the warning whatever profile is in force. Read an
|
|
2466
|
+
* `ASTM_RECORD_UNKNOWN_TYPE` on a stream you are about to group as "a message boundary may be
|
|
2467
|
+
* missing here", not as cosmetic noise.
|
|
2468
|
+
*
|
|
2469
|
+
* Recognizing a mangled header *as* a header is deliberately not attempted. Deciding that an
|
|
2470
|
+
* unsupported record was "really" an `H` means guessing at a byte the sender did not send, and
|
|
2471
|
+
* that guess would silently split a stream a different way; a warning that the reader lost its
|
|
2472
|
+
* place is the honest answer, and refusing the stream in strict mode is the safe one.
|
|
2473
|
+
*
|
|
2439
2474
|
* **What is deliberately not modeled.** ASTM's within-message record hierarchy, which `P`
|
|
2440
2475
|
* a given `R` files against when one message carries several patients, is not modeled
|
|
2441
2476
|
* here. The clauses that would ground it (the message-level structure diagram, and the `P`
|
|
@@ -2912,7 +2947,7 @@ declare function deepFreeze<T>(value: T): T;
|
|
|
2912
2947
|
* description: "Acme's inbound raw-TCP analyzer",
|
|
2913
2948
|
* transport: "raw",
|
|
2914
2949
|
* tolerate: [
|
|
2915
|
-
* { code: "
|
|
2950
|
+
* { code: "ASTM_NONSTANDARD_DELIMITERS", rationale: "declares its own delimiter set" },
|
|
2916
2951
|
* ],
|
|
2917
2952
|
* provenance: { source: "Acme host-interface manual", reference: "internal-2026" },
|
|
2918
2953
|
* });
|
|
@@ -2938,7 +2973,8 @@ declare function defineAstmProfile(opts: DefineAstmProfileOptions): AstmProfile;
|
|
|
2938
2973
|
* `tolerate` entry whose code is unknown, whose `rationale` is empty, or, the
|
|
2939
2974
|
* load-bearing safety rule, whose code is **safety-critical** (a result value,
|
|
2940
2975
|
* abnormal flag, result status, reference range, units, patient/comment context,
|
|
2941
|
-
* message-kind, code system, or any frame/LTP
|
|
2976
|
+
* message-kind, code system, an unrecognized record type, or any frame/LTP
|
|
2977
|
+
* integrity warning). Carries the
|
|
2942
2978
|
* offending profile's `name` when it is known, so a multi-profile build names the
|
|
2943
2979
|
* culprit.
|
|
2944
2980
|
*
|
|
@@ -3059,6 +3095,17 @@ declare function getDefaultAstmProfile(): AstmProfile | undefined;
|
|
|
3059
3095
|
* never reallocated). A warning that is already `expected` (e.g. re-processed) is
|
|
3060
3096
|
* passed through untouched.
|
|
3061
3097
|
*
|
|
3098
|
+
* **The safety gate is re-checked here, and that is not redundant.**
|
|
3099
|
+
* {@link defineAstmProfile} refuses a safety-critical code at definition time, but
|
|
3100
|
+
* {@link AstmProfile} is a plain interface: a hand-authored object literal
|
|
3101
|
+
* type-checks, and `parseAstmRecords(raw, { profile })` and `setDefaultAstmProfile`
|
|
3102
|
+
* both accept one without re-running the factory. A definition-time-only gate
|
|
3103
|
+
* therefore guards a door that has a second entrance. A safety-critical code is
|
|
3104
|
+
* **not downgraded here whatever the profile says**, so the original warning
|
|
3105
|
+
* survives and `strict` still escalates it. Silently declining to downgrade, rather
|
|
3106
|
+
* than throwing, is the fail-safe direction: the signal is preserved either way, and
|
|
3107
|
+
* a parse is not turned into an exception by a profile the caller built by hand.
|
|
3108
|
+
*
|
|
3062
3109
|
* @param profile - The active profile.
|
|
3063
3110
|
* @param warning - One accumulated warning.
|
|
3064
3111
|
* @returns The re-badged warning when tolerated, else the original.
|
|
@@ -3109,7 +3156,7 @@ declare function resolveProfileTransport(profile: AstmProfile | undefined): Astm
|
|
|
3109
3156
|
* The safety spine of the profile subsystem: the set of warning codes a profile is
|
|
3110
3157
|
* **forbidden** to tolerate. A vendor profile exists to quiet known, benign
|
|
3111
3158
|
* structural noise: never to hide a deviation that could change a clinical
|
|
3112
|
-
* reading or corrupt the wire.
|
|
3159
|
+
* reading, lose track of where one message ends, or corrupt the wire.
|
|
3113
3160
|
*
|
|
3114
3161
|
* **Design: default-deny.** Rather than enumerate the *forbidden* codes (a list you
|
|
3115
3162
|
* can forget to extend when a new warning ships, the exact incompleteness a
|
|
@@ -3121,43 +3168,93 @@ declare function resolveProfileTransport(profile: AstmProfile | undefined): Astm
|
|
|
3121
3168
|
* the allow-list below. Widening the allow-list can only ever be an explicit,
|
|
3122
3169
|
* argued act; forgetting to touch this file can never *weaken* the gate.
|
|
3123
3170
|
*
|
|
3124
|
-
* The
|
|
3125
|
-
*
|
|
3126
|
-
*
|
|
3127
|
-
*
|
|
3128
|
-
* profile
|
|
3171
|
+
* **The test for admitting a code has two parts, and the second one is easy to
|
|
3172
|
+
* miss.** A code may sit on the allow-list only while
|
|
3173
|
+
*
|
|
3174
|
+
* 1. it is structural or syntactic noise that **cannot alter, drop, or fabricate
|
|
3175
|
+
* an extracted value** (a profile re-badges a *warning*, it never re-parses, so
|
|
3176
|
+
* the underlying datum is surfaced identically with or without the profile);
|
|
3177
|
+
* **and**
|
|
3178
|
+
* 2. **nothing else in this package reads the condition the warning reports.**
|
|
3179
|
+
*
|
|
3180
|
+
* Part 2 is a claim about the whole library, not about the warning, and it is the
|
|
3181
|
+
* part that can stop being true without anyone touching this file. A code admitted
|
|
3182
|
+
* as benign stops being benign the moment a later feature starts making a decision
|
|
3183
|
+
* out of the very condition the warning is reporting, and adding that feature does
|
|
3184
|
+
* not force this file to be revisited. That is not hypothetical: it happened once
|
|
3185
|
+
* here, to `ASTM_RECORD_UNKNOWN_TYPE`, and the consequence was a wrong-patient
|
|
3186
|
+
* path.
|
|
3187
|
+
*
|
|
3188
|
+
* **A record's type letter acquired two load-bearing readers, and the code was
|
|
3189
|
+
* admitted before either existed.** Message grouping reads the letter to decide
|
|
3190
|
+
* where one message ends and the next begins, so an unrecognized header opens no
|
|
3191
|
+
* message and two messages are read as one, pairing a patient with results that
|
|
3192
|
+
* arrived under a different header. Message classification counts `Q` / `R` / `O`
|
|
3193
|
+
* records by the same letter, so an unrecognized `Q` costs a host-query request its
|
|
3194
|
+
* `Q`-dominates guarantee: it reads as a result set, and the
|
|
3195
|
+
* `ASTM_RECORD_AMBIGUOUS_MESSAGE_KIND` warning that flags that contradiction goes
|
|
3196
|
+
* with it. In both cases `ASTM_RECORD_UNKNOWN_TYPE` is the only warning left on the
|
|
3197
|
+
* stream, while the allow-list still described it as harmless. Tolerating it
|
|
3198
|
+
* re-badged the single report that the reader had lost its place, and a strict parse
|
|
3199
|
+
* then accepted the stream. It is off the list for that reason, and its removal is
|
|
3200
|
+
* what part 2 above is written down to prevent a repeat of.
|
|
3201
|
+
*
|
|
3202
|
+
* **So the three that remain are recorded with the reading each one survives**, not
|
|
3203
|
+
* merely with the value it preserves. Each was re-derived against both readers of
|
|
3204
|
+
* record structure above, plus the single-message / single-patient guards:
|
|
3129
3205
|
*
|
|
3130
|
-
* - `ASTM_RECORD_UNKNOWN_TYPE`: an unmodeled record type, surfaced verbatim as an
|
|
3131
|
-
* `unsupported` record carrying no interpreted clinical field.
|
|
3132
3206
|
* - `ASTM_NONSTANDARD_DELIMITERS`: the header's own declared delimiters are read
|
|
3133
|
-
* and honored either way
|
|
3134
|
-
*
|
|
3135
|
-
*
|
|
3136
|
-
*
|
|
3137
|
-
*
|
|
3138
|
-
*
|
|
3139
|
-
*
|
|
3140
|
-
* is never
|
|
3207
|
+
* and honored either way, and this notes only that they differ from the canonical
|
|
3208
|
+
* set. Neither reader sees them: the stream is cut into records on a line break,
|
|
3209
|
+
* and a record's type letter is the first character of its line, both taken before
|
|
3210
|
+
* any delimiter-driven tokenization runs.
|
|
3211
|
+
* - `ASTM_UNKNOWN_ESCAPE_SEQUENCE`: an unrecognized escape body is **preserved
|
|
3212
|
+
* byte-for-byte** in the decoded value (the escape codec does not guess at one),
|
|
3213
|
+
* so the value is identical with or without the profile. Neither reader sees it
|
|
3214
|
+
* either: a decoded field value is never cut back into records, and the type
|
|
3215
|
+
* letter is read before decoding.
|
|
3216
|
+
* - `ASTM_RECORD_UNINTERPRETED_QUERY_STATUS`: a request-information status carried
|
|
3217
|
+
* verbatim as a leaf field on a `Q` record; the code set is paywalled and is not
|
|
3218
|
+
* interpreted, profile or not. Classification reads whether a `Q` record is
|
|
3219
|
+
* *present*, never what this field says, and grouping does not read `Q` at all.
|
|
3141
3220
|
*
|
|
3142
3221
|
* Everything a wrong value could hide from: a result value split ambiguity, an
|
|
3143
3222
|
* undefined abnormal flag or result status, an unparseable reference range, absent
|
|
3144
3223
|
* units, a mis-attached comment, a partial timestamp, a query-vs-result ambiguity,
|
|
3145
|
-
* a bad frame checksum / sequence gap / unterminated /
|
|
3146
|
-
* ambiguous transport, an unexpected protocol event, or a
|
|
3147
|
-
* forbidden.
|
|
3224
|
+
* an unrecognized record type, a bad frame checksum / sequence gap / unterminated /
|
|
3225
|
+
* oversize frame, an ambiguous transport, an unexpected protocol event, or a
|
|
3226
|
+
* rejected frame: is forbidden.
|
|
3227
|
+
*
|
|
3228
|
+
* **What this file cannot do for you.** Part 2 is a review obligation, not a
|
|
3229
|
+
* mechanical one. There is no honest automatic check for "nothing load-bearing
|
|
3230
|
+
* reads this", because deciding whether a future reader of record structure has put
|
|
3231
|
+
* an allow-listed code back in the path takes reading the reader. What is
|
|
3232
|
+
* mechanical is the direction of the failure: default-deny means a *new* code is
|
|
3233
|
+
* refused until argued in, so the residual risk is confined to the small, explicit
|
|
3234
|
+
* list below. Re-derive that list whenever something new starts reading record
|
|
3235
|
+
* structure.
|
|
3148
3236
|
*/
|
|
3149
3237
|
|
|
3150
3238
|
/**
|
|
3151
|
-
* The **only** warning codes a profile may list in its `tolerate` set: benign
|
|
3152
|
-
* structural
|
|
3153
|
-
* extracted value
|
|
3154
|
-
*
|
|
3155
|
-
*
|
|
3239
|
+
* The **only** warning codes a profile may list in its `tolerate` set: benign
|
|
3240
|
+
* structural or syntactic vendor noise that cannot alter, drop, or fabricate an
|
|
3241
|
+
* extracted value, **and** whose reported condition nothing else in this package
|
|
3242
|
+
* reads: not to decide where one message ends and the next begins, and not to
|
|
3243
|
+
* decide what kind of message it is. Frozen so it cannot be mutated at runtime to
|
|
3244
|
+
* smuggle a code in. Adding to this set is a deliberate, reviewable act, and an
|
|
3245
|
+
* addition has to satisfy both halves of that test, not just the first.
|
|
3246
|
+
*
|
|
3247
|
+
* `ASTM_RECORD_UNKNOWN_TYPE` is **not** on this list, and used to be. An
|
|
3248
|
+
* unrecognized record type may be a header the reader did not recognize as one, in
|
|
3249
|
+
* which case two messages have been read as one, or a `Q` it did not recognize, in
|
|
3250
|
+
* which case a host-query request reads as a result set. Tolerating it can quiet
|
|
3251
|
+
* the only report either happened.
|
|
3156
3252
|
*
|
|
3157
3253
|
* @example
|
|
3158
3254
|
* ```ts
|
|
3159
3255
|
* import { TOLERABLE_CODES } from "@cosyte/astm";
|
|
3160
3256
|
* TOLERABLE_CODES.has("ASTM_UNKNOWN_ESCAPE_SEQUENCE"); // true
|
|
3257
|
+
* TOLERABLE_CODES.has("ASTM_RECORD_UNKNOWN_TYPE"); // false
|
|
3161
3258
|
* TOLERABLE_CODES.has("ASTM_FRAME_BAD_CHECKSUM"); // false
|
|
3162
3259
|
* ```
|
|
3163
3260
|
*/
|
|
@@ -3182,6 +3279,7 @@ declare const ALL_ASTM_WARNING_CODES: ReadonlySet<AnyAstmWarningCode>;
|
|
|
3182
3279
|
* ```ts
|
|
3183
3280
|
* import { SAFETY_CRITICAL_CODES } from "@cosyte/astm";
|
|
3184
3281
|
* SAFETY_CRITICAL_CODES.has("ASTM_RECORD_UNDEFINED_RESULT_STATUS"); // true
|
|
3282
|
+
* SAFETY_CRITICAL_CODES.has("ASTM_RECORD_UNKNOWN_TYPE"); // true
|
|
3185
3283
|
* SAFETY_CRITICAL_CODES.has("ASTM_FRAME_BAD_CHECKSUM"); // true
|
|
3186
3284
|
* ```
|
|
3187
3285
|
*/
|
|
@@ -3199,6 +3297,7 @@ declare const SAFETY_CRITICAL_CODES: ReadonlySet<AnyAstmWarningCode>;
|
|
|
3199
3297
|
* import { isSafetyCriticalCode } from "@cosyte/astm";
|
|
3200
3298
|
* isSafetyCriticalCode("ASTM_UNKNOWN_ESCAPE_SEQUENCE"); // false (tolerable)
|
|
3201
3299
|
* isSafetyCriticalCode("ASTM_RECORD_AMBIGUOUS_VALUE_SPLIT"); // true
|
|
3300
|
+
* isSafetyCriticalCode("ASTM_RECORD_UNKNOWN_TYPE"); // true (it can mean a lost message boundary)
|
|
3202
3301
|
* ```
|
|
3203
3302
|
*/
|
|
3204
3303
|
declare function isSafetyCriticalCode(code: string): boolean;
|
|
@@ -3255,8 +3354,11 @@ declare const defaultProfile: AstmProfile;
|
|
|
3255
3354
|
* only re-badges the *warning*: no value, flag, status, or identifier is altered
|
|
3256
3355
|
* or dropped. And by the safety gate in `defineAstmProfile`, it **cannot** tolerate
|
|
3257
3356
|
* a result value / flag / status / range / units, a patient or comment context, a
|
|
3258
|
-
* message-kind ambiguity, or any frame / LTP integrity
|
|
3259
|
-
* surface, profile or not.
|
|
3357
|
+
* message-kind ambiguity, an unrecognized record type, or any frame / LTP integrity
|
|
3358
|
+
* warning: those always surface, profile or not. The unrecognized-record-type case
|
|
3359
|
+
* matters because message grouping reads a record's type letter, so a letter the
|
|
3360
|
+
* reader does not recognize may be a header, and a header read as something else
|
|
3361
|
+
* joins two messages into one.
|
|
3260
3362
|
*
|
|
3261
3363
|
* **Honesty.** This is a *reference-corpus* profile, not a claim about any named
|
|
3262
3364
|
* analyzer vendor. No per-vendor ("cobas does X") behaviour is asserted; a named
|