@glion/lint-profile-segment-order 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -28
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +188 -31
- package/dist/index.js.map +1 -1
- package/package.json +9 -7
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Lint rule that validates HL7v2 segment order against the event schema defined by
|
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Runs the message's segment IDs through the event schema from `@glion/profiles`. When they do not fit, reads the message as the one the schema accepts with the fewest edits and reports each edit: a segment the schema requires and the message does not have, or a segment the message has and the schema does not allow there. When no `definition` is given, the rule resolves the schema from MSH-9 and MSH-12.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -15,9 +15,10 @@ npm install @glion/lint-profile-segment-order
|
|
|
15
15
|
## Use
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
import { hl7v2Parser } from "@glion/parser";
|
|
19
18
|
import hl7v2LintSegmentOrder from "@glion/lint-profile-segment-order";
|
|
19
|
+
import { parseHL7v2 } from "@glion/parser";
|
|
20
20
|
import { unified } from "unified";
|
|
21
|
+
import { VFile } from "vfile";
|
|
21
22
|
import { reporter } from "vfile-reporter";
|
|
22
23
|
|
|
23
24
|
const message = [
|
|
@@ -26,10 +27,8 @@ const message = [
|
|
|
26
27
|
"PID|1||PATID1234^^^HOSP^MR||DOE^JANE||19800101|F",
|
|
27
28
|
].join("\r");
|
|
28
29
|
|
|
29
|
-
const file =
|
|
30
|
-
|
|
31
|
-
.use(hl7v2LintSegmentOrder)
|
|
32
|
-
.process(message);
|
|
30
|
+
const file = new VFile();
|
|
31
|
+
await unified().use(hl7v2LintSegmentOrder).run(parseHL7v2(message), file);
|
|
33
32
|
|
|
34
33
|
console.error(reporter([file]));
|
|
35
34
|
```
|
|
@@ -124,7 +123,9 @@ declare const hl7v2LintSegmentOrder: Plugin<[SegmentOrderOptions?], Root>;
|
|
|
124
123
|
export default hl7v2LintSegmentOrder;
|
|
125
124
|
```
|
|
126
125
|
|
|
127
|
-
All messages use `ruleId: "segment-order"` and `source: "hl7v2-lint"`. The rule reports
|
|
126
|
+
All messages use `ruleId: "segment-order"` and `source: "hl7v2-lint"`. The rule reports each edit of the repair with the fewest edits, as `repair` in `@glion/profiles` finds it. A missing segment's message names the segment groups it belongs in, and has `place` a point, where the segment would be inserted, `ancestors` the nodes that hold that point, and `expected` the segment ID. A slot the schema fills with any segment (`Hxx`) or any Z-segment (`anyZSegment`) is described in words and has no `expected`. An unexpected segment's message names the segment groups at the point it is read, those of the segment before it, and has `place` the segment's position, `ancestors` the segment and its ancestors, and `actual` the segment ID.
|
|
127
|
+
|
|
128
|
+
The rule reports at most 20 edits for a message, or 21 when the 21st is the out-of-place segment standing where the 20th, a missing one, belongs, then one message with the count of the rest. A message of more than 1,000 segments is not repaired: the rule reports the first segment that does not fit, with `actual` its ID and `expected` the segment IDs valid there, or, when the message ends early, the end of the message with `expected` the segment IDs valid next. Its `note` says why only that one is reported.
|
|
128
129
|
|
|
129
130
|
## What it checks
|
|
130
131
|
|
|
@@ -151,61 +152,67 @@ ZPD|1|site patient data
|
|
|
151
152
|
PV1|1|I|WARD^101^1
|
|
152
153
|
```
|
|
153
154
|
|
|
154
|
-
### Invalid —
|
|
155
|
+
### Invalid — missing segment
|
|
155
156
|
|
|
156
|
-
`
|
|
157
|
+
An `ORU_R01` order with results and no `OBR`:
|
|
157
158
|
|
|
158
159
|
```hl7
|
|
159
|
-
MSH|^~\&|
|
|
160
|
+
MSH|^~\&|LAB|FAC|EMR|RFAC|20250601120000||ORU^R01^ORU_R01|MSG00001|P|2.5
|
|
160
161
|
PID|1||PATID1234^^^HOSP^MR||DOE^JANE||19800101|F
|
|
162
|
+
OBX|1|NM|WBC||5.0
|
|
163
|
+
OBX|2|NM|RBC||4.5
|
|
161
164
|
```
|
|
162
165
|
|
|
163
166
|
Reported message:
|
|
164
167
|
|
|
165
168
|
```
|
|
166
|
-
|
|
169
|
+
Segment `OBR` of segment group `PATIENT_RESULT > ORDER_OBSERVATION` is missing; the event schema requires it at this point.
|
|
167
170
|
```
|
|
168
171
|
|
|
169
|
-
|
|
172
|
+
A missing segment is reported at the start of the segment it comes before, with the IDs of the segment groups it belongs in, outermost first.
|
|
170
173
|
|
|
171
|
-
### Invalid —
|
|
174
|
+
### Invalid — more than one missing segment
|
|
172
175
|
|
|
173
|
-
`
|
|
176
|
+
An `ADT_A01` without its `EVN` and its `PV1`:
|
|
174
177
|
|
|
175
178
|
```hl7
|
|
176
|
-
MSH|^~\&|
|
|
179
|
+
MSH|^~\&|SENDER|FAC|RECV|RFAC|20250601120000||ADT^A01^ADT_A01|MSG00001|P|2.5
|
|
177
180
|
PID|1||PATID1234^^^HOSP^MR||DOE^JANE||19800101|F
|
|
178
|
-
OBR|1|ORD1||CBC
|
|
179
|
-
OBX|1|NM|WBC||5.0
|
|
180
|
-
DSC|1
|
|
181
|
-
PID|2||PATID5678^^^HOSP^MR||DOE^JOHN||19750101|M
|
|
182
181
|
```
|
|
183
182
|
|
|
184
|
-
Reported
|
|
183
|
+
Reported messages:
|
|
185
184
|
|
|
186
185
|
```
|
|
187
|
-
|
|
186
|
+
Segment `EVN` is missing; the event schema requires it at this point.
|
|
187
|
+
Segment `PV1` is missing; the event schema requires it at this point.
|
|
188
188
|
```
|
|
189
189
|
|
|
190
|
-
|
|
190
|
+
A segment missing at the end of the message is reported at the end of the message.
|
|
191
191
|
|
|
192
|
-
### Invalid —
|
|
192
|
+
### Invalid — unexpected segment
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
`DSC` ends an `ORU_R01`, so no segment may follow it:
|
|
195
195
|
|
|
196
196
|
```hl7
|
|
197
|
-
MSH|^~\&|
|
|
198
|
-
EVN|A01|20250601120000
|
|
197
|
+
MSH|^~\&|LAB|FAC|EMR|RFAC|20250601120000||ORU^R01^ORU_R01|MSG00001|P|2.5
|
|
199
198
|
PID|1||PATID1234^^^HOSP^MR||DOE^JANE||19800101|F
|
|
199
|
+
OBR|1|ORD1||CBC
|
|
200
|
+
OBX|1|NM|WBC||5.0
|
|
201
|
+
DSC|1
|
|
202
|
+
PID|2||PATID5678^^^HOSP^MR||DOE^JOHN||19750101|M
|
|
200
203
|
```
|
|
201
204
|
|
|
202
205
|
Reported message:
|
|
203
206
|
|
|
204
207
|
```
|
|
205
|
-
|
|
208
|
+
Segment `PID` is out of place; the event schema does not allow it at this point.
|
|
206
209
|
```
|
|
207
210
|
|
|
208
|
-
|
|
211
|
+
An unexpected segment is reported on itself. A segment in place of a required one is reported as unexpected, and the required one as missing.
|
|
212
|
+
|
|
213
|
+
### Which edits
|
|
214
|
+
|
|
215
|
+
Of the repairs with the fewest edits, the rule reports one with the fewest unexpected segments: in `ORU_R01`, `MSH PID OBR OBX ORC OBX` reports `OBR` missing before the last `OBX`, not `ORC` out of place.
|
|
209
216
|
|
|
210
217
|
### No schema
|
|
211
218
|
|
package/dist/index.d.ts
CHANGED
|
@@ -27,6 +27,52 @@ export interface SegmentOrderOptions {
|
|
|
27
27
|
*/
|
|
28
28
|
allowZSegments?: boolean;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Lint rule that validates HL7v2 segment order against event schema
|
|
32
|
+
* profiles.
|
|
33
|
+
*
|
|
34
|
+
* Verifies each segment appears in the order the event schema defines.
|
|
35
|
+
*
|
|
36
|
+
* **Resolution**: If no `definition` is provided, the rule resolves the
|
|
37
|
+
* schema from MSH-12 (version) and MSH-9.3 (event schema), or MSH-9.1
|
|
38
|
+
* and MSH-9.2 when MSH-9.3 is empty. If the schema is unavailable, the rule
|
|
39
|
+
* reports nothing.
|
|
40
|
+
*
|
|
41
|
+
* **Behavior**: When the segments do not fit the schema, reports each edit of
|
|
42
|
+
* the repair with the fewest: each segment the schema requires and the message
|
|
43
|
+
* does not have, at the point it would be inserted, with `expected` its ID; and
|
|
44
|
+
* each segment the message has and the schema does not allow there, on that
|
|
45
|
+
* segment, with `actual` its ID. Reports at most 20 edits, or 21 when the 21st
|
|
46
|
+
* is the out-of-place segment standing where the 20th, a missing one, belongs,
|
|
47
|
+
* then one message with the count of the rest. A message of more than 1,000
|
|
48
|
+
* segments gets one report: the first segment that does not fit, with
|
|
49
|
+
* `expected` the segment IDs valid there, or the end of the message when it
|
|
50
|
+
* ends early. A Z-segment the schema does not name is allowed anywhere unless
|
|
51
|
+
* `allowZSegments` is `false`.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* // With the schema MSH-9 names:
|
|
56
|
+
* unified().use(hl7v2LintSegmentOrder);
|
|
57
|
+
*
|
|
58
|
+
* // With a schema of your own:
|
|
59
|
+
* unified().use(hl7v2LintSegmentOrder, {
|
|
60
|
+
* definition: {
|
|
61
|
+
* elements: [
|
|
62
|
+
* { name: "MSH", optional: false, repeating: false, type: "segment" },
|
|
63
|
+
* { name: "PID", optional: false, repeating: false, type: "segment" },
|
|
64
|
+
* ],
|
|
65
|
+
* id: "ADT_SITE",
|
|
66
|
+
* },
|
|
67
|
+
* });
|
|
68
|
+
*
|
|
69
|
+
* // With a schema chosen per message, and the one MSH-9 names otherwise:
|
|
70
|
+
* unified().use(hl7v2LintSegmentOrder, {
|
|
71
|
+
* definition: ({ tree }) =>
|
|
72
|
+
* isSiteMessage(tree) ? SITE_STRUCTURE : undefined,
|
|
73
|
+
* });
|
|
74
|
+
* ```;
|
|
75
|
+
*/
|
|
30
76
|
declare const hl7v2LintSegmentOrder: import("unified-lint-rule").Plugin<Root, SegmentOrderOptions>;
|
|
31
77
|
export default hl7v2LintSegmentOrder;
|
|
32
78
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,IAAI,EAAW,MAAM,YAAY,CAAC;AACvD,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,IAAI,EAAW,MAAM,YAAY,CAAC;AACvD,OAAO,KAAK,EACV,WAAW,EAIZ,MAAM,iBAAiB,CAAC;AAMzB,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AA4FnC,uEAAuE;AACvE,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,IAAI,CAAC;IACX,IAAI,EAAE,KAAK,CAAC;CACb;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;;;OAOG;IACH,UAAU,CAAC,EACP,WAAW,GACX,CAAC,CACC,OAAO,EAAE,mBAAmB,KACzB,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAAC,CAAC;IACrE;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AA6KD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,QAAA,MAAM,qBAAqB,+DAmD1B,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,159 @@
|
|
|
1
|
-
import { profiles, runner } from "@glion/profiles";
|
|
1
|
+
import { profiles, repair, runner } from "@glion/profiles";
|
|
2
2
|
import { value } from "@glion/util-query";
|
|
3
3
|
import { SKIP, visit } from "@glion/util-visit";
|
|
4
|
+
import pluralize from "pluralize";
|
|
4
5
|
import { lintRule } from "unified-lint-rule";
|
|
5
6
|
//#region src/index.ts
|
|
6
7
|
/**
|
|
8
|
+
* The most segments a message may have for the rule to report every missing
|
|
9
|
+
* and out-of-place segment. A longer message gets the first segment that does
|
|
10
|
+
* not fit. The repair's memory grows with segments × program size; at the
|
|
11
|
+
* largest bundled program (454 instructions), 1,000 segments take about 4 MB.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_REPAIRED_SEGMENTS = 1e3;
|
|
14
|
+
/**
|
|
15
|
+
* The most edits reported for one message, before a count of the rest; one
|
|
16
|
+
* more when the last is a missing segment and the next the out-of-place
|
|
17
|
+
* segment standing where it belongs.
|
|
18
|
+
*/
|
|
19
|
+
const MAX_REPORTED_EDITS = 20;
|
|
20
|
+
/** The name an event schema gives a slot that any segment fills. */
|
|
21
|
+
const ANY_SEGMENT = "Hxx";
|
|
22
|
+
/** The name an event schema gives a slot that any Z-segment fills. */
|
|
23
|
+
const ANY_Z_SEGMENT = "anyZSegment";
|
|
24
|
+
const segmentIds = new Intl.ListFormat("en", { type: "disjunction" });
|
|
25
|
+
const counts = new Intl.NumberFormat("en");
|
|
26
|
+
/** A segment ID the event schema names, as a message writes it. */
|
|
27
|
+
const written = (id) => {
|
|
28
|
+
switch (id) {
|
|
29
|
+
case ANY_SEGMENT: return "any segment";
|
|
30
|
+
case ANY_Z_SEGMENT: return "any Z-segment";
|
|
31
|
+
default: return `\`${id}\``;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/** The segment IDs a slot takes, without the any-segment slots. */
|
|
35
|
+
const segmentIdsOnly = (ids) => ids.filter((id) => id !== ANY_SEGMENT && id !== ANY_Z_SEGMENT);
|
|
36
|
+
const messages = {
|
|
37
|
+
endsBeforeComplete: (expected) => `The message ends before its event schema is complete; the event schema requires ${segmentIds.format(expected.map(written))} next.`,
|
|
38
|
+
missingAnySegment: "A segment is missing; the event schema requires a segment with any Segment ID at this point.",
|
|
39
|
+
missingAnySegmentInGroup: (groups) => `A segment of segment group \`${groups}\` is missing; the event schema requires a segment with any Segment ID at this point.`,
|
|
40
|
+
missingAnyZSegment: "A Z-segment is missing; the event schema requires a Z-segment at this point.",
|
|
41
|
+
missingAnyZSegmentInGroup: (groups) => `A Z-segment of segment group \`${groups}\` is missing; the event schema requires a Z-segment at this point.`,
|
|
42
|
+
missingSegment: (segmentId) => `Segment \`${segmentId}\` is missing; the event schema requires it at this point.`,
|
|
43
|
+
missingSegmentInGroup: (segmentId, groups) => `Segment \`${segmentId}\` of segment group \`${groups}\` is missing; the event schema requires it at this point.`,
|
|
44
|
+
moreEdits: (count, reported) => `The message has ${pluralize("more missing or out-of-place segment", count, true)}; the rule reports the first ${reported}.`,
|
|
45
|
+
unexpectedEmptySegmentId: "A segment with an empty Segment ID is out of place; the event schema does not allow it at this point.",
|
|
46
|
+
unexpectedEmptySegmentIdInGroup: (groups) => `A segment with an empty Segment ID is out of place; the event schema does not allow it at this point in segment group \`${groups}\`.`,
|
|
47
|
+
unexpectedSegment: (segmentId) => `Segment \`${segmentId}\` is out of place; the event schema does not allow it at this point.`,
|
|
48
|
+
unexpectedSegmentInGroup: (segmentId, groups) => `Segment \`${segmentId}\` is out of place; the event schema does not allow it at this point in segment group \`${groups}\`.`
|
|
49
|
+
};
|
|
50
|
+
const notes = {
|
|
51
|
+
notRepairedEnd: (segmentCount) => `The message has ${counts.format(segmentCount)} segments; above ${counts.format(MAX_REPAIRED_SEGMENTS)}, the rule reports only where the message ends, not every missing or out-of-place segment.`,
|
|
52
|
+
notRepairedMisfit: (segmentCount) => `The message has ${counts.format(segmentCount)} segments; above ${counts.format(MAX_REPAIRED_SEGMENTS)}, the rule reports only the first segment that does not fit, not every missing or out-of-place segment.`,
|
|
53
|
+
unrepairedEnd: "The rule could not list every missing or out-of-place segment in this message, so it reports only where the message ends. This is a bug; please report it.",
|
|
54
|
+
unrepairedMisfit: "The rule could not list every missing or out-of-place segment in this message, so it reports only the first segment that does not fit. This is a bug; please report it."
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* The event schema MSH-12 and MSH-9 name: MSH-9.3, or MSH-9.1 and MSH-9.2,
|
|
58
|
+
* which the events store resolves through the version's event map.
|
|
59
|
+
*/
|
|
60
|
+
const schemaOf = async (tree) => {
|
|
61
|
+
const version = value(tree, "MSH-12.1")?.value;
|
|
62
|
+
if (!version) return;
|
|
63
|
+
const id = value(tree, "MSH-9.3")?.value || `${value(tree, "MSH-9.1")?.value ?? ""}_${value(tree, "MSH-9.2")?.value ?? ""}`;
|
|
64
|
+
return await profiles.events.load(version, id);
|
|
65
|
+
};
|
|
66
|
+
/** The reason for a missing segment, by the slot the schema names. */
|
|
67
|
+
const missingReason = (segment, path) => {
|
|
68
|
+
const groups = path.join(" > ");
|
|
69
|
+
switch (segment) {
|
|
70
|
+
case ANY_SEGMENT: return path.length === 0 ? messages.missingAnySegment : messages.missingAnySegmentInGroup(groups);
|
|
71
|
+
case ANY_Z_SEGMENT: return path.length === 0 ? messages.missingAnyZSegment : messages.missingAnyZSegmentInGroup(groups);
|
|
72
|
+
default: return path.length === 0 ? messages.missingSegment(segment) : messages.missingSegmentInGroup(segment, groups);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
/** The reason for an out-of-place segment, by its ID and where it was read. */
|
|
76
|
+
const unexpectedReason = (segment, path) => {
|
|
77
|
+
const groups = path.join(" > ");
|
|
78
|
+
if (segment === "") return path.length === 0 ? messages.unexpectedEmptySegmentId : messages.unexpectedEmptySegmentIdInGroup(groups);
|
|
79
|
+
return path.length === 0 ? messages.unexpectedSegment(segment) : messages.unexpectedSegmentInGroup(segment, groups);
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* How many edits to report: `MAX_REPORTED_EDITS`, or one more when the last
|
|
83
|
+
* of those is a missing segment and the next is the out-of-place segment
|
|
84
|
+
* standing where it belongs.
|
|
85
|
+
*/
|
|
86
|
+
const reportedCount = (edits) => {
|
|
87
|
+
const last = edits[MAX_REPORTED_EDITS - 1];
|
|
88
|
+
const next = edits[MAX_REPORTED_EDITS];
|
|
89
|
+
return last?.type === "missing" && next?.type === "unexpected" && next.index === last.index ? MAX_REPORTED_EDITS + 1 : MAX_REPORTED_EDITS;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Reports each edit of the repair, up to `reportedCount()`, then a count of
|
|
93
|
+
* the rest.
|
|
94
|
+
*/
|
|
95
|
+
const reportEdits = (file, tree, segments, edits) => {
|
|
96
|
+
const reported = reportedCount(edits);
|
|
97
|
+
for (const edit of edits.slice(0, reported)) switch (edit.type) {
|
|
98
|
+
case "missing": {
|
|
99
|
+
const reason = missingReason(edit.segment, edit.path);
|
|
100
|
+
const next = segments[edit.index];
|
|
101
|
+
const message = next ? file.message(reason, {
|
|
102
|
+
ancestors: next.ancestors.slice(0, -1),
|
|
103
|
+
place: next.node.position?.start
|
|
104
|
+
}) : file.message(reason, {
|
|
105
|
+
ancestors: [tree],
|
|
106
|
+
place: tree.position?.end
|
|
107
|
+
});
|
|
108
|
+
const expected = segmentIdsOnly([edit.segment]);
|
|
109
|
+
if (expected.length > 0) message.expected = expected;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case "unexpected": {
|
|
113
|
+
const at = segments[edit.index];
|
|
114
|
+
if (!at) throw new Error(`repair() reported an unexpected segment at index ${edit.index} of a ${segments.length}-segment message. This is a bug; please report it.`);
|
|
115
|
+
const message = file.message(unexpectedReason(edit.segment, edit.path), {
|
|
116
|
+
ancestors: at.ancestors,
|
|
117
|
+
place: at.node.position
|
|
118
|
+
});
|
|
119
|
+
message.actual = edit.segment;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (edits.length > reported) file.message(messages.moreEdits(edits.length - reported, reported), {
|
|
124
|
+
ancestors: [tree],
|
|
125
|
+
place: tree.position
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
/** Reports the first segment that does not fit, as the runner found it. */
|
|
129
|
+
const reportFirstMisfit = (file, tree, segments, result, note) => {
|
|
130
|
+
switch (result.type) {
|
|
131
|
+
case "mismatched": {
|
|
132
|
+
const at = segments[result.index];
|
|
133
|
+
if (!at) throw new Error(`runner() reported a mismatch at index ${result.index} of a ${segments.length}-segment message. This is a bug; please report it.`);
|
|
134
|
+
const message = file.message(at.node.name === "" ? messages.unexpectedEmptySegmentId : messages.unexpectedSegment(at.node.name), {
|
|
135
|
+
ancestors: at.ancestors,
|
|
136
|
+
place: at.node.position
|
|
137
|
+
});
|
|
138
|
+
message.actual = at.node.name;
|
|
139
|
+
const expected = segmentIdsOnly(result.expected);
|
|
140
|
+
if (expected.length > 0) message.expected = expected;
|
|
141
|
+
message.note = note.misfit;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case "incomplete": {
|
|
145
|
+
const message = file.message(messages.endsBeforeComplete(result.expected), {
|
|
146
|
+
ancestors: [tree],
|
|
147
|
+
place: tree.position?.end
|
|
148
|
+
});
|
|
149
|
+
const expected = segmentIdsOnly(result.expected);
|
|
150
|
+
if (expected.length > 0) message.expected = expected;
|
|
151
|
+
message.note = note.end;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
7
157
|
* Lint rule that validates HL7v2 segment order against event schema
|
|
8
158
|
* profiles.
|
|
9
159
|
*
|
|
@@ -14,9 +164,17 @@ import { lintRule } from "unified-lint-rule";
|
|
|
14
164
|
* and MSH-9.2 when MSH-9.3 is empty. If the schema is unavailable, the rule
|
|
15
165
|
* reports nothing.
|
|
16
166
|
*
|
|
17
|
-
* **Behavior**:
|
|
18
|
-
* the
|
|
19
|
-
*
|
|
167
|
+
* **Behavior**: When the segments do not fit the schema, reports each edit of
|
|
168
|
+
* the repair with the fewest: each segment the schema requires and the message
|
|
169
|
+
* does not have, at the point it would be inserted, with `expected` its ID; and
|
|
170
|
+
* each segment the message has and the schema does not allow there, on that
|
|
171
|
+
* segment, with `actual` its ID. Reports at most 20 edits, or 21 when the 21st
|
|
172
|
+
* is the out-of-place segment standing where the 20th, a missing one, belongs,
|
|
173
|
+
* then one message with the count of the rest. A message of more than 1,000
|
|
174
|
+
* segments gets one report: the first segment that does not fit, with
|
|
175
|
+
* `expected` the segment IDs valid there, or the end of the message when it
|
|
176
|
+
* ends early. A Z-segment the schema does not name is allowed anywhere unless
|
|
177
|
+
* `allowZSegments` is `false`.
|
|
20
178
|
*
|
|
21
179
|
* @example
|
|
22
180
|
* ```typescript
|
|
@@ -41,17 +199,10 @@ import { lintRule } from "unified-lint-rule";
|
|
|
41
199
|
* });
|
|
42
200
|
* ```;
|
|
43
201
|
*/
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const schemaOf = async (tree) => {
|
|
49
|
-
const version = value(tree, "MSH-12.1")?.value;
|
|
50
|
-
if (!version) return;
|
|
51
|
-
const id = value(tree, "MSH-9.3")?.value || `${value(tree, "MSH-9.1")?.value ?? ""}_${value(tree, "MSH-9.2")?.value ?? ""}`;
|
|
52
|
-
return await profiles.events.load(version, id);
|
|
53
|
-
};
|
|
54
|
-
const hl7v2LintSegmentOrder = lintRule({ origin: "hl7v2-lint:segment-order" }, async (tree, file, options) => {
|
|
202
|
+
const hl7v2LintSegmentOrder = lintRule({
|
|
203
|
+
origin: "hl7v2-lint:segment-order",
|
|
204
|
+
url: "https://github.com/rethinkhealth/glion/tree/main/packages/lint-profile-segment-order#readme"
|
|
205
|
+
}, async (tree, file, options) => {
|
|
55
206
|
const definition = options?.definition;
|
|
56
207
|
const schema = (typeof definition === "function" ? await definition({
|
|
57
208
|
file,
|
|
@@ -66,24 +217,30 @@ const hl7v2LintSegmentOrder = lintRule({ origin: "hl7v2-lint:segment-order" }, a
|
|
|
66
217
|
});
|
|
67
218
|
return SKIP;
|
|
68
219
|
});
|
|
69
|
-
const
|
|
220
|
+
const input = segments.map(({ node }) => node.name);
|
|
221
|
+
const runOptions = { allowZSegments: options?.allowZSegments };
|
|
222
|
+
const result = runner(schema, input, runOptions);
|
|
70
223
|
switch (result.type) {
|
|
71
|
-
case "matched":
|
|
72
|
-
case "mismatched":
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
224
|
+
case "matched": return;
|
|
225
|
+
case "mismatched":
|
|
226
|
+
case "incomplete": {
|
|
227
|
+
if (segments.length > MAX_REPAIRED_SEGMENTS) {
|
|
228
|
+
reportFirstMisfit(file, tree, segments, result, {
|
|
229
|
+
end: notes.notRepairedEnd(segments.length),
|
|
230
|
+
misfit: notes.notRepairedMisfit(segments.length)
|
|
231
|
+
});
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const edits = repair(schema, input, runOptions);
|
|
235
|
+
if (edits.length === 0) {
|
|
236
|
+
reportFirstMisfit(file, tree, segments, result, {
|
|
237
|
+
end: notes.unrepairedEnd,
|
|
238
|
+
misfit: notes.unrepairedMisfit
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
reportEdits(file, tree, segments, edits);
|
|
80
243
|
}
|
|
81
|
-
case "incomplete":
|
|
82
|
-
file.message(`Message ended prematurely. Expected: ${result.expected.join(", ")}`, {
|
|
83
|
-
ancestors: [tree],
|
|
84
|
-
place: tree.position
|
|
85
|
-
});
|
|
86
|
-
break;
|
|
87
244
|
}
|
|
88
245
|
});
|
|
89
246
|
//#endregion
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Nodes, Root, Segment } from \"@glion/ast\";\nimport type { EventSchema } from \"@glion/profiles\";\nimport { profiles, runner } from \"@glion/profiles\";\nimport { value } from \"@glion/util-query\";\nimport { SKIP, visit } from \"@glion/util-visit\";\nimport { lintRule } from \"unified-lint-rule\";\nimport type { VFile } from \"vfile\";\n\n/** The message a `definition` function chooses an event schema for. */\nexport interface SegmentOrderContext {\n tree: Root;\n file: VFile;\n}\n\n/**\n * Options for the segment order lint rule.\n */\nexport interface SegmentOrderOptions {\n /**\n * The event schema to validate against, or a function that returns the\n * one to use for a message. Default: the schema MSH-9 names.\n *\n * When the function returns `undefined`, the rule uses the schema MSH-9\n * names. Return the same object for every message that uses a schema: a\n * new object is compiled on its first run.\n */\n definition?:\n | EventSchema\n | ((\n context: SegmentOrderContext\n ) => EventSchema | undefined | Promise<EventSchema | undefined>);\n /**\n * Whether a Z-segment (a segment ID that starts with `Z`) that the event\n * schema does not name fits at any position. HL7v2 allows local Z-segments\n * in any message and segment group (v2.5.1 §2.11). When `false`, the rule\n * reports such a segment as unexpected. Default: `true`.\n */\n allowZSegments?: boolean;\n}\n\n/**\n * Lint rule that validates HL7v2 segment order against event schema\n * profiles.\n *\n * Verifies each segment appears in the order the event schema defines.\n *\n * **Resolution**: If no `definition` is provided, the rule resolves the\n * schema from MSH-12 (version) and MSH-9.3 (event schema), or MSH-9.1\n * and MSH-9.2 when MSH-9.3 is empty. If the schema is unavailable, the rule\n * reports nothing.\n *\n * **Behavior**: Reports at most one order error per message: the first segment\n * the schema does not allow. A Z-segment the schema does not name is allowed\n * anywhere unless `allowZSegments` is `false`.\n *\n * @example\n * ```typescript\n * // With the schema MSH-9 names:\n * unified().use(hl7v2LintSegmentOrder);\n *\n * // With a schema of your own:\n * unified().use(hl7v2LintSegmentOrder, {\n * definition: {\n * elements: [\n * { name: \"MSH\", optional: false, repeating: false, type: \"segment\" },\n * { name: \"PID\", optional: false, repeating: false, type: \"segment\" },\n * ],\n * id: \"ADT_SITE\",\n * },\n * });\n *\n * // With a schema chosen per message, and the one MSH-9 names otherwise:\n * unified().use(hl7v2LintSegmentOrder, {\n * definition: ({ tree }) =>\n * isSiteMessage(tree) ? SITE_STRUCTURE : undefined,\n * });\n * ```;\n */\n/**\n * The event schema MSH-12 and MSH-9 name: MSH-9.3, or MSH-9.1 and MSH-9.2,\n * which the events store resolves through the version's event map.\n */\nconst schemaOf = async (tree: Root): Promise<EventSchema | undefined> => {\n const version = value(tree, \"MSH-12.1\")?.value;\n if (!version) {\n return;\n }\n const id =\n value(tree, \"MSH-9.3\")?.value ||\n `${value(tree, \"MSH-9.1\")?.value ?? \"\"}_${value(tree, \"MSH-9.2\")?.value ?? \"\"}`;\n return await profiles.events.load(version, id);\n};\n\nconst hl7v2LintSegmentOrder = lintRule<Root, SegmentOrderOptions>(\n {\n origin: \"hl7v2-lint:segment-order\",\n },\n async (tree, file, options) => {\n const definition = options?.definition;\n const chosen =\n typeof definition === \"function\"\n ? await definition({ file, tree })\n : definition;\n const schema = chosen ?? (await schemaOf(tree));\n\n if (!schema) {\n return;\n }\n\n const segments: { node: Segment; ancestors: Nodes[] }[] = [];\n visit(tree, \"segment\", (node, parents) => {\n segments.push({ ancestors: [...parents, node], node });\n return SKIP;\n });\n\n const result = runner(\n schema,\n segments.map(({ node }) => node.name),\n { allowZSegments: options?.allowZSegments }\n );\n\n switch (result.type) {\n case \"matched\": {\n break;\n }\n case \"mismatched\": {\n const { ancestors, node } = segments[\n result.index\n ] as (typeof segments)[number];\n const reason =\n result.expected.length > 0\n ? `Unexpected segment '${node.name}'. Expected: ${result.expected.join(\", \")}`\n : `Unexpected segment '${node.name}'`;\n file.message(reason, { ancestors, place: node.position });\n break;\n }\n case \"incomplete\": {\n file.message(\n `Message ended prematurely. Expected: ${result.expected.join(\", \")}`,\n { ancestors: [tree], place: tree.position }\n );\n break;\n }\n }\n }\n);\n\nexport default hl7v2LintSegmentOrder;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFA,MAAM,WAAW,OAAO,SAAiD;CACvE,MAAM,UAAU,MAAM,MAAM,WAAW,EAAE;AACzC,KAAI,CAAC,QACH;CAEF,MAAM,KACJ,MAAM,MAAM,UAAU,EAAE,SACxB,GAAG,MAAM,MAAM,UAAU,EAAE,SAAS,GAAG,GAAG,MAAM,MAAM,UAAU,EAAE,SAAS;AAC7E,QAAO,MAAM,SAAS,OAAO,KAAK,SAAS,GAAG;;AAGhD,MAAM,wBAAwB,SAC5B,EACE,QAAQ,4BACT,EACD,OAAO,MAAM,MAAM,YAAY;CAC7B,MAAM,aAAa,SAAS;CAK5B,MAAM,UAHJ,OAAO,eAAe,aAClB,MAAM,WAAW;EAAE;EAAM;EAAM,CAAC,GAChC,eACoB,MAAM,SAAS,KAAK;AAE9C,KAAI,CAAC,OACH;CAGF,MAAM,WAAoD,EAAE;AAC5D,OAAM,MAAM,YAAY,MAAM,YAAY;AACxC,WAAS,KAAK;GAAE,WAAW,CAAC,GAAG,SAAS,KAAK;GAAE;GAAM,CAAC;AACtD,SAAO;GACP;CAEF,MAAM,SAAS,OACb,QACA,SAAS,KAAK,EAAE,WAAW,KAAK,KAAK,EACrC,EAAE,gBAAgB,SAAS,gBAAgB,CAC5C;AAED,SAAQ,OAAO,MAAf;EACE,KAAK,UACH;EAEF,KAAK,cAAc;GACjB,MAAM,EAAE,WAAW,SAAS,SAC1B,OAAO;GAET,MAAM,SACJ,OAAO,SAAS,SAAS,IACrB,uBAAuB,KAAK,KAAK,eAAe,OAAO,SAAS,KAAK,KAAK,KAC1E,uBAAuB,KAAK,KAAK;AACvC,QAAK,QAAQ,QAAQ;IAAE;IAAW,OAAO,KAAK;IAAU,CAAC;AACzD;;EAEF,KAAK;AACH,QAAK,QACH,wCAAwC,OAAO,SAAS,KAAK,KAAK,IAClE;IAAE,WAAW,CAAC,KAAK;IAAE,OAAO,KAAK;IAAU,CAC5C;AACD;;EAIP"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Nodes, Root, Segment } from \"@glion/ast\";\nimport type {\n EventSchema,\n RepairEdit,\n RunnerIncomplete,\n RunnerMismatched,\n} from \"@glion/profiles\";\nimport { profiles, repair, runner } from \"@glion/profiles\";\nimport { value } from \"@glion/util-query\";\nimport { SKIP, visit } from \"@glion/util-visit\";\nimport pluralize from \"pluralize\";\nimport { lintRule } from \"unified-lint-rule\";\nimport type { VFile } from \"vfile\";\n\n/**\n * The most segments a message may have for the rule to report every missing\n * and out-of-place segment. A longer message gets the first segment that does\n * not fit. The repair's memory grows with segments × program size; at the\n * largest bundled program (454 instructions), 1,000 segments take about 4 MB.\n */\nconst MAX_REPAIRED_SEGMENTS = 1000;\n\n/**\n * The most edits reported for one message, before a count of the rest; one\n * more when the last is a missing segment and the next the out-of-place\n * segment standing where it belongs.\n */\nconst MAX_REPORTED_EDITS = 20;\n\n/** The name an event schema gives a slot that any segment fills. */\nconst ANY_SEGMENT = \"Hxx\";\n\n/** The name an event schema gives a slot that any Z-segment fills. */\nconst ANY_Z_SEGMENT = \"anyZSegment\";\n\nconst segmentIds = new Intl.ListFormat(\"en\", { type: \"disjunction\" });\n\nconst counts = new Intl.NumberFormat(\"en\");\n\n/** A segment ID the event schema names, as a message writes it. */\nconst written = (id: string): string => {\n switch (id) {\n case ANY_SEGMENT: {\n return \"any segment\";\n }\n case ANY_Z_SEGMENT: {\n return \"any Z-segment\";\n }\n default: {\n return `\\`${id}\\``;\n }\n }\n};\n\n/** The segment IDs a slot takes, without the any-segment slots. */\nconst segmentIdsOnly = (ids: readonly string[]): string[] =>\n ids.filter((id) => id !== ANY_SEGMENT && id !== ANY_Z_SEGMENT);\n\nconst messages = {\n endsBeforeComplete: (expected: readonly string[]) =>\n `The message ends before its event schema is complete; the event schema requires ${segmentIds.format(expected.map(written))} next.`,\n missingAnySegment:\n \"A segment is missing; the event schema requires a segment with any Segment ID at this point.\",\n missingAnySegmentInGroup: (groups: string) =>\n `A segment of segment group \\`${groups}\\` is missing; the event schema requires a segment with any Segment ID at this point.`,\n missingAnyZSegment:\n \"A Z-segment is missing; the event schema requires a Z-segment at this point.\",\n missingAnyZSegmentInGroup: (groups: string) =>\n `A Z-segment of segment group \\`${groups}\\` is missing; the event schema requires a Z-segment at this point.`,\n missingSegment: (segmentId: string) =>\n `Segment \\`${segmentId}\\` is missing; the event schema requires it at this point.`,\n missingSegmentInGroup: (segmentId: string, groups: string) =>\n `Segment \\`${segmentId}\\` of segment group \\`${groups}\\` is missing; the event schema requires it at this point.`,\n moreEdits: (count: number, reported: number) =>\n `The message has ${pluralize(\"more missing or out-of-place segment\", count, true)}; the rule reports the first ${reported}.`,\n unexpectedEmptySegmentId:\n \"A segment with an empty Segment ID is out of place; the event schema does not allow it at this point.\",\n unexpectedEmptySegmentIdInGroup: (groups: string) =>\n `A segment with an empty Segment ID is out of place; the event schema does not allow it at this point in segment group \\`${groups}\\`.`,\n unexpectedSegment: (segmentId: string) =>\n `Segment \\`${segmentId}\\` is out of place; the event schema does not allow it at this point.`,\n unexpectedSegmentInGroup: (segmentId: string, groups: string) =>\n `Segment \\`${segmentId}\\` is out of place; the event schema does not allow it at this point in segment group \\`${groups}\\`.`,\n} as const;\n\nconst notes = {\n notRepairedEnd: (segmentCount: number) =>\n `The message has ${counts.format(segmentCount)} segments; above ${counts.format(MAX_REPAIRED_SEGMENTS)}, the rule reports only where the message ends, not every missing or out-of-place segment.`,\n notRepairedMisfit: (segmentCount: number) =>\n `The message has ${counts.format(segmentCount)} segments; above ${counts.format(MAX_REPAIRED_SEGMENTS)}, the rule reports only the first segment that does not fit, not every missing or out-of-place segment.`,\n unrepairedEnd:\n \"The rule could not list every missing or out-of-place segment in this message, so it reports only where the message ends. This is a bug; please report it.\",\n unrepairedMisfit:\n \"The rule could not list every missing or out-of-place segment in this message, so it reports only the first segment that does not fit. This is a bug; please report it.\",\n} as const;\n\n/** The notes a report of the first segment that does not fit carries. */\ninterface MisfitNotes {\n /** When a segment does not fit. */\n misfit: string;\n /** When the message ends before its event schema is complete. */\n end: string;\n}\n\n/** The message a `definition` function chooses an event schema for. */\nexport interface SegmentOrderContext {\n tree: Root;\n file: VFile;\n}\n\n/**\n * Options for the segment order lint rule.\n */\nexport interface SegmentOrderOptions {\n /**\n * The event schema to validate against, or a function that returns the\n * one to use for a message. Default: the schema MSH-9 names.\n *\n * When the function returns `undefined`, the rule uses the schema MSH-9\n * names. Return the same object for every message that uses a schema: a\n * new object is compiled on its first run.\n */\n definition?:\n | EventSchema\n | ((\n context: SegmentOrderContext\n ) => EventSchema | undefined | Promise<EventSchema | undefined>);\n /**\n * Whether a Z-segment (a segment ID that starts with `Z`) that the event\n * schema does not name fits at any position. HL7v2 allows local Z-segments\n * in any message and segment group (v2.5.1 §2.11). When `false`, the rule\n * reports such a segment as unexpected. Default: `true`.\n */\n allowZSegments?: boolean;\n}\n\n/**\n * The event schema MSH-12 and MSH-9 name: MSH-9.3, or MSH-9.1 and MSH-9.2,\n * which the events store resolves through the version's event map.\n */\nconst schemaOf = async (tree: Root): Promise<EventSchema | undefined> => {\n const version = value(tree, \"MSH-12.1\")?.value;\n if (!version) {\n return;\n }\n const id =\n value(tree, \"MSH-9.3\")?.value ||\n `${value(tree, \"MSH-9.1\")?.value ?? \"\"}_${value(tree, \"MSH-9.2\")?.value ?? \"\"}`;\n return await profiles.events.load(version, id);\n};\n\n/** A segment of the message, and its ancestors, inclusive. */\ninterface Located {\n node: Segment;\n ancestors: Nodes[];\n}\n\n/** The reason for a missing segment, by the slot the schema names. */\nconst missingReason = (segment: string, path: readonly string[]): string => {\n const groups = path.join(\" > \");\n switch (segment) {\n case ANY_SEGMENT: {\n return path.length === 0\n ? messages.missingAnySegment\n : messages.missingAnySegmentInGroup(groups);\n }\n case ANY_Z_SEGMENT: {\n return path.length === 0\n ? messages.missingAnyZSegment\n : messages.missingAnyZSegmentInGroup(groups);\n }\n default: {\n return path.length === 0\n ? messages.missingSegment(segment)\n : messages.missingSegmentInGroup(segment, groups);\n }\n }\n};\n\n/** The reason for an out-of-place segment, by its ID and where it was read. */\nconst unexpectedReason = (segment: string, path: readonly string[]): string => {\n const groups = path.join(\" > \");\n if (segment === \"\") {\n return path.length === 0\n ? messages.unexpectedEmptySegmentId\n : messages.unexpectedEmptySegmentIdInGroup(groups);\n }\n return path.length === 0\n ? messages.unexpectedSegment(segment)\n : messages.unexpectedSegmentInGroup(segment, groups);\n};\n\n/**\n * How many edits to report: `MAX_REPORTED_EDITS`, or one more when the last\n * of those is a missing segment and the next is the out-of-place segment\n * standing where it belongs.\n */\nconst reportedCount = (edits: readonly RepairEdit[]): number => {\n const last = edits[MAX_REPORTED_EDITS - 1];\n const next = edits[MAX_REPORTED_EDITS];\n const splitsPair =\n last?.type === \"missing\" &&\n next?.type === \"unexpected\" &&\n next.index === last.index;\n return splitsPair ? MAX_REPORTED_EDITS + 1 : MAX_REPORTED_EDITS;\n};\n\n/**\n * Reports each edit of the repair, up to `reportedCount()`, then a count of\n * the rest.\n */\nconst reportEdits = (\n file: VFile,\n tree: Root,\n segments: readonly Located[],\n edits: readonly RepairEdit[]\n): void => {\n const reported = reportedCount(edits);\n for (const edit of edits.slice(0, reported)) {\n switch (edit.type) {\n case \"missing\": {\n const reason = missingReason(edit.segment, edit.path);\n const next = segments[edit.index];\n const message = next\n ? file.message(reason, {\n ancestors: next.ancestors.slice(0, -1),\n place: next.node.position?.start,\n })\n : file.message(reason, {\n ancestors: [tree],\n place: tree.position?.end,\n });\n const expected = segmentIdsOnly([edit.segment]);\n if (expected.length > 0) {\n message.expected = expected;\n }\n break;\n }\n case \"unexpected\": {\n const at = segments[edit.index];\n if (!at) {\n throw new Error(\n `repair() reported an unexpected segment at index ${edit.index} of a ${segments.length}-segment message. This is a bug; please report it.`\n );\n }\n const message = file.message(\n unexpectedReason(edit.segment, edit.path),\n { ancestors: at.ancestors, place: at.node.position }\n );\n message.actual = edit.segment;\n break;\n }\n }\n }\n if (edits.length > reported) {\n file.message(messages.moreEdits(edits.length - reported, reported), {\n ancestors: [tree],\n place: tree.position,\n });\n }\n};\n\n/** Reports the first segment that does not fit, as the runner found it. */\nconst reportFirstMisfit = (\n file: VFile,\n tree: Root,\n segments: readonly Located[],\n result: RunnerMismatched | RunnerIncomplete,\n note: MisfitNotes\n): void => {\n switch (result.type) {\n case \"mismatched\": {\n const at = segments[result.index];\n if (!at) {\n throw new Error(\n `runner() reported a mismatch at index ${result.index} of a ${segments.length}-segment message. This is a bug; please report it.`\n );\n }\n const message = file.message(\n at.node.name === \"\"\n ? messages.unexpectedEmptySegmentId\n : messages.unexpectedSegment(at.node.name),\n { ancestors: at.ancestors, place: at.node.position }\n );\n message.actual = at.node.name;\n const expected = segmentIdsOnly(result.expected);\n if (expected.length > 0) {\n message.expected = expected;\n }\n message.note = note.misfit;\n break;\n }\n case \"incomplete\": {\n const message = file.message(\n messages.endsBeforeComplete(result.expected),\n { ancestors: [tree], place: tree.position?.end }\n );\n const expected = segmentIdsOnly(result.expected);\n if (expected.length > 0) {\n message.expected = expected;\n }\n message.note = note.end;\n break;\n }\n }\n};\n\n/**\n * Lint rule that validates HL7v2 segment order against event schema\n * profiles.\n *\n * Verifies each segment appears in the order the event schema defines.\n *\n * **Resolution**: If no `definition` is provided, the rule resolves the\n * schema from MSH-12 (version) and MSH-9.3 (event schema), or MSH-9.1\n * and MSH-9.2 when MSH-9.3 is empty. If the schema is unavailable, the rule\n * reports nothing.\n *\n * **Behavior**: When the segments do not fit the schema, reports each edit of\n * the repair with the fewest: each segment the schema requires and the message\n * does not have, at the point it would be inserted, with `expected` its ID; and\n * each segment the message has and the schema does not allow there, on that\n * segment, with `actual` its ID. Reports at most 20 edits, or 21 when the 21st\n * is the out-of-place segment standing where the 20th, a missing one, belongs,\n * then one message with the count of the rest. A message of more than 1,000\n * segments gets one report: the first segment that does not fit, with\n * `expected` the segment IDs valid there, or the end of the message when it\n * ends early. A Z-segment the schema does not name is allowed anywhere unless\n * `allowZSegments` is `false`.\n *\n * @example\n * ```typescript\n * // With the schema MSH-9 names:\n * unified().use(hl7v2LintSegmentOrder);\n *\n * // With a schema of your own:\n * unified().use(hl7v2LintSegmentOrder, {\n * definition: {\n * elements: [\n * { name: \"MSH\", optional: false, repeating: false, type: \"segment\" },\n * { name: \"PID\", optional: false, repeating: false, type: \"segment\" },\n * ],\n * id: \"ADT_SITE\",\n * },\n * });\n *\n * // With a schema chosen per message, and the one MSH-9 names otherwise:\n * unified().use(hl7v2LintSegmentOrder, {\n * definition: ({ tree }) =>\n * isSiteMessage(tree) ? SITE_STRUCTURE : undefined,\n * });\n * ```;\n */\nconst hl7v2LintSegmentOrder = lintRule<Root, SegmentOrderOptions>(\n {\n origin: \"hl7v2-lint:segment-order\",\n url: \"https://github.com/rethinkhealth/glion/tree/main/packages/lint-profile-segment-order#readme\",\n },\n async (tree, file, options) => {\n const definition = options?.definition;\n const chosen =\n typeof definition === \"function\"\n ? await definition({ file, tree })\n : definition;\n const schema = chosen ?? (await schemaOf(tree));\n\n if (!schema) {\n return;\n }\n\n const segments: Located[] = [];\n visit(tree, \"segment\", (node, parents) => {\n segments.push({ ancestors: [...parents, node], node });\n return SKIP;\n });\n\n const input = segments.map(({ node }) => node.name);\n const runOptions = { allowZSegments: options?.allowZSegments };\n const result = runner(schema, input, runOptions);\n switch (result.type) {\n case \"matched\": {\n return;\n }\n case \"mismatched\":\n case \"incomplete\": {\n if (segments.length > MAX_REPAIRED_SEGMENTS) {\n reportFirstMisfit(file, tree, segments, result, {\n end: notes.notRepairedEnd(segments.length),\n misfit: notes.notRepairedMisfit(segments.length),\n });\n return;\n }\n const edits = repair(schema, input, runOptions);\n if (edits.length === 0) {\n reportFirstMisfit(file, tree, segments, result, {\n end: notes.unrepairedEnd,\n misfit: notes.unrepairedMisfit,\n });\n return;\n }\n reportEdits(file, tree, segments, edits);\n }\n }\n }\n);\n\nexport default hl7v2LintSegmentOrder;\n"],"mappings":";;;;;;;;;;;;AAoBA,MAAM,wBAAwB;;;;;;AAO9B,MAAM,qBAAqB;;AAG3B,MAAM,cAAc;;AAGpB,MAAM,gBAAgB;AAEtB,MAAM,aAAa,IAAI,KAAK,WAAW,MAAM,EAAE,MAAM,eAAe,CAAC;AAErE,MAAM,SAAS,IAAI,KAAK,aAAa,KAAK;;AAG1C,MAAM,WAAW,OAAuB;AACtC,SAAQ,IAAR;EACE,KAAK,YACH,QAAO;EAET,KAAK,cACH,QAAO;EAET,QACE,QAAO,KAAK,GAAG;;;;AAMrB,MAAM,kBAAkB,QACtB,IAAI,QAAQ,OAAO,OAAO,eAAe,OAAO,cAAc;AAEhE,MAAM,WAAW;CACf,qBAAqB,aACnB,mFAAmF,WAAW,OAAO,SAAS,IAAI,QAAQ,CAAC,CAAC;CAC9H,mBACE;CACF,2BAA2B,WACzB,gCAAgC,OAAO;CACzC,oBACE;CACF,4BAA4B,WAC1B,kCAAkC,OAAO;CAC3C,iBAAiB,cACf,aAAa,UAAU;CACzB,wBAAwB,WAAmB,WACzC,aAAa,UAAU,wBAAwB,OAAO;CACxD,YAAY,OAAe,aACzB,mBAAmB,UAAU,wCAAwC,OAAO,KAAK,CAAC,+BAA+B,SAAS;CAC5H,0BACE;CACF,kCAAkC,WAChC,2HAA2H,OAAO;CACpI,oBAAoB,cAClB,aAAa,UAAU;CACzB,2BAA2B,WAAmB,WAC5C,aAAa,UAAU,0FAA0F,OAAO;CAC3H;AAED,MAAM,QAAQ;CACZ,iBAAiB,iBACf,mBAAmB,OAAO,OAAO,aAAa,CAAC,mBAAmB,OAAO,OAAO,sBAAsB,CAAC;CACzG,oBAAoB,iBAClB,mBAAmB,OAAO,OAAO,aAAa,CAAC,mBAAmB,OAAO,OAAO,sBAAsB,CAAC;CACzG,eACE;CACF,kBACE;CACH;;;;;AA8CD,MAAM,WAAW,OAAO,SAAiD;CACvE,MAAM,UAAU,MAAM,MAAM,WAAW,EAAE;AACzC,KAAI,CAAC,QACH;CAEF,MAAM,KACJ,MAAM,MAAM,UAAU,EAAE,SACxB,GAAG,MAAM,MAAM,UAAU,EAAE,SAAS,GAAG,GAAG,MAAM,MAAM,UAAU,EAAE,SAAS;AAC7E,QAAO,MAAM,SAAS,OAAO,KAAK,SAAS,GAAG;;;AAUhD,MAAM,iBAAiB,SAAiB,SAAoC;CAC1E,MAAM,SAAS,KAAK,KAAK,MAAM;AAC/B,SAAQ,SAAR;EACE,KAAK,YACH,QAAO,KAAK,WAAW,IACnB,SAAS,oBACT,SAAS,yBAAyB,OAAO;EAE/C,KAAK,cACH,QAAO,KAAK,WAAW,IACnB,SAAS,qBACT,SAAS,0BAA0B,OAAO;EAEhD,QACE,QAAO,KAAK,WAAW,IACnB,SAAS,eAAe,QAAQ,GAChC,SAAS,sBAAsB,SAAS,OAAO;;;;AAMzD,MAAM,oBAAoB,SAAiB,SAAoC;CAC7E,MAAM,SAAS,KAAK,KAAK,MAAM;AAC/B,KAAI,YAAY,GACd,QAAO,KAAK,WAAW,IACnB,SAAS,2BACT,SAAS,gCAAgC,OAAO;AAEtD,QAAO,KAAK,WAAW,IACnB,SAAS,kBAAkB,QAAQ,GACnC,SAAS,yBAAyB,SAAS,OAAO;;;;;;;AAQxD,MAAM,iBAAiB,UAAyC;CAC9D,MAAM,OAAO,MAAM,qBAAqB;CACxC,MAAM,OAAO,MAAM;AAKnB,QAHE,MAAM,SAAS,aACf,MAAM,SAAS,gBACf,KAAK,UAAU,KAAK,QACF,qBAAqB,IAAI;;;;;;AAO/C,MAAM,eACJ,MACA,MACA,UACA,UACS;CACT,MAAM,WAAW,cAAc,MAAM;AACrC,MAAK,MAAM,QAAQ,MAAM,MAAM,GAAG,SAAS,CACzC,SAAQ,KAAK,MAAb;EACE,KAAK,WAAW;GACd,MAAM,SAAS,cAAc,KAAK,SAAS,KAAK,KAAK;GACrD,MAAM,OAAO,SAAS,KAAK;GAC3B,MAAM,UAAU,OACZ,KAAK,QAAQ,QAAQ;IACnB,WAAW,KAAK,UAAU,MAAM,GAAG,GAAG;IACtC,OAAO,KAAK,KAAK,UAAU;IAC5B,CAAC,GACF,KAAK,QAAQ,QAAQ;IACnB,WAAW,CAAC,KAAK;IACjB,OAAO,KAAK,UAAU;IACvB,CAAC;GACN,MAAM,WAAW,eAAe,CAAC,KAAK,QAAQ,CAAC;AAC/C,OAAI,SAAS,SAAS,EACpB,SAAQ,WAAW;AAErB;;EAEF,KAAK,cAAc;GACjB,MAAM,KAAK,SAAS,KAAK;AACzB,OAAI,CAAC,GACH,OAAM,IAAI,MACR,oDAAoD,KAAK,MAAM,QAAQ,SAAS,OAAO,oDACxF;GAEH,MAAM,UAAU,KAAK,QACnB,iBAAiB,KAAK,SAAS,KAAK,KAAK,EACzC;IAAE,WAAW,GAAG;IAAW,OAAO,GAAG,KAAK;IAAU,CACrD;AACD,WAAQ,SAAS,KAAK;AACtB;;;AAIN,KAAI,MAAM,SAAS,SACjB,MAAK,QAAQ,SAAS,UAAU,MAAM,SAAS,UAAU,SAAS,EAAE;EAClE,WAAW,CAAC,KAAK;EACjB,OAAO,KAAK;EACb,CAAC;;;AAKN,MAAM,qBACJ,MACA,MACA,UACA,QACA,SACS;AACT,SAAQ,OAAO,MAAf;EACE,KAAK,cAAc;GACjB,MAAM,KAAK,SAAS,OAAO;AAC3B,OAAI,CAAC,GACH,OAAM,IAAI,MACR,yCAAyC,OAAO,MAAM,QAAQ,SAAS,OAAO,oDAC/E;GAEH,MAAM,UAAU,KAAK,QACnB,GAAG,KAAK,SAAS,KACb,SAAS,2BACT,SAAS,kBAAkB,GAAG,KAAK,KAAK,EAC5C;IAAE,WAAW,GAAG;IAAW,OAAO,GAAG,KAAK;IAAU,CACrD;AACD,WAAQ,SAAS,GAAG,KAAK;GACzB,MAAM,WAAW,eAAe,OAAO,SAAS;AAChD,OAAI,SAAS,SAAS,EACpB,SAAQ,WAAW;AAErB,WAAQ,OAAO,KAAK;AACpB;;EAEF,KAAK,cAAc;GACjB,MAAM,UAAU,KAAK,QACnB,SAAS,mBAAmB,OAAO,SAAS,EAC5C;IAAE,WAAW,CAAC,KAAK;IAAE,OAAO,KAAK,UAAU;IAAK,CACjD;GACD,MAAM,WAAW,eAAe,OAAO,SAAS;AAChD,OAAI,SAAS,SAAS,EACpB,SAAQ,WAAW;AAErB,WAAQ,OAAO,KAAK;AACpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDN,MAAM,wBAAwB,SAC5B;CACE,QAAQ;CACR,KAAK;CACN,EACD,OAAO,MAAM,MAAM,YAAY;CAC7B,MAAM,aAAa,SAAS;CAK5B,MAAM,UAHJ,OAAO,eAAe,aAClB,MAAM,WAAW;EAAE;EAAM;EAAM,CAAC,GAChC,eACoB,MAAM,SAAS,KAAK;AAE9C,KAAI,CAAC,OACH;CAGF,MAAM,WAAsB,EAAE;AAC9B,OAAM,MAAM,YAAY,MAAM,YAAY;AACxC,WAAS,KAAK;GAAE,WAAW,CAAC,GAAG,SAAS,KAAK;GAAE;GAAM,CAAC;AACtD,SAAO;GACP;CAEF,MAAM,QAAQ,SAAS,KAAK,EAAE,WAAW,KAAK,KAAK;CACnD,MAAM,aAAa,EAAE,gBAAgB,SAAS,gBAAgB;CAC9D,MAAM,SAAS,OAAO,QAAQ,OAAO,WAAW;AAChD,SAAQ,OAAO,MAAf;EACE,KAAK,UACH;EAEF,KAAK;EACL,KAAK,cAAc;AACjB,OAAI,SAAS,SAAS,uBAAuB;AAC3C,sBAAkB,MAAM,MAAM,UAAU,QAAQ;KAC9C,KAAK,MAAM,eAAe,SAAS,OAAO;KAC1C,QAAQ,MAAM,kBAAkB,SAAS,OAAO;KACjD,CAAC;AACF;;GAEF,MAAM,QAAQ,OAAO,QAAQ,OAAO,WAAW;AAC/C,OAAI,MAAM,WAAW,GAAG;AACtB,sBAAkB,MAAM,MAAM,UAAU,QAAQ;KAC9C,KAAK,MAAM;KACX,QAAQ,MAAM;KACf,CAAC;AACF;;AAEF,eAAY,MAAM,MAAM,UAAU,MAAM;;;EAI/C"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glion/lint-profile-segment-order",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Lints the segment order of an HL7v2 message against its message structure",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"health",
|
|
@@ -33,24 +33,26 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"pluralize": "8.0.0",
|
|
36
37
|
"unified-lint-rule": "3.0.1",
|
|
37
38
|
"vfile": "6.0.3",
|
|
38
|
-
"@glion/ast": "0.
|
|
39
|
-
"@glion/
|
|
40
|
-
"@glion/util-
|
|
41
|
-
"@glion/
|
|
39
|
+
"@glion/ast": "0.25.0",
|
|
40
|
+
"@glion/util-query": "0.25.0",
|
|
41
|
+
"@glion/util-visit": "0.25.0",
|
|
42
|
+
"@glion/profiles": "0.25.0"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@types/node": "25.6.0",
|
|
46
|
+
"@types/pluralize": "^0.0.33",
|
|
45
47
|
"@vitest/coverage-v8": "4.1.11",
|
|
46
48
|
"publint": "0.3.18",
|
|
47
49
|
"tsdown": "0.21.10",
|
|
48
50
|
"typescript": "6.0.3",
|
|
49
51
|
"unified": "^11.0.5",
|
|
50
52
|
"vitest": "4.1.11",
|
|
51
|
-
"@glion/builder": "0.
|
|
53
|
+
"@glion/builder": "0.25.0",
|
|
52
54
|
"@glion/check-readme": "0.18.0",
|
|
53
|
-
"@glion/parser": "0.
|
|
55
|
+
"@glion/parser": "0.25.0",
|
|
54
56
|
"@glion/testing": "0.18.0",
|
|
55
57
|
"@glion/tsconfig": "0.18.0"
|
|
56
58
|
},
|