@gefyra/diffyr6-cli 1.1.4 → 1.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gefyra/diffyr6-cli",
3
- "version": "1.1.4",
3
+ "version": "1.2.1",
4
4
  "description": "FHIR R4 to R6 migration pipeline runner with automated profile comparison and rule-based analysis",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -0,0 +1,328 @@
1
+ import fsp from 'fs/promises';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ export async function compareSearchParameters(resourcesDir, outputDir, options = {}) {
9
+ const { debug = false } = options;
10
+
11
+ if (debug) {
12
+ console.log(' Scanning CapabilityStatements for removed search parameters...');
13
+ }
14
+
15
+ const removedParams = await loadRemovedSearchParameters();
16
+ const capabilityStatements = await collectCapabilityStatements(resourcesDir);
17
+ const usedParams = extractUsedSearchParameters(capabilityStatements);
18
+ const matches = findRemovedMatches(usedParams, removedParams);
19
+
20
+ const timestamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+/, '').replace('T', '-');
21
+ const reportFilename = `searchparameter-report-${timestamp}.md`;
22
+ const reportPath = path.join(outputDir, reportFilename);
23
+ const jsonFilename = `searchparameter-report-${timestamp}.json`;
24
+ const jsonPath = path.join(outputDir, jsonFilename);
25
+
26
+ const markdown = generateSearchParameterReport(matches);
27
+ await fsp.writeFile(reportPath, markdown, 'utf8');
28
+
29
+ const affectedCapabilityStatements = new Set(
30
+ matches.map(match => match.capabilityStatement.id || match.capabilityStatement.url || match.capabilityStatement.sourceFile)
31
+ );
32
+ const jsonData = {
33
+ generated: new Date().toISOString(),
34
+ totalRemovedMatches: matches.length,
35
+ affectedCapabilityStatements: affectedCapabilityStatements.size,
36
+ matches,
37
+ };
38
+ await fsp.writeFile(jsonPath, JSON.stringify(jsonData, null, 2), 'utf8');
39
+
40
+ return {
41
+ path: reportPath,
42
+ filename: reportFilename,
43
+ jsonPath,
44
+ jsonFilename,
45
+ matchCount: matches.length,
46
+ affectedCpsCount: affectedCapabilityStatements.size,
47
+ };
48
+ }
49
+
50
+ async function loadRemovedSearchParameters() {
51
+ const configPath = path.resolve(__dirname, '..', 'config', 'searchparameters-r4-not-in-r6.json');
52
+ const content = await fsp.readFile(configPath, 'utf8');
53
+ const data = JSON.parse(content);
54
+ return data.searchParameters || [];
55
+ }
56
+
57
+ async function collectCapabilityStatements(resourcesDir) {
58
+ const files = await collectJsonFiles(resourcesDir);
59
+ const capabilityStatements = [];
60
+ const baseDir = path.resolve(resourcesDir);
61
+
62
+ for (const filePath of files) {
63
+ const raw = await fsp.readFile(filePath, 'utf8').catch(() => '');
64
+ if (!raw) {
65
+ continue;
66
+ }
67
+
68
+ let data;
69
+ try {
70
+ data = JSON.parse(raw);
71
+ } catch {
72
+ continue;
73
+ }
74
+
75
+ if (data?.resourceType !== 'CapabilityStatement') {
76
+ continue;
77
+ }
78
+
79
+ capabilityStatements.push({
80
+ id: data.id || '',
81
+ url: data.url || '',
82
+ name: data.name || data.title || data.id || path.basename(filePath, '.json'),
83
+ sourceFile: path.relative(baseDir, filePath) || path.basename(filePath),
84
+ data,
85
+ });
86
+ }
87
+
88
+ return capabilityStatements;
89
+ }
90
+
91
+ function extractUsedSearchParameters(capabilityStatements) {
92
+ const used = [];
93
+
94
+ for (const capabilityStatement of capabilityStatements) {
95
+ const restEntries = Array.isArray(capabilityStatement.data.rest) ? capabilityStatement.data.rest : [];
96
+
97
+ for (const rest of restEntries) {
98
+ const resources = Array.isArray(rest.resource) ? rest.resource : [];
99
+
100
+ for (const resource of resources) {
101
+ const resourceType = typeof resource.type === 'string' ? resource.type : '';
102
+ const searchParams = Array.isArray(resource.searchParam) ? resource.searchParam : [];
103
+
104
+ for (const searchParam of searchParams) {
105
+ const name = normalizeString(searchParam.name || searchParam.code);
106
+ const definition = normalizeString(searchParam.definition);
107
+
108
+ if (!name && !definition) {
109
+ continue;
110
+ }
111
+
112
+ used.push({
113
+ name,
114
+ definition,
115
+ resourceType,
116
+ capabilityStatementId: capabilityStatement.id,
117
+ capabilityStatementUrl: capabilityStatement.url,
118
+ capabilityStatementName: capabilityStatement.name,
119
+ sourceFile: capabilityStatement.sourceFile,
120
+ });
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ return used;
127
+ }
128
+
129
+ function findRemovedMatches(usedParams, removedParams) {
130
+ const removedByUrl = new Map();
131
+ const removedByBaseAndCode = new Map();
132
+
133
+ for (const removed of removedParams) {
134
+ const url = normalizeString(removed.url);
135
+ if (url) {
136
+ removedByUrl.set(url, removed);
137
+ }
138
+
139
+ const bases = Array.isArray(removed.base) ? removed.base : [];
140
+ for (const base of bases) {
141
+ const key = buildBaseCodeKey(base, removed.code || removed.name);
142
+ if (!key) {
143
+ continue;
144
+ }
145
+ if (!removedByBaseAndCode.has(key)) {
146
+ removedByBaseAndCode.set(key, []);
147
+ }
148
+ removedByBaseAndCode.get(key).push(removed);
149
+ }
150
+ }
151
+
152
+ const seen = new Set();
153
+ const matches = [];
154
+
155
+ for (const used of usedParams) {
156
+ let removed = null;
157
+ let matchedBy = '';
158
+
159
+ if (used.definition && removedByUrl.has(used.definition)) {
160
+ removed = removedByUrl.get(used.definition);
161
+ matchedBy = 'definition';
162
+ } else if (used.resourceType && used.name) {
163
+ const fallbackMatches = removedByBaseAndCode.get(buildBaseCodeKey(used.resourceType, used.name)) || [];
164
+ if (fallbackMatches.length > 0) {
165
+ removed = fallbackMatches[0];
166
+ matchedBy = 'name+base';
167
+ }
168
+ }
169
+
170
+ if (!removed) {
171
+ continue;
172
+ }
173
+
174
+ const matchKey = [
175
+ removed.url || removed.id || removed.code,
176
+ used.capabilityStatementId || used.capabilityStatementUrl || used.sourceFile,
177
+ used.resourceType,
178
+ used.name,
179
+ used.definition,
180
+ ].join('::').toLowerCase();
181
+
182
+ if (seen.has(matchKey)) {
183
+ continue;
184
+ }
185
+ seen.add(matchKey);
186
+
187
+ matches.push({
188
+ removedSearchParameter: {
189
+ id: removed.id || '',
190
+ name: removed.name || '',
191
+ code: removed.code || '',
192
+ url: removed.url || '',
193
+ base: Array.isArray(removed.base) ? removed.base : [],
194
+ type: removed.type || '',
195
+ description: removed.description || '',
196
+ },
197
+ capabilityStatement: {
198
+ id: used.capabilityStatementId || '',
199
+ url: used.capabilityStatementUrl || '',
200
+ name: used.capabilityStatementName || '',
201
+ sourceFile: used.sourceFile,
202
+ },
203
+ resourceType: used.resourceType,
204
+ searchParameter: {
205
+ name: used.name,
206
+ definition: used.definition,
207
+ },
208
+ matchedBy,
209
+ });
210
+ }
211
+
212
+ return matches.sort(compareMatches);
213
+ }
214
+
215
+ function generateSearchParameterReport(matches) {
216
+ const lines = [];
217
+ const generated = new Date().toISOString();
218
+ const affectedCapabilityStatements = new Set(
219
+ matches.map(match => match.capabilityStatement.id || match.capabilityStatement.url || match.capabilityStatement.sourceFile)
220
+ );
221
+
222
+ lines.push('# Search Parameter Report');
223
+ lines.push('');
224
+ lines.push(`**Generated:** ${generated}`);
225
+ lines.push(`**Removed search parameter matches:** ${matches.length}`);
226
+ lines.push(`**Affected CapabilityStatements:** ${affectedCapabilityStatements.size}`);
227
+ lines.push('');
228
+ lines.push('---');
229
+ lines.push('');
230
+
231
+ if (matches.length === 0) {
232
+ lines.push('No CapabilityStatements were found that reference search parameters removed in FHIR R6.');
233
+ lines.push('');
234
+ return lines.join('\n');
235
+ }
236
+
237
+ const grouped = new Map();
238
+ for (const match of matches) {
239
+ const key = match.removedSearchParameter.url || `${match.removedSearchParameter.code}::${match.resourceType}`;
240
+ if (!grouped.has(key)) {
241
+ grouped.set(key, []);
242
+ }
243
+ grouped.get(key).push(match);
244
+ }
245
+
246
+ for (const key of Array.from(grouped.keys()).sort()) {
247
+ const group = grouped.get(key);
248
+ const first = group[0];
249
+ const removed = first.removedSearchParameter;
250
+
251
+ lines.push(`## ${removed.code || removed.name || removed.id}`);
252
+ lines.push('');
253
+ lines.push(`**Resource Type:** ${(removed.base || []).join(', ') || first.resourceType || 'Unknown'}`);
254
+ lines.push(`**Definition:** ${removed.url || 'n/a'}`);
255
+ lines.push(`**Matches:** ${group.length}`);
256
+ if (removed.description) {
257
+ lines.push(`**Description:** ${removed.description}`);
258
+ }
259
+ lines.push('');
260
+ lines.push('Affected CapabilityStatements:');
261
+ lines.push('');
262
+
263
+ for (const match of group) {
264
+ const cpsLabel =
265
+ match.capabilityStatement.name ||
266
+ match.capabilityStatement.id ||
267
+ match.capabilityStatement.url ||
268
+ path.basename(match.capabilityStatement.sourceFile);
269
+ lines.push(`- **${cpsLabel}**`);
270
+ lines.push(` Resource: ${match.resourceType || 'Unknown'}`);
271
+ lines.push(` Parameter: ${match.searchParameter.name || 'n/a'}`);
272
+ lines.push(` Match: ${match.matchedBy}`);
273
+ if (match.searchParameter.definition) {
274
+ lines.push(` Definition: ${match.searchParameter.definition}`);
275
+ }
276
+ lines.push(` Source: ${match.capabilityStatement.sourceFile}`);
277
+ }
278
+
279
+ lines.push('');
280
+ }
281
+
282
+ return lines.join('\n');
283
+ }
284
+
285
+ async function collectJsonFiles(dir) {
286
+ const results = [];
287
+ const entries = await fsp.readdir(dir, { withFileTypes: true }).catch(() => []);
288
+
289
+ for (const entry of entries) {
290
+ const entryPath = path.join(dir, entry.name);
291
+ if (entry.isDirectory()) {
292
+ const nested = await collectJsonFiles(entryPath);
293
+ results.push(...nested);
294
+ continue;
295
+ }
296
+ if (entry.isFile() && entry.name.toLowerCase().endsWith('.json')) {
297
+ results.push(entryPath);
298
+ }
299
+ }
300
+
301
+ return results;
302
+ }
303
+
304
+ function buildBaseCodeKey(base, code) {
305
+ const normalizedBase = normalizeString(base);
306
+ const normalizedCode = normalizeString(code);
307
+ if (!normalizedBase || !normalizedCode) {
308
+ return '';
309
+ }
310
+ return `${normalizedBase}::${normalizedCode}`;
311
+ }
312
+
313
+ function normalizeString(value) {
314
+ return typeof value === 'string' ? value.trim() : '';
315
+ }
316
+
317
+ function compareMatches(a, b) {
318
+ return (
319
+ compareStrings(a.removedSearchParameter.code, b.removedSearchParameter.code) ||
320
+ compareStrings(a.resourceType, b.resourceType) ||
321
+ compareStrings(a.capabilityStatement.name || a.capabilityStatement.id, b.capabilityStatement.name || b.capabilityStatement.id) ||
322
+ compareStrings(a.searchParameter.name, b.searchParameter.name)
323
+ );
324
+ }
325
+
326
+ function compareStrings(a, b) {
327
+ return (a || '').localeCompare(b || '');
328
+ }
@@ -54,10 +54,13 @@ export async function compareTerminology(resourcesDir, resourcesR6Dir, outputDir
54
54
  console.log(` Found ${findings.length} profile(s) with binding differences`);
55
55
 
56
56
  // Identify common bindings across all profiles
57
- const commonBindings = identifyCommonBindings(findings);
57
+ const commonBindings = sortFindings(identifyCommonBindings(findings));
58
58
 
59
59
  // Remove common bindings from individual profiles
60
- const filteredFindings = removeCommonBindingsFromProfiles(findings, commonBindings);
60
+ const filteredFindings = removeCommonBindingsFromProfiles(findings, commonBindings).map(profile => ({
61
+ ...profile,
62
+ findings: sortFindings(profile.findings),
63
+ }));
61
64
 
62
65
  // Generate reports
63
66
  const timestamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+/, '').replace('T', '-');
@@ -280,6 +283,7 @@ async function compareProfileBindings(r4Profile, r6Profile, options = {}) {
280
283
  findings.push({
281
284
  type: 'new-binding',
282
285
  path,
286
+ isMustSupport: isMustSupportElement(r6Elem),
283
287
  r6Binding: r6Elem.binding,
284
288
  });
285
289
  continue;
@@ -297,6 +301,7 @@ async function compareProfileBindings(r4Profile, r6Profile, options = {}) {
297
301
  const finding = {
298
302
  type: 'strength-and-valueset-change',
299
303
  path,
304
+ isMustSupport: isMustSupportElement(r4Elem) || isMustSupportElement(r6Elem),
300
305
  r4Strength: r4Binding.strength,
301
306
  r6Strength: r6Binding.strength,
302
307
  r4ValueSet: r4Binding.valueSet,
@@ -328,6 +333,7 @@ async function compareProfileBindings(r4Profile, r6Profile, options = {}) {
328
333
  findings.push({
329
334
  type: 'strength-change',
330
335
  path,
336
+ isMustSupport: isMustSupportElement(r4Elem) || isMustSupportElement(r6Elem),
331
337
  r4Strength: r4Binding.strength,
332
338
  r6Strength: r6Binding.strength,
333
339
  r4ValueSet: r4Binding.valueSet,
@@ -341,6 +347,7 @@ async function compareProfileBindings(r4Profile, r6Profile, options = {}) {
341
347
  findings.push({
342
348
  type: 'strength-change',
343
349
  path,
350
+ isMustSupport: isMustSupportElement(r4Elem) || isMustSupportElement(r6Elem),
344
351
  r4Strength: r4Binding.strength,
345
352
  r6Strength: r6Binding.strength,
346
353
  r4ValueSet: r4Binding.valueSet,
@@ -351,6 +358,7 @@ async function compareProfileBindings(r4Profile, r6Profile, options = {}) {
351
358
  const finding = {
352
359
  type: 'valueset-change',
353
360
  path,
361
+ isMustSupport: isMustSupportElement(r4Elem) || isMustSupportElement(r6Elem),
354
362
  r4ValueSet: r4Binding.valueSet,
355
363
  r6ValueSet: r6Binding.valueSet,
356
364
  r4Strength: r4Binding.strength,
@@ -392,6 +400,7 @@ async function compareProfileBindings(r4Profile, r6Profile, options = {}) {
392
400
  findings.push({
393
401
  type: 'removed-binding',
394
402
  path,
403
+ isMustSupport: isMustSupportElement(r4Elem),
395
404
  r4Binding: r4Elem.binding,
396
405
  });
397
406
  }
@@ -400,6 +409,10 @@ async function compareProfileBindings(r4Profile, r6Profile, options = {}) {
400
409
  return findings;
401
410
  }
402
411
 
412
+ function isMustSupportElement(element) {
413
+ return element?.mustSupport === true;
414
+ }
415
+
403
416
  /**
404
417
  * Check if valueSet URL has a version (pipe notation)
405
418
  */
@@ -756,6 +769,38 @@ function groupFindingsByType(findings) {
756
769
  return byType;
757
770
  }
758
771
 
772
+ function sortFindings(findings) {
773
+ return [...findings].sort((left, right) => {
774
+ if ((left.isMustSupport === true) !== (right.isMustSupport === true)) {
775
+ return left.isMustSupport === true ? -1 : 1;
776
+ }
777
+ const pathCompare = (left.path || '').localeCompare(right.path || '');
778
+ if (pathCompare !== 0) {
779
+ return pathCompare;
780
+ }
781
+ return (left.type || '').localeCompare(right.type || '');
782
+ });
783
+ }
784
+
785
+ function splitFindingsByMustSupport(findings) {
786
+ const mustSupport = [];
787
+ const others = [];
788
+
789
+ for (const finding of findings) {
790
+ if (finding.isMustSupport === true) {
791
+ mustSupport.push(finding);
792
+ } else {
793
+ others.push(finding);
794
+ }
795
+ }
796
+
797
+ return { mustSupport, others };
798
+ }
799
+
800
+ function appendFindingMetadata(lines, finding) {
801
+ lines.push(`- Must Support: ${finding.isMustSupport === true ? 'yes' : 'no'}`);
802
+ }
803
+
759
804
  /**
760
805
  * Append findings to markdown lines
761
806
  */
@@ -767,6 +812,7 @@ function appendFindingsToMarkdown(lines, byType) {
767
812
 
768
813
  for (const f of byType['strength-and-valueset-change']) {
769
814
  lines.push(`**${f.path}**`);
815
+ appendFindingMetadata(lines, f);
770
816
  lines.push(`- Strength: \`${f.r4Strength}\` → \`${f.r6Strength}\``);
771
817
  lines.push(`- R4 ValueSet: ${f.r4ValueSet || 'none'}`);
772
818
  lines.push(`- R6 ValueSet: ${f.r6ValueSet || 'none'}`);
@@ -818,6 +864,7 @@ function appendFindingsToMarkdown(lines, byType) {
818
864
 
819
865
  for (const f of byType['strength-change']) {
820
866
  lines.push(`**${f.path}**`);
867
+ appendFindingMetadata(lines, f);
821
868
  lines.push(`- Strength: \`${f.r4Strength}\` → \`${f.r6Strength}\``);
822
869
  if (f.r4ValueSet) {
823
870
  lines.push(`- ValueSet (R4): ${f.r4ValueSet}`);
@@ -836,6 +883,7 @@ function appendFindingsToMarkdown(lines, byType) {
836
883
 
837
884
  for (const f of byType['valueset-change']) {
838
885
  lines.push(`**${f.path}**`);
886
+ appendFindingMetadata(lines, f);
839
887
  lines.push(`- R4 ValueSet: ${f.r4ValueSet || 'none'}`);
840
888
  lines.push(`- R6 ValueSet: ${f.r6ValueSet || 'none'}`);
841
889
 
@@ -890,6 +938,7 @@ function appendFindingsToMarkdown(lines, byType) {
890
938
 
891
939
  for (const f of byType['new-binding']) {
892
940
  lines.push(`**${f.path}**`);
941
+ appendFindingMetadata(lines, f);
893
942
  if (f.r6Binding?.valueSet) {
894
943
  lines.push(`- ValueSet: ${f.r6Binding.valueSet}`);
895
944
  }
@@ -907,6 +956,7 @@ function appendFindingsToMarkdown(lines, byType) {
907
956
 
908
957
  for (const f of byType['removed-binding']) {
909
958
  lines.push(`**${f.path}**`);
959
+ appendFindingMetadata(lines, f);
910
960
  if (f.r4Binding?.valueSet) {
911
961
  lines.push(`- ValueSet (R4): ${f.r4Binding.valueSet}`);
912
962
  }
@@ -923,11 +973,24 @@ function appendFindingsToMarkdown(lines, byType) {
923
973
  */
924
974
  function generateTerminologyReport(findings, commonBindings = []) {
925
975
  const lines = [];
976
+ const sortedProfiles = sortProfilesAlphabetically(findings);
977
+ const mustSupportProfiles = sortedProfiles
978
+ .map(profile => ({
979
+ ...profile,
980
+ findings: profile.findings.filter(finding => finding.isMustSupport === true),
981
+ }))
982
+ .filter(profile => profile.findings.length > 0);
983
+ const otherProfiles = sortedProfiles
984
+ .map(profile => ({
985
+ ...profile,
986
+ findings: profile.findings.filter(finding => finding.isMustSupport !== true),
987
+ }))
988
+ .filter(profile => profile.findings.length > 0);
926
989
 
927
990
  lines.push('# Terminology Binding Comparison Report');
928
991
  lines.push('');
929
992
  lines.push(`**Generated:** ${new Date().toISOString()}`);
930
- lines.push(`**Profiles with Differences:** ${findings.length}`);
993
+ lines.push(`**Profiles with Differences:** ${sortedProfiles.length}`);
931
994
  if (commonBindings.length > 0) {
932
995
  lines.push(`**Common Bindings Across All Profiles:** ${commonBindings.length}`);
933
996
  }
@@ -943,34 +1006,75 @@ function generateTerminologyReport(findings, commonBindings = []) {
943
1006
  lines.push('');
944
1007
  lines.push('The following binding changes occur in **all** profiles:');
945
1008
  lines.push('');
946
-
947
- const groupedCommon = groupFindingsByType(commonBindings);
948
- appendFindingsToMarkdown(lines, groupedCommon);
1009
+
1010
+ appendMustSupportSections(lines, commonBindings);
949
1011
 
950
1012
  lines.push('---');
951
1013
  lines.push('');
952
1014
  }
953
1015
 
954
- if (findings.length === 0) {
1016
+ if (sortedProfiles.length === 0) {
955
1017
  lines.push('✅ **No profile-specific binding differences found.**');
956
1018
  lines.push('');
957
1019
  return lines.join('\n');
958
1020
  }
959
-
960
- for (const profile of findings) {
961
- lines.push(`## ${profile.profileName}`);
1021
+
1022
+ if (mustSupportProfiles.length > 0) {
1023
+ lines.push('## Must Support');
962
1024
  lines.push('');
963
- lines.push(`- **R4 URL:** ${profile.r4Url}`);
964
- lines.push(`- **R6 URL:** ${profile.r6Url}`);
965
- lines.push(`- **Differences:** ${profile.findings.length}`);
1025
+ lines.push('The following profiles contain binding differences on Must-Support elements:');
966
1026
  lines.push('');
967
-
968
- const groupedFindings = groupFindingsByType(profile.findings);
969
- appendFindingsToMarkdown(lines, groupedFindings);
970
-
971
- lines.push('---');
1027
+
1028
+ for (const profile of mustSupportProfiles) {
1029
+ appendProfileSection(lines, profile);
1030
+ }
1031
+ }
1032
+
1033
+ if (otherProfiles.length > 0) {
1034
+ lines.push('## Non Must Support');
1035
+ lines.push('');
1036
+ lines.push('The following profiles contain binding differences on elements without Must Support:');
972
1037
  lines.push('');
1038
+
1039
+ for (const profile of otherProfiles) {
1040
+ appendProfileSection(lines, profile);
1041
+ }
973
1042
  }
974
1043
 
975
1044
  return lines.join('\n');
976
1045
  }
1046
+
1047
+ function appendMustSupportSections(lines, findings) {
1048
+ const sortedFindings = sortFindings(findings);
1049
+ const { mustSupport, others } = splitFindingsByMustSupport(sortedFindings);
1050
+
1051
+ if (mustSupport.length > 0) {
1052
+ lines.push('### Must Support Elements');
1053
+ lines.push('');
1054
+ appendFindingsToMarkdown(lines, groupFindingsByType(mustSupport));
1055
+ }
1056
+
1057
+ if (others.length > 0) {
1058
+ lines.push('### Other Elements');
1059
+ lines.push('');
1060
+ appendFindingsToMarkdown(lines, groupFindingsByType(others));
1061
+ }
1062
+ }
1063
+
1064
+ function appendProfileSection(lines, profile) {
1065
+ lines.push(`### ${profile.profileName}`);
1066
+ lines.push('');
1067
+ lines.push(`- **R4 URL:** ${profile.r4Url}`);
1068
+ lines.push(`- **R6 URL:** ${profile.r6Url}`);
1069
+ lines.push(`- **Differences:** ${profile.findings.length}`);
1070
+ lines.push('');
1071
+ appendFindingsToMarkdown(lines, groupFindingsByType(sortFindings(profile.findings)));
1072
+ lines.push('---');
1073
+ lines.push('');
1074
+ }
1075
+
1076
+ function sortProfilesAlphabetically(profiles) {
1077
+ return [...profiles].sort((left, right) =>
1078
+ (left.profileName || '').localeCompare(right.profileName || '')
1079
+ );
1080
+ }
package/src/config.js CHANGED
@@ -6,7 +6,7 @@ import { pathExists } from './utils/fs.js';
6
6
  const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = path.dirname(__filename);
8
8
 
9
- export const CONFIG_VERSION = '1.0.2';
9
+ export const CONFIG_VERSION = '1.0.3';
10
10
 
11
11
  /**
12
12
  * Default configuration values
@@ -26,6 +26,7 @@ export const DEFAULT_CONFIG = {
26
26
  compareMode: 'incremental',
27
27
  exportZip: true,
28
28
  skipTerminologyReport: false,
29
+ skipSearchParameterReport: false,
29
30
  };
30
31
 
31
32
  /**
@@ -68,17 +69,31 @@ function migrateConfig(config) {
68
69
  if (config.skipTerminologyReport === undefined) {
69
70
  config.skipTerminologyReport = false;
70
71
  }
72
+ if (config.skipSearchParameterReport === undefined) {
73
+ config.skipSearchParameterReport = false;
74
+ }
71
75
  return config;
72
76
  }
73
77
 
74
78
  const [major, minor, patch] = config.configVersion.split('.').map(Number);
75
79
 
76
- // Migrate from 1.0.0 or 1.0.1 to 1.0.2
80
+ // Migrate from 1.0.0 or 1.0.1 to 1.0.2/1.0.3
77
81
  if (major === 1 && minor === 0 && (patch === 0 || patch === 1)) {
78
82
  console.log(` Migrating config from ${config.configVersion} to ${CONFIG_VERSION}...`);
79
83
  if (config.skipTerminologyReport === undefined) {
80
84
  config.skipTerminologyReport = false;
81
85
  }
86
+ if (config.skipSearchParameterReport === undefined) {
87
+ config.skipSearchParameterReport = false;
88
+ }
89
+ config.configVersion = CONFIG_VERSION;
90
+ }
91
+
92
+ if (major === 1 && minor === 0 && patch === 2) {
93
+ console.log(` Migrating config from ${config.configVersion} to ${CONFIG_VERSION}...`);
94
+ if (config.skipSearchParameterReport === undefined) {
95
+ config.skipSearchParameterReport = false;
96
+ }
82
97
  config.configVersion = CONFIG_VERSION;
83
98
  }
84
99
 
@@ -142,6 +157,10 @@ function validateConfig(config) {
142
157
  if (typeof config.skipTerminologyReport !== 'boolean') {
143
158
  errors.push('skipTerminologyReport must be a boolean');
144
159
  }
160
+
161
+ if (typeof config.skipSearchParameterReport !== 'boolean') {
162
+ errors.push('skipSearchParameterReport must be a boolean');
163
+ }
145
164
 
146
165
  if (errors.length > 0) {
147
166
  throw new Error(`Invalid configuration:\n${errors.map(e => ` - ${e}`).join('\n')}`);
@@ -166,7 +185,8 @@ export async function createExampleConfig(outputPath) {
166
185
  workdir: null,
167
186
  compareMode: 'incremental',
168
187
  exportZip: true,
169
- skipTerminologyReport: false
188
+ skipTerminologyReport: false,
189
+ skipSearchParameterReport: false
170
190
  };
171
191
 
172
192
  await fsp.writeFile(