@glion/lint-profile-segment-order 0.22.0 → 0.24.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 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
- Walks the parsed tree segment-by-segment, feeding each segment name to a runner over the event schema from `@glion/profiles`. Reports one message for the first segment that is not valid at its position, or for a message that ends before the schema is complete. When no `definition` is given, the rule resolves the schema from MSH-9 and MSH-12.
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 = await unified()
30
- .use(hl7v2Parser)
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 at most one order error per message: the first segment the schema does not allow.
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,40 +152,67 @@ ZPD|1|site patient data
151
152
  PV1|1|I|WARD^101^1
152
153
  ```
153
154
 
154
- ### Invalid — unexpected segment
155
+ ### Invalid — missing segment
155
156
 
156
- `PID` appears before `EVN` in an `ADT_A01`:
157
+ An `ORU_R01` order with results and no `OBR`:
157
158
 
158
159
  ```hl7
159
- MSH|^~\&|SENDER|FAC|RECV|RFAC|20250601120000||ADT^A01^ADT_A01|MSG00001|P|2.5
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
- Unexpected segment 'PID'. Expected: EVN, SFT
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
- The offending segment name and the segments valid at that position, sorted, are interpolated.
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 — message ended prematurely
174
+ ### Invalid — more than one missing segment
172
175
 
173
- All segments were consumed but the schema still requires more; an `ADT_A01` needs a `PV1`:
176
+ An `ADT_A01` without its `EVN` and its `PV1`:
174
177
 
175
178
  ```hl7
176
179
  MSH|^~\&|SENDER|FAC|RECV|RFAC|20250601120000||ADT^A01^ADT_A01|MSG00001|P|2.5
177
- EVN|A01|20250601120000
178
180
  PID|1||PATID1234^^^HOSP^MR||DOE^JANE||19800101|F
179
181
  ```
180
182
 
183
+ Reported messages:
184
+
185
+ ```
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
+ ```
189
+
190
+ A segment missing at the end of the message is reported at the end of the message.
191
+
192
+ ### Invalid — unexpected segment
193
+
194
+ `DSC` ends an `ORU_R01`, so no segment may follow it:
195
+
196
+ ```hl7
197
+ MSH|^~\&|LAB|FAC|EMR|RFAC|20250601120000||ORU^R01^ORU_R01|MSG00001|P|2.5
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
203
+ ```
204
+
181
205
  Reported message:
182
206
 
183
207
  ```
184
- Message ended prematurely. Expected: NK1, PD1, PV1, ROL
208
+ Segment `PID` is out of place; the event schema does not allow it at this point.
185
209
  ```
186
210
 
187
- The list is the segments valid after the last one, sorted. Only reported when no other validation error was emitted.
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.
188
216
 
189
217
  ### No schema
190
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
@@ -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,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAKnD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAEnC,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;AAuDD,QAAA,MAAM,qBAAqB,+DAmD1B,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
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**: Reports at most one order error per message: the first segment
18
- * the schema does not allow. A Z-segment the schema does not name is allowed
19
- * anywhere unless `allowZSegments` is `false`.
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
- * The event schema MSH-12 and MSH-9 name: MSH-9.3, or MSH-9.1 and MSH-9.2,
46
- * which the events store resolves through the version's event map.
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,23 +217,30 @@ const hl7v2LintSegmentOrder = lintRule({ origin: "hl7v2-lint:segment-order" }, a
66
217
  });
67
218
  return SKIP;
68
219
  });
69
- const result = runner(schema, segments.map(({ node }) => node.name), { allowZSegments: options?.allowZSegments });
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": break;
72
- case "mismatched": {
73
- const { ancestors, node } = segments[result.index];
74
- file.message(`Unexpected segment '${node.name}'. Expected: ${result.expected.join(", ")}`, {
75
- ancestors,
76
- place: node.position
77
- });
78
- break;
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);
79
243
  }
80
- case "incomplete":
81
- file.message(`Message ended prematurely. Expected: ${result.expected.join(", ")}`, {
82
- ancestors: [tree],
83
- place: tree.position
84
- });
85
- break;
86
244
  }
87
245
  });
88
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 file.message(\n `Unexpected segment '${node.name}'. Expected: ${result.expected.join(\", \")}`,\n { ancestors, place: node.position }\n );\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;AAET,QAAK,QACH,uBAAuB,KAAK,KAAK,eAAe,OAAO,SAAS,KAAK,KAAK,IAC1E;IAAE;IAAW,OAAO,KAAK;IAAU,CACpC;AACD;;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":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glion/lint-profile-segment-order",
3
- "version": "0.22.0",
3
+ "version": "0.24.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.22.0",
39
- "@glion/util-query": "0.22.0",
40
- "@glion/profiles": "0.22.0",
41
- "@glion/util-visit": "0.22.0"
39
+ "@glion/ast": "0.24.0",
40
+ "@glion/profiles": "0.24.0",
41
+ "@glion/util-query": "0.24.0",
42
+ "@glion/util-visit": "0.24.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.22.0",
53
+ "@glion/builder": "0.24.0",
54
+ "@glion/parser": "0.24.0",
52
55
  "@glion/check-readme": "0.18.0",
53
- "@glion/parser": "0.22.0",
54
56
  "@glion/testing": "0.18.0",
55
57
  "@glion/tsconfig": "0.18.0"
56
58
  },