@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/config/default-rules.json +3 -3
- package/config/searchparameters-r4-not-in-r6.json +1203 -0
- package/package.json +1 -1
- package/src/compare-searchparameters.js +328 -0
- package/src/compare-terminology.js +122 -18
- package/src/config.js +23 -3
- package/src/index.js +137 -20
- package/src/rules-engine.js +111 -1
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) {
|
package/src/rules-engine.js
CHANGED
|
@@ -565,7 +565,7 @@ function evaluateCondition(condition, row, lookup) {
|
|
|
565
565
|
}
|
|
566
566
|
const cellValue = row.values[columnAlias] || '';
|
|
567
567
|
const expected = resolveExpectedValue(condition, row, lookup);
|
|
568
|
-
const operator = (condition.operator || '')
|
|
568
|
+
const operator = normalizeOperator(condition.operator || '');
|
|
569
569
|
const caseSensitive = Boolean(condition.caseSensitive);
|
|
570
570
|
if (operator === 'equals') {
|
|
571
571
|
return compareEquals(cellValue, expected, caseSensitive);
|
|
@@ -576,9 +576,23 @@ function evaluateCondition(condition, row, lookup) {
|
|
|
576
576
|
if (operator === 'contains') {
|
|
577
577
|
return compareContains(cellValue, expected, caseSensitive);
|
|
578
578
|
}
|
|
579
|
+
if (operator === 'typesubsetof') {
|
|
580
|
+
return typeListIsSubset(cellValue, expected);
|
|
581
|
+
}
|
|
582
|
+
if (operator === '!typesubsetof') {
|
|
583
|
+
return !typeListIsSubset(cellValue, expected);
|
|
584
|
+
}
|
|
579
585
|
return false;
|
|
580
586
|
}
|
|
581
587
|
|
|
588
|
+
function normalizeOperator(operator) {
|
|
589
|
+
const value = String(operator).trim().toLowerCase();
|
|
590
|
+
if (value === 'nottypesubsetof' || value === 'not-typesubsetof') {
|
|
591
|
+
return '!typesubsetof';
|
|
592
|
+
}
|
|
593
|
+
return value;
|
|
594
|
+
}
|
|
595
|
+
|
|
582
596
|
function resolveExpectedValue(condition, row, lookup) {
|
|
583
597
|
if (condition.valueColumn) {
|
|
584
598
|
const alias = resolveColumnAlias(condition.valueColumn, lookup);
|
|
@@ -611,6 +625,102 @@ function compareContains(left, right, caseSensitive) {
|
|
|
611
625
|
return String(left).toLowerCase().includes(String(right).toLowerCase());
|
|
612
626
|
}
|
|
613
627
|
|
|
628
|
+
function normalizeTypeName(name) {
|
|
629
|
+
return String(name)
|
|
630
|
+
.trim()
|
|
631
|
+
.replace(/[- ]?r[46]$/i, '')
|
|
632
|
+
.toLowerCase();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function normalizeReferenceGroup(referenceStr) {
|
|
636
|
+
const match = String(referenceStr)
|
|
637
|
+
.trim()
|
|
638
|
+
.match(/^reference\s*\((.*)\)$/i);
|
|
639
|
+
if (!match) {
|
|
640
|
+
return `reference:${normalizeTypeName(referenceStr)}`;
|
|
641
|
+
}
|
|
642
|
+
const normalizedTargets = match[1]
|
|
643
|
+
.split('|')
|
|
644
|
+
.map((token) => normalizeTypeName(token))
|
|
645
|
+
.filter(Boolean)
|
|
646
|
+
.sort();
|
|
647
|
+
return `reference:${normalizedTargets.join('|')}`;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function parseTypeList(typeStr) {
|
|
651
|
+
const types = new Set();
|
|
652
|
+
const references = [];
|
|
653
|
+
let current = '';
|
|
654
|
+
let depth = 0;
|
|
655
|
+
|
|
656
|
+
const pushCurrent = () => {
|
|
657
|
+
const token = current.trim();
|
|
658
|
+
current = '';
|
|
659
|
+
if (!token) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (token.toLowerCase().startsWith('reference')) {
|
|
663
|
+
references.push(parseReferenceTargets(token));
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const normalized = normalizeTypeName(token);
|
|
667
|
+
if (normalized) {
|
|
668
|
+
types.add(normalized);
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
for (const char of String(typeStr)) {
|
|
673
|
+
if (char === '(') {
|
|
674
|
+
depth += 1;
|
|
675
|
+
current += char;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if (char === ')') {
|
|
679
|
+
depth = Math.max(0, depth - 1);
|
|
680
|
+
current += char;
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
if (char === ',' && depth === 0) {
|
|
684
|
+
pushCurrent();
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
current += char;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
pushCurrent();
|
|
691
|
+
return { types, references };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function parseReferenceTargets(referenceStr) {
|
|
695
|
+
const normalized = normalizeReferenceGroup(referenceStr);
|
|
696
|
+
const [, targetList = ''] = normalized.split(':');
|
|
697
|
+
return new Set(targetList.split('|').filter(Boolean));
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function typeListIsSubset(leftStr, rightStr) {
|
|
701
|
+
const leftTypes = parseTypeList(leftStr);
|
|
702
|
+
const rightTypes = parseTypeList(rightStr);
|
|
703
|
+
for (const type of leftTypes.types) {
|
|
704
|
+
if (!rightTypes.types.has(type)) {
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
for (const leftReference of leftTypes.references) {
|
|
709
|
+
const hasSuperset = rightTypes.references.some((rightReference) => {
|
|
710
|
+
for (const target of leftReference) {
|
|
711
|
+
if (!rightReference.has(target)) {
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return true;
|
|
716
|
+
});
|
|
717
|
+
if (!hasSuperset) {
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
|
|
614
724
|
function renderTemplate(template, variables) {
|
|
615
725
|
const rendered = template.replace(/{{\s*([^}]+)\s*}}/g, (_, key) => {
|
|
616
726
|
const resolved = resolveVariableValue(key.trim(), variables);
|