@gefyra/diffyr6-cli 1.1.3 → 1.2.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 +24 -3
- package/config/default-rules.json +50 -6
- package/config/searchparameters-r4-not-in-r6.json +1203 -0
- package/package.json +1 -1
- package/src/compare-searchparameters.js +328 -0
- package/src/config.js +23 -3
- package/src/index.js +137 -20
- package/src/rules-engine.js +111 -1
package/package.json
CHANGED
|
@@ -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
|
+
}
|
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.
|
|
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(
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { generateFshFromPackage } from './generate-fsh.js';
|
|
|
8
8
|
import { upgradeSushiToR6 } from './upgrade-sushi.js';
|
|
9
9
|
import { compareProfiles } from './compare-profiles.js';
|
|
10
10
|
import { compareTerminology, hasSnapshots, runSushiWithSnapshots } from './compare-terminology.js';
|
|
11
|
+
import { compareSearchParameters } from './compare-searchparameters.js';
|
|
11
12
|
import { findRemovedResources } from './utils/removed-resources.js';
|
|
12
13
|
import { createZip } from './utils/zip.js';
|
|
13
14
|
import { checkForUpdates } from './utils/update-check.js';
|
|
@@ -38,42 +39,45 @@ export async function runMigration(config) {
|
|
|
38
39
|
outputDir,
|
|
39
40
|
steps: [],
|
|
40
41
|
};
|
|
42
|
+
|
|
43
|
+
const totalSteps = 7;
|
|
41
44
|
|
|
42
45
|
// Step 1: GoFSH (if enabled and not already done)
|
|
43
46
|
if (config.enableGoFSH) {
|
|
44
47
|
const shouldRunGoFSH = await checkShouldRunGoFSH(resourcesDir);
|
|
45
48
|
if (shouldRunGoFSH) {
|
|
46
|
-
console.log(
|
|
49
|
+
console.log(`\n[1/${totalSteps}] Downloading package and generating FSH...`);
|
|
47
50
|
await runGoFSH(context);
|
|
48
51
|
context.steps.push('gofsh');
|
|
49
52
|
} else {
|
|
50
|
-
console.log(
|
|
53
|
+
console.log(`\n[1/${totalSteps}] GoFSH - SKIPPED (Resources directory with sushi-config.yaml already exists)`);
|
|
51
54
|
}
|
|
52
55
|
} else {
|
|
53
|
-
console.log(
|
|
56
|
+
console.log(`\n[1/${totalSteps}] GoFSH - DISABLED in config`);
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
// Step 2: Upgrade to R6
|
|
57
60
|
const shouldRunUpgrade = await checkShouldRunUpgrade(resourcesR6Dir);
|
|
58
61
|
if (shouldRunUpgrade) {
|
|
59
|
-
console.log(
|
|
62
|
+
console.log(`\n[2/${totalSteps}] Upgrading to R6...`);
|
|
60
63
|
await runUpgradeToR6(context);
|
|
61
64
|
context.steps.push('upgrade');
|
|
62
65
|
} else {
|
|
63
|
-
console.log(
|
|
66
|
+
console.log(`\n[2/${totalSteps}] Upgrade - SKIPPED (ResourcesR6 directory with sushi-config.yaml already exists)`);
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
// Step 3: Build snapshots for both R4 and R6
|
|
67
|
-
console.log(
|
|
70
|
+
console.log(`\n[3/${totalSteps}] Building snapshots with SUSHI...`);
|
|
68
71
|
await runSnapshotBuild(context);
|
|
69
72
|
context.steps.push('snapshots');
|
|
70
73
|
|
|
71
74
|
// Step 4: Compare profiles
|
|
72
|
-
console.log(
|
|
75
|
+
console.log(`\n[4/${totalSteps}] Comparing R4 vs R6 profiles...`);
|
|
73
76
|
const compareResults = await runProfileComparison(context);
|
|
74
77
|
context.steps.push('compare');
|
|
75
78
|
|
|
76
|
-
// Step 5:
|
|
79
|
+
// Step 5: Generate migration report
|
|
80
|
+
console.log(`\n[5/${totalSteps}] Generating migration report...`);
|
|
77
81
|
const removedResources = await findRemovedResources(resourcesDir);
|
|
78
82
|
const report = await generateReport(context, compareResults, removedResources);
|
|
79
83
|
context.steps.push('report');
|
|
@@ -81,9 +85,9 @@ export async function runMigration(config) {
|
|
|
81
85
|
// Step 6: Compare terminology bindings
|
|
82
86
|
let terminologyReport = null;
|
|
83
87
|
if (config.skipTerminologyReport) {
|
|
84
|
-
console.log(
|
|
88
|
+
console.log(`\n[6/${totalSteps}] Terminology comparison - SKIPPED (skipTerminologyReport is enabled)`);
|
|
85
89
|
} else {
|
|
86
|
-
console.log(
|
|
90
|
+
console.log(`\n[6/${totalSteps}] Comparing terminology bindings...`);
|
|
87
91
|
try {
|
|
88
92
|
terminologyReport = await runTerminologyComparison(context);
|
|
89
93
|
if (terminologyReport) {
|
|
@@ -95,10 +99,26 @@ export async function runMigration(config) {
|
|
|
95
99
|
}
|
|
96
100
|
}
|
|
97
101
|
|
|
102
|
+
let searchParameterReport = null;
|
|
103
|
+
if (config.skipSearchParameterReport) {
|
|
104
|
+
console.log(`\n[7/${totalSteps}] SearchParameter report - SKIPPED (skipSearchParameterReport is enabled)`);
|
|
105
|
+
} else {
|
|
106
|
+
console.log(`\n[7/${totalSteps}] Analyzing removed search parameters...`);
|
|
107
|
+
try {
|
|
108
|
+
searchParameterReport = await runSearchParameterComparison(context);
|
|
109
|
+
if (searchParameterReport) {
|
|
110
|
+
context.steps.push('searchParameters');
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.warn(` SearchParameter analysis failed: ${error.message}`);
|
|
114
|
+
console.warn(' Continuing without search parameter report...');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
98
118
|
let exportZipPath = null;
|
|
99
119
|
if (config.exportZip) {
|
|
100
120
|
console.log('\nGenerating export ZIP...');
|
|
101
|
-
exportZipPath = await exportComparisonZip(context, report, terminologyReport);
|
|
121
|
+
exportZipPath = await exportComparisonZip(context, report, terminologyReport, searchParameterReport);
|
|
102
122
|
context.steps.push('exportZip');
|
|
103
123
|
}
|
|
104
124
|
|
|
@@ -106,6 +126,12 @@ export async function runMigration(config) {
|
|
|
106
126
|
console.log(` Report: ${report.path}`);
|
|
107
127
|
console.log(` Total Score: ${report.score}`);
|
|
108
128
|
console.log(` Findings: ${report.findingsCount}`);
|
|
129
|
+
if (terminologyReport?.path) {
|
|
130
|
+
console.log(` Terminology report: ${terminologyReport.path}`);
|
|
131
|
+
}
|
|
132
|
+
if (searchParameterReport?.path) {
|
|
133
|
+
console.log(` SearchParameter report: ${searchParameterReport.path}`);
|
|
134
|
+
}
|
|
109
135
|
if (exportZipPath) {
|
|
110
136
|
console.log(` Export ZIP: ${exportZipPath}`);
|
|
111
137
|
}
|
|
@@ -239,6 +265,25 @@ async function runTerminologyComparison(context) {
|
|
|
239
265
|
return result;
|
|
240
266
|
}
|
|
241
267
|
|
|
268
|
+
async function runSearchParameterComparison(context) {
|
|
269
|
+
const { resourcesDir, outputDir, config } = context;
|
|
270
|
+
|
|
271
|
+
const options = {
|
|
272
|
+
debug: config.debug || false,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const result = await compareSearchParameters(resourcesDir, outputDir, options);
|
|
276
|
+
|
|
277
|
+
if (result) {
|
|
278
|
+
console.log(` Removed search parameter matches: ${result.matchCount}`);
|
|
279
|
+
console.log(` Affected CapabilityStatements: ${result.affectedCpsCount}`);
|
|
280
|
+
console.log(` Markdown report: ${result.path}`);
|
|
281
|
+
console.log(` JSON report: ${result.jsonPath}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
242
287
|
/**
|
|
243
288
|
* Get list of profiles that need to be compared
|
|
244
289
|
*/
|
|
@@ -341,7 +386,7 @@ async function generateReport(context, compareResults, removedResources = []) {
|
|
|
341
386
|
/**
|
|
342
387
|
* Create a ZIP export with compare HTML files, report, and run config
|
|
343
388
|
*/
|
|
344
|
-
async function exportComparisonZip(context, report, terminologyReport = null) {
|
|
389
|
+
async function exportComparisonZip(context, report, terminologyReport = null, searchParameterReport = null) {
|
|
345
390
|
const { compareDir, outputDir, config } = context;
|
|
346
391
|
const exportFilename = 'diffyr6-publish.zip';
|
|
347
392
|
const exportPath = path.join(outputDir, exportFilename);
|
|
@@ -368,22 +413,43 @@ async function exportComparisonZip(context, report, terminologyReport = null) {
|
|
|
368
413
|
mtime: (await fsp.stat(report.path)).mtime,
|
|
369
414
|
});
|
|
370
415
|
|
|
416
|
+
const terminologyReportForZip = await resolveReportForZip(outputDir, 'terminology-report', terminologyReport);
|
|
417
|
+
const searchParameterReportForZip = await resolveReportForZip(outputDir, 'searchparameter-report', searchParameterReport);
|
|
418
|
+
|
|
371
419
|
// Add terminology report if available
|
|
372
|
-
if (
|
|
373
|
-
const termContent = await fsp.readFile(
|
|
420
|
+
if (terminologyReportForZip?.path) {
|
|
421
|
+
const termContent = await fsp.readFile(terminologyReportForZip.path);
|
|
374
422
|
entries.push({
|
|
375
|
-
name:
|
|
423
|
+
name: terminologyReportForZip.filename,
|
|
376
424
|
data: termContent,
|
|
377
|
-
mtime: (await fsp.stat(
|
|
425
|
+
mtime: (await fsp.stat(terminologyReportForZip.path)).mtime,
|
|
378
426
|
});
|
|
379
427
|
|
|
380
428
|
// Add terminology JSON if available
|
|
381
|
-
if (
|
|
382
|
-
const termJsonContent = await fsp.readFile(
|
|
429
|
+
if (terminologyReportForZip.jsonPath) {
|
|
430
|
+
const termJsonContent = await fsp.readFile(terminologyReportForZip.jsonPath);
|
|
383
431
|
entries.push({
|
|
384
|
-
name:
|
|
432
|
+
name: terminologyReportForZip.jsonFilename,
|
|
385
433
|
data: termJsonContent,
|
|
386
|
-
mtime: (await fsp.stat(
|
|
434
|
+
mtime: (await fsp.stat(terminologyReportForZip.jsonPath)).mtime,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (searchParameterReportForZip?.path) {
|
|
440
|
+
const searchParamContent = await fsp.readFile(searchParameterReportForZip.path);
|
|
441
|
+
entries.push({
|
|
442
|
+
name: searchParameterReportForZip.filename,
|
|
443
|
+
data: searchParamContent,
|
|
444
|
+
mtime: (await fsp.stat(searchParameterReportForZip.path)).mtime,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
if (searchParameterReportForZip.jsonPath) {
|
|
448
|
+
const searchParamJsonContent = await fsp.readFile(searchParameterReportForZip.jsonPath);
|
|
449
|
+
entries.push({
|
|
450
|
+
name: searchParameterReportForZip.jsonFilename,
|
|
451
|
+
data: searchParamJsonContent,
|
|
452
|
+
mtime: (await fsp.stat(searchParameterReportForZip.jsonPath)).mtime,
|
|
387
453
|
});
|
|
388
454
|
}
|
|
389
455
|
}
|
|
@@ -399,6 +465,57 @@ async function exportComparisonZip(context, report, terminologyReport = null) {
|
|
|
399
465
|
return exportPath;
|
|
400
466
|
}
|
|
401
467
|
|
|
468
|
+
async function resolveReportForZip(outputDir, prefix, currentReport = null) {
|
|
469
|
+
if (currentReport?.path && await fileExists(currentReport.path)) {
|
|
470
|
+
return currentReport;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const latestReport = await findLatestExistingReport(outputDir, prefix);
|
|
474
|
+
return latestReport;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function findLatestExistingReport(outputDir, prefix) {
|
|
478
|
+
const exists = await directoryExists(outputDir);
|
|
479
|
+
if (!exists) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const entries = await fsp.readdir(outputDir).catch(() => []);
|
|
484
|
+
const reportFiles = entries.filter(name => new RegExp(`^${escapeRegex(prefix)}-.+\\.(md|json)$`, 'i').test(name));
|
|
485
|
+
if (reportFiles.length === 0) {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const groups = new Map();
|
|
490
|
+
for (const filename of reportFiles) {
|
|
491
|
+
const ext = path.extname(filename).toLowerCase();
|
|
492
|
+
const stem = filename.slice(0, -ext.length);
|
|
493
|
+
const filePath = path.join(outputDir, filename);
|
|
494
|
+
const stat = await fsp.stat(filePath).catch(() => null);
|
|
495
|
+
if (!stat?.isFile()) {
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const existing = groups.get(stem) || { stem, latestMtimeMs: 0 };
|
|
500
|
+
existing.latestMtimeMs = Math.max(existing.latestMtimeMs, stat.mtimeMs);
|
|
501
|
+
if (ext === '.md') {
|
|
502
|
+
existing.path = filePath;
|
|
503
|
+
existing.filename = filename;
|
|
504
|
+
} else if (ext === '.json') {
|
|
505
|
+
existing.jsonPath = filePath;
|
|
506
|
+
existing.jsonFilename = filename;
|
|
507
|
+
}
|
|
508
|
+
groups.set(stem, existing);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const sorted = Array.from(groups.values()).sort((a, b) => b.latestMtimeMs - a.latestMtimeMs);
|
|
512
|
+
return sorted[0] || null;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function escapeRegex(value) {
|
|
516
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
517
|
+
}
|
|
518
|
+
|
|
402
519
|
async function listExportHtmlFiles(compareDir) {
|
|
403
520
|
const exists = await directoryExists(compareDir);
|
|
404
521
|
if (!exists) {
|