@elisra-devops/docgen-data-provider 1.78.0 → 1.80.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/bin/models/mewp-reporting.d.ts +1 -0
- package/bin/modules/ResultDataProvider.d.ts +2 -1
- package/bin/modules/ResultDataProvider.js +208 -60
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/tests/modules/ResultDataProvider.test.js +6 -6
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -1
- package/package.json +1 -1
- package/src/models/mewp-reporting.ts +1 -0
- package/src/modules/ResultDataProvider.ts +225 -54
- package/src/tests/modules/ResultDataProvider.test.ts +6 -7
|
@@ -472,6 +472,11 @@ export default class ResultDataProvider {
|
|
|
472
472
|
allRequirements,
|
|
473
473
|
scopedRequirementKeys?.size ? scopedRequirementKeys : undefined
|
|
474
474
|
);
|
|
475
|
+
const l2ToLinkedL1BaseKeys = await this.buildMewpL2ToLinkedL1BaseKeys(
|
|
476
|
+
allRequirements,
|
|
477
|
+
projectName,
|
|
478
|
+
testData
|
|
479
|
+
);
|
|
475
480
|
const requirementSapWbsByBaseKey = this.buildRequirementSapWbsByBaseKey(allRequirements);
|
|
476
481
|
const externalBugsByTestCase = await this.loadExternalBugsByTestCase(options?.externalBugsFile);
|
|
477
482
|
const externalL3L4ByBaseKey = await this.loadExternalL3L4ByBaseKey(
|
|
@@ -523,6 +528,10 @@ export default class ResultDataProvider {
|
|
|
523
528
|
sheetName: this.buildMewpCoverageSheetName(planName, testPlanId),
|
|
524
529
|
};
|
|
525
530
|
}
|
|
531
|
+
const externalJoinKeysByL2 = this.buildMewpExternalJoinKeysByL2Requirement(
|
|
532
|
+
requirements,
|
|
533
|
+
l2ToLinkedL1BaseKeys
|
|
534
|
+
);
|
|
526
535
|
|
|
527
536
|
const requirementIndex: MewpRequirementIndex = new Map();
|
|
528
537
|
const observedTestCaseIdsByRequirement = new Map<string, Set<number>>();
|
|
@@ -601,8 +610,16 @@ export default class ResultDataProvider {
|
|
|
601
610
|
const requirementBaseKeys = new Set<string>(
|
|
602
611
|
requirements.map((item) => String(item?.baseKey || '').trim()).filter((item) => !!item)
|
|
603
612
|
);
|
|
613
|
+
const externalJoinKeyUniverse = new Set<string>();
|
|
614
|
+
for (const keySet of externalJoinKeysByL2.values()) {
|
|
615
|
+
for (const key of keySet) {
|
|
616
|
+
if (key) externalJoinKeyUniverse.add(key);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
604
619
|
const externalL3L4BaseKeys = new Set<string>([...externalL3L4ByBaseKey.keys()]);
|
|
605
|
-
const externalL3L4OverlapKeys = [...externalL3L4BaseKeys].filter((key) =>
|
|
620
|
+
const externalL3L4OverlapKeys = [...externalL3L4BaseKeys].filter((key) =>
|
|
621
|
+
externalJoinKeyUniverse.has(key)
|
|
622
|
+
);
|
|
606
623
|
const failedRequirementBaseKeys = new Set<string>();
|
|
607
624
|
const failedTestCaseIds = new Set<number>();
|
|
608
625
|
for (const [requirementBaseKey, byTestCase] of requirementIndex.entries()) {
|
|
@@ -625,13 +642,14 @@ export default class ResultDataProvider {
|
|
|
625
642
|
}
|
|
626
643
|
}
|
|
627
644
|
const externalBugRequirementOverlap = [...externalBugBaseKeys].filter((key) =>
|
|
628
|
-
|
|
645
|
+
externalJoinKeyUniverse.has(key)
|
|
629
646
|
);
|
|
630
647
|
const externalBugFailedRequirementOverlap = [...externalBugBaseKeys].filter((key) =>
|
|
631
|
-
|
|
648
|
+
externalJoinKeyUniverse.has(key)
|
|
632
649
|
);
|
|
633
650
|
logger.info(
|
|
634
651
|
`MEWP coverage join diagnostics: requirementBaseKeys=${requirementBaseKeys.size} ` +
|
|
652
|
+
`externalJoinKeys=${externalJoinKeyUniverse.size} ` +
|
|
635
653
|
`failedRequirementBaseKeys=${failedRequirementBaseKeys.size} failedTestCases=${failedTestCaseIds.size}; ` +
|
|
636
654
|
`externalL3L4BaseKeys=${externalL3L4BaseKeys.size} externalL3L4Overlap=${externalL3L4OverlapKeys.length}; ` +
|
|
637
655
|
`externalBugTestCases=${externalBugTestCaseIds.size} externalBugFailedTestCaseOverlap=${externalBugFailedTestCaseOverlap.length}; ` +
|
|
@@ -640,18 +658,22 @@ export default class ResultDataProvider {
|
|
|
640
658
|
);
|
|
641
659
|
if (externalL3L4BaseKeys.size > 0 && externalL3L4OverlapKeys.length === 0) {
|
|
642
660
|
const sampleReq = [...requirementBaseKeys].slice(0, 5);
|
|
661
|
+
const sampleJoin = [...externalJoinKeyUniverse].slice(0, 5);
|
|
643
662
|
const sampleExt = [...externalL3L4BaseKeys].slice(0, 5);
|
|
644
663
|
logger.warn(
|
|
645
664
|
`MEWP coverage join diagnostics: no L3/L4 key overlap found. ` +
|
|
646
|
-
`sampleRequirementKeys=${sampleReq.join(', ')}
|
|
665
|
+
`sampleRequirementKeys=${sampleReq.join(', ')} sampleJoinKeys=${sampleJoin.join(', ')} ` +
|
|
666
|
+
`sampleExternalL3L4Keys=${sampleExt.join(', ')}`
|
|
647
667
|
);
|
|
648
668
|
}
|
|
649
669
|
if (externalBugBaseKeys.size > 0 && externalBugRequirementOverlap.length === 0) {
|
|
650
670
|
const sampleReq = [...requirementBaseKeys].slice(0, 5);
|
|
671
|
+
const sampleJoin = [...externalJoinKeyUniverse].slice(0, 5);
|
|
651
672
|
const sampleExt = [...externalBugBaseKeys].slice(0, 5);
|
|
652
673
|
logger.warn(
|
|
653
674
|
`MEWP coverage join diagnostics: no bug requirement-key overlap found. ` +
|
|
654
|
-
`sampleRequirementKeys=${sampleReq.join(', ')}
|
|
675
|
+
`sampleRequirementKeys=${sampleReq.join(', ')} sampleJoinKeys=${sampleJoin.join(', ')} ` +
|
|
676
|
+
`sampleExternalBugKeys=${sampleExt.join(', ')}`
|
|
655
677
|
);
|
|
656
678
|
}
|
|
657
679
|
if (externalBugTestCaseIds.size > 0 && externalBugFailedTestCaseOverlap.length === 0) {
|
|
@@ -667,7 +689,8 @@ export default class ResultDataProvider {
|
|
|
667
689
|
observedTestCaseIdsByRequirement,
|
|
668
690
|
linkedRequirementsByTestCase,
|
|
669
691
|
externalL3L4ByBaseKey,
|
|
670
|
-
externalBugsByTestCase
|
|
692
|
+
externalBugsByTestCase,
|
|
693
|
+
externalJoinKeysByL2
|
|
671
694
|
);
|
|
672
695
|
const coverageRowStats = rows.reduce(
|
|
673
696
|
(acc, row) => {
|
|
@@ -757,14 +780,26 @@ export default class ResultDataProvider {
|
|
|
757
780
|
}
|
|
758
781
|
|
|
759
782
|
const validL2BaseKeys = new Set<string>([...requirementFamilies.keys()]);
|
|
783
|
+
const diagnostics = {
|
|
784
|
+
totalTestCases: 0,
|
|
785
|
+
totalParsedSteps: 0,
|
|
786
|
+
totalStepsWithMentions: 0,
|
|
787
|
+
totalMentionedCustomerIds: 0,
|
|
788
|
+
testCasesWithoutMentionedCustomerIds: 0,
|
|
789
|
+
failingRows: 0,
|
|
790
|
+
};
|
|
760
791
|
|
|
761
792
|
for (const testCaseId of [...allTestCaseIds].sort((a, b) => a - b)) {
|
|
793
|
+
diagnostics.totalTestCases += 1;
|
|
762
794
|
const stepsXml = stepsXmlByTestCase.get(testCaseId) || '';
|
|
763
795
|
const parsedSteps =
|
|
764
796
|
stepsXml && String(stepsXml).trim() !== ''
|
|
765
797
|
? await this.testStepParserHelper.parseTestSteps(stepsXml, new Map<number, number>())
|
|
766
798
|
: [];
|
|
799
|
+
const executableSteps = parsedSteps.filter((step) => !step?.isSharedStepTitle);
|
|
800
|
+
diagnostics.totalParsedSteps += executableSteps.length;
|
|
767
801
|
const mentionEntries = this.extractRequirementMentionsFromExpectedSteps(parsedSteps, true);
|
|
802
|
+
diagnostics.totalStepsWithMentions += mentionEntries.length;
|
|
768
803
|
const mentionedL2Only = new Set<string>();
|
|
769
804
|
const mentionedCodeFirstStep = new Map<string, string>();
|
|
770
805
|
const mentionedBaseFirstStep = new Map<string, string>();
|
|
@@ -787,6 +822,10 @@ export default class ResultDataProvider {
|
|
|
787
822
|
}
|
|
788
823
|
}
|
|
789
824
|
}
|
|
825
|
+
diagnostics.totalMentionedCustomerIds += mentionedL2Only.size;
|
|
826
|
+
if (mentionedL2Only.size === 0) {
|
|
827
|
+
diagnostics.testCasesWithoutMentionedCustomerIds += 1;
|
|
828
|
+
}
|
|
790
829
|
|
|
791
830
|
const mentionedBaseKeys = new Set<string>(
|
|
792
831
|
[...mentionedL2Only].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
|
|
@@ -874,6 +913,15 @@ export default class ResultDataProvider {
|
|
|
874
913
|
const linkedButNotMentioned = sortedExtraLinked.join('; ');
|
|
875
914
|
const validationStatus: 'Pass' | 'Fail' =
|
|
876
915
|
mentionedButNotLinked || linkedButNotMentioned ? 'Fail' : 'Pass';
|
|
916
|
+
if (validationStatus === 'Fail') diagnostics.failingRows += 1;
|
|
917
|
+
logger.debug(
|
|
918
|
+
`MEWP internal validation parse diagnostics: ` +
|
|
919
|
+
`testCaseId=${testCaseId} parsedSteps=${executableSteps.length} ` +
|
|
920
|
+
`stepsWithMentions=${mentionEntries.length} customerIdsFound=${mentionedL2Only.size} ` +
|
|
921
|
+
`linkedRequirements=${linkedFullCodes.size} mentionedButNotLinked=${sortedMissingMentioned.length + sortedMissingFamily.length} ` +
|
|
922
|
+
`linkedButNotMentioned=${sortedExtraLinked.length} status=${validationStatus} ` +
|
|
923
|
+
`customerIdSample='${[...mentionedL2Only].slice(0, 5).join(', ')}'`
|
|
924
|
+
);
|
|
877
925
|
|
|
878
926
|
rows.push({
|
|
879
927
|
'Test Case ID': testCaseId,
|
|
@@ -883,6 +931,13 @@ export default class ResultDataProvider {
|
|
|
883
931
|
'Validation Status': validationStatus,
|
|
884
932
|
});
|
|
885
933
|
}
|
|
934
|
+
logger.info(
|
|
935
|
+
`MEWP internal validation summary: testCases=${diagnostics.totalTestCases} ` +
|
|
936
|
+
`parsedSteps=${diagnostics.totalParsedSteps} stepsWithMentions=${diagnostics.totalStepsWithMentions} ` +
|
|
937
|
+
`totalCustomerIdsFound=${diagnostics.totalMentionedCustomerIds} ` +
|
|
938
|
+
`testCasesWithoutCustomerIds=${diagnostics.testCasesWithoutMentionedCustomerIds} ` +
|
|
939
|
+
`failingRows=${diagnostics.failingRows}`
|
|
940
|
+
);
|
|
886
941
|
|
|
887
942
|
return {
|
|
888
943
|
sheetName: this.buildInternalValidationSheetName(planName, testPlanId),
|
|
@@ -999,12 +1054,16 @@ export default class ResultDataProvider {
|
|
|
999
1054
|
}
|
|
1000
1055
|
|
|
1001
1056
|
private createMewpCoverageRow(
|
|
1002
|
-
requirement: Pick<
|
|
1057
|
+
requirement: Pick<
|
|
1058
|
+
MewpL2RequirementFamily,
|
|
1059
|
+
'workItemId' | 'requirementId' | 'title' | 'subSystem' | 'responsibility'
|
|
1060
|
+
>,
|
|
1003
1061
|
runStatus: MewpRunStatus,
|
|
1004
1062
|
bug: MewpCoverageBugCell,
|
|
1005
1063
|
linkedL3L4: MewpCoverageL3L4Cell
|
|
1006
1064
|
): MewpCoverageRow {
|
|
1007
|
-
const
|
|
1065
|
+
const l2ReqIdNumeric = Number(requirement?.workItemId || 0);
|
|
1066
|
+
const l2ReqId = l2ReqIdNumeric > 0 ? String(l2ReqIdNumeric) : '';
|
|
1008
1067
|
const l2ReqTitle = this.toMewpComparableText(requirement.title);
|
|
1009
1068
|
const l2SubSystem = this.toMewpComparableText(requirement.subSystem);
|
|
1010
1069
|
|
|
@@ -1032,7 +1091,21 @@ export default class ResultDataProvider {
|
|
|
1032
1091
|
}
|
|
1033
1092
|
|
|
1034
1093
|
private buildMewpCoverageL3L4Rows(links: MewpL3L4Link[]): MewpCoverageL3L4Cell[] {
|
|
1035
|
-
const
|
|
1094
|
+
const deduped = new Map<string, MewpL3L4Link>();
|
|
1095
|
+
for (const item of links || []) {
|
|
1096
|
+
const level = item?.level === 'L4' ? 'L4' : 'L3';
|
|
1097
|
+
const id = String(item?.id || '').trim();
|
|
1098
|
+
if (!id) continue;
|
|
1099
|
+
const key = `${level}:${id}`;
|
|
1100
|
+
if (!deduped.has(key)) {
|
|
1101
|
+
deduped.set(key, {
|
|
1102
|
+
id,
|
|
1103
|
+
level,
|
|
1104
|
+
title: String(item?.title || '').trim(),
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const sorted = [...deduped.values()].sort((a, b) => {
|
|
1036
1109
|
if (a.level !== b.level) return a.level === 'L3' ? -1 : 1;
|
|
1037
1110
|
return String(a.id || '').localeCompare(String(b.id || ''));
|
|
1038
1111
|
});
|
|
@@ -1050,27 +1123,21 @@ export default class ResultDataProvider {
|
|
|
1050
1123
|
return rows;
|
|
1051
1124
|
}
|
|
1052
1125
|
|
|
1053
|
-
private formatMewpCustomerId(rawValue: string): string {
|
|
1054
|
-
const normalized = this.normalizeMewpRequirementCode(this.toMewpComparableText(rawValue));
|
|
1055
|
-
if (normalized) return normalized;
|
|
1056
|
-
|
|
1057
|
-
const onlyDigits = String(rawValue || '').replace(/\D/g, '');
|
|
1058
|
-
if (onlyDigits) return `SR${onlyDigits}`;
|
|
1059
|
-
return '';
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
1126
|
private buildMewpCoverageRows(
|
|
1063
1127
|
requirements: MewpL2RequirementFamily[],
|
|
1064
1128
|
requirementIndex: MewpRequirementIndex,
|
|
1065
1129
|
observedTestCaseIdsByRequirement: Map<string, Set<number>>,
|
|
1066
1130
|
linkedRequirementsByTestCase: MewpLinkedRequirementsByTestCase,
|
|
1067
1131
|
l3l4ByBaseKey: Map<string, MewpL3L4Link[]>,
|
|
1068
|
-
externalBugsByTestCase: Map<number, MewpBugLink[]
|
|
1132
|
+
externalBugsByTestCase: Map<number, MewpBugLink[]>,
|
|
1133
|
+
externalJoinKeysByL2?: Map<string, Set<string>>
|
|
1069
1134
|
): MewpCoverageRow[] {
|
|
1070
1135
|
const rows: MewpCoverageRow[] = [];
|
|
1071
1136
|
const linkedByRequirement = this.invertBaseRequirementLinks(linkedRequirementsByTestCase);
|
|
1137
|
+
const joinKeysByRequirement = externalJoinKeysByL2 || new Map<string, Set<string>>();
|
|
1072
1138
|
for (const requirement of requirements) {
|
|
1073
1139
|
const key = String(requirement?.baseKey || this.toRequirementKey(requirement.requirementId) || '').trim();
|
|
1140
|
+
const externalJoinKeys = joinKeysByRequirement.get(key) || new Set<string>([key]);
|
|
1074
1141
|
const linkedTestCaseIds = (requirement?.linkedTestCaseIds || []).filter(
|
|
1075
1142
|
(id) => Number.isFinite(id) && Number(id) > 0
|
|
1076
1143
|
);
|
|
@@ -1100,7 +1167,7 @@ export default class ResultDataProvider {
|
|
|
1100
1167
|
const externalBugs = externalBugsByTestCase.get(testCaseId) || [];
|
|
1101
1168
|
for (const bug of externalBugs) {
|
|
1102
1169
|
const bugBaseKey = String(bug?.requirementBaseKey || '').trim();
|
|
1103
|
-
if (bugBaseKey && bugBaseKey
|
|
1170
|
+
if (bugBaseKey && !externalJoinKeys.has(bugBaseKey)) continue;
|
|
1104
1171
|
const bugId = Number(bug?.id || 0);
|
|
1105
1172
|
if (!Number.isFinite(bugId) || bugId <= 0) continue;
|
|
1106
1173
|
aggregatedBugs.set(bugId, {
|
|
@@ -1125,7 +1192,7 @@ export default class ResultDataProvider {
|
|
|
1125
1192
|
runStatus === 'Fail'
|
|
1126
1193
|
? Array.from(aggregatedBugs.values()).sort((a, b) => a.id - b.id)
|
|
1127
1194
|
: [];
|
|
1128
|
-
const l3l4ForRows = [...(l3l4ByBaseKey.get(
|
|
1195
|
+
const l3l4ForRows = [...externalJoinKeys].flatMap((joinKey) => l3l4ByBaseKey.get(joinKey) || []);
|
|
1129
1196
|
|
|
1130
1197
|
const bugRows: MewpCoverageBugCell[] =
|
|
1131
1198
|
bugsForRows.length > 0
|
|
@@ -1790,7 +1857,7 @@ export default class ResultDataProvider {
|
|
|
1790
1857
|
const workItems = await this.fetchWorkItemsByIds(projectName, requirementIds, true);
|
|
1791
1858
|
const requirements = workItems.map((wi: any) => {
|
|
1792
1859
|
const fields = wi?.fields || {};
|
|
1793
|
-
const requirementId = this.extractMewpRequirementIdentifier(fields
|
|
1860
|
+
const requirementId = this.extractMewpRequirementIdentifier(fields);
|
|
1794
1861
|
const areaPath = this.toMewpComparableText(fields?.['System.AreaPath']);
|
|
1795
1862
|
return {
|
|
1796
1863
|
workItemId: Number(wi?.id || 0),
|
|
@@ -1878,6 +1945,7 @@ export default class ResultDataProvider {
|
|
|
1878
1945
|
|
|
1879
1946
|
return [...families.entries()]
|
|
1880
1947
|
.map(([baseKey, family]) => ({
|
|
1948
|
+
workItemId: Number(family?.representative?.workItemId || 0),
|
|
1881
1949
|
requirementId: String(family?.representative?.requirementId || baseKey),
|
|
1882
1950
|
baseKey,
|
|
1883
1951
|
title: String(family?.representative?.title || ''),
|
|
@@ -1888,6 +1956,123 @@ export default class ResultDataProvider {
|
|
|
1888
1956
|
.sort((a, b) => String(a.requirementId).localeCompare(String(b.requirementId)));
|
|
1889
1957
|
}
|
|
1890
1958
|
|
|
1959
|
+
private async buildMewpL2ToLinkedL1BaseKeys(
|
|
1960
|
+
requirements: MewpL2RequirementWorkItem[],
|
|
1961
|
+
projectName: string,
|
|
1962
|
+
testData: any[]
|
|
1963
|
+
): Promise<Map<string, Set<string>>> {
|
|
1964
|
+
const out = new Map<string, Set<string>>();
|
|
1965
|
+
const relatedIds = new Set<number>();
|
|
1966
|
+
const linkedTestCaseIdsByL2 = new Map<string, Set<number>>();
|
|
1967
|
+
const testCaseTitleMap = this.buildMewpTestCaseTitleMap(testData);
|
|
1968
|
+
|
|
1969
|
+
for (const requirement of requirements || []) {
|
|
1970
|
+
const l2BaseKey = String(requirement?.baseKey || '').trim();
|
|
1971
|
+
if (!l2BaseKey) continue;
|
|
1972
|
+
if (!out.has(l2BaseKey)) out.set(l2BaseKey, new Set<string>());
|
|
1973
|
+
if (!linkedTestCaseIdsByL2.has(l2BaseKey)) linkedTestCaseIdsByL2.set(l2BaseKey, new Set<number>());
|
|
1974
|
+
for (const testCaseId of requirement?.linkedTestCaseIds || []) {
|
|
1975
|
+
const numeric = Number(testCaseId);
|
|
1976
|
+
if (Number.isFinite(numeric) && numeric > 0) {
|
|
1977
|
+
linkedTestCaseIdsByL2.get(l2BaseKey)!.add(numeric);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
for (const relatedId of requirement?.relatedWorkItemIds || []) {
|
|
1981
|
+
const id = Number(relatedId);
|
|
1982
|
+
if (Number.isFinite(id) && id > 0) relatedIds.add(id);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
let titleDerivedCount = 0;
|
|
1987
|
+
for (const [l2BaseKey, testCaseIds] of linkedTestCaseIdsByL2.entries()) {
|
|
1988
|
+
const targetSet = out.get(l2BaseKey) || new Set<string>();
|
|
1989
|
+
for (const testCaseId of testCaseIds) {
|
|
1990
|
+
const title = String(testCaseTitleMap.get(testCaseId) || '').trim();
|
|
1991
|
+
if (!title) continue;
|
|
1992
|
+
const fromTitleCodes = this.extractRequirementCodesFromExpectedText(title, false);
|
|
1993
|
+
if (fromTitleCodes.size > 0) {
|
|
1994
|
+
for (const code of fromTitleCodes) {
|
|
1995
|
+
const normalized = this.toRequirementKey(code);
|
|
1996
|
+
if (normalized) targetSet.add(normalized);
|
|
1997
|
+
}
|
|
1998
|
+
} else {
|
|
1999
|
+
const normalized = this.toRequirementKey(title);
|
|
2000
|
+
if (normalized) targetSet.add(normalized);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
if (targetSet.size > 0) {
|
|
2004
|
+
titleDerivedCount += 1;
|
|
2005
|
+
}
|
|
2006
|
+
out.set(l2BaseKey, targetSet);
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
if (relatedIds.size === 0) {
|
|
2010
|
+
const linkedL1Count = [...out.values()].reduce((sum, set) => sum + set.size, 0);
|
|
2011
|
+
logger.info(
|
|
2012
|
+
`MEWP L2->L1 mapping summary: l2Families=${out.size} ` +
|
|
2013
|
+
`fromTitle=${titleDerivedCount} fallbackFromLinkedL1=0 linkedL1Keys=${linkedL1Count}`
|
|
2014
|
+
);
|
|
2015
|
+
return out;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
const relatedWorkItems = await this.fetchWorkItemsByIds(projectName, [...relatedIds], false);
|
|
2019
|
+
const l1BaseByWorkItemId = new Map<number, string>();
|
|
2020
|
+
for (const workItem of relatedWorkItems || []) {
|
|
2021
|
+
const workItemId = Number(workItem?.id || 0);
|
|
2022
|
+
if (!Number.isFinite(workItemId) || workItemId <= 0) continue;
|
|
2023
|
+
const fields = workItem?.fields || {};
|
|
2024
|
+
const customerId = this.extractMewpRequirementIdentifier(fields);
|
|
2025
|
+
const baseKey = this.toRequirementKey(customerId);
|
|
2026
|
+
if (!baseKey) continue;
|
|
2027
|
+
l1BaseByWorkItemId.set(workItemId, baseKey);
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
let linkedFallbackCount = 0;
|
|
2031
|
+
for (const requirement of requirements || []) {
|
|
2032
|
+
const l2BaseKey = String(requirement?.baseKey || '').trim();
|
|
2033
|
+
if (!l2BaseKey) continue;
|
|
2034
|
+
if (!out.has(l2BaseKey)) out.set(l2BaseKey, new Set<string>());
|
|
2035
|
+
const targetSet = out.get(l2BaseKey)!;
|
|
2036
|
+
if (targetSet.size > 0) {
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
for (const relatedId of requirement?.relatedWorkItemIds || []) {
|
|
2040
|
+
const baseKey = l1BaseByWorkItemId.get(Number(relatedId));
|
|
2041
|
+
if (baseKey) targetSet.add(baseKey);
|
|
2042
|
+
}
|
|
2043
|
+
if (targetSet.size > 0) {
|
|
2044
|
+
linkedFallbackCount += 1;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
const l2Families = out.size;
|
|
2049
|
+
const withLinkedL1 = [...out.values()].filter((set) => set.size > 0).length;
|
|
2050
|
+
const linkedL1Count = [...out.values()].reduce((sum, set) => sum + set.size, 0);
|
|
2051
|
+
logger.info(
|
|
2052
|
+
`MEWP L2->L1 mapping summary: l2Families=${l2Families} withLinkedL1=${withLinkedL1} ` +
|
|
2053
|
+
`fromTitle=${titleDerivedCount} fallbackFromLinkedL1=${linkedFallbackCount} linkedL1Keys=${linkedL1Count}`
|
|
2054
|
+
);
|
|
2055
|
+
return out;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
private buildMewpExternalJoinKeysByL2Requirement(
|
|
2059
|
+
requirements: MewpL2RequirementFamily[],
|
|
2060
|
+
l2ToLinkedL1BaseKeys: Map<string, Set<string>>
|
|
2061
|
+
): Map<string, Set<string>> {
|
|
2062
|
+
const out = new Map<string, Set<string>>();
|
|
2063
|
+
for (const requirement of requirements || []) {
|
|
2064
|
+
const l2BaseKey = String(requirement?.baseKey || '').trim();
|
|
2065
|
+
if (!l2BaseKey) continue;
|
|
2066
|
+
const joinKeys = new Set<string>([l2BaseKey]);
|
|
2067
|
+
for (const l1Key of l2ToLinkedL1BaseKeys.get(l2BaseKey) || []) {
|
|
2068
|
+
const normalized = String(l1Key || '').trim();
|
|
2069
|
+
if (normalized) joinKeys.add(normalized);
|
|
2070
|
+
}
|
|
2071
|
+
out.set(l2BaseKey, joinKeys);
|
|
2072
|
+
}
|
|
2073
|
+
return out;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
1891
2076
|
private buildRequirementFamilyMap(
|
|
1892
2077
|
requirements: Array<Pick<MewpL2RequirementWorkItem, 'requirementId' | 'baseKey'>>,
|
|
1893
2078
|
scopedRequirementKeys?: Set<string>
|
|
@@ -2153,47 +2338,33 @@ export default class ResultDataProvider {
|
|
|
2153
2338
|
return [...out].sort((a, b) => a - b);
|
|
2154
2339
|
}
|
|
2155
2340
|
|
|
2156
|
-
private extractMewpRequirementIdentifier(fields: Record<string, any
|
|
2341
|
+
private extractMewpRequirementIdentifier(fields: Record<string, any>): string {
|
|
2157
2342
|
const entries = Object.entries(fields || {});
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2343
|
+
const normalizeFieldKey = (value: string): string =>
|
|
2344
|
+
String(value || '')
|
|
2345
|
+
.toLowerCase()
|
|
2346
|
+
.replace(/[^a-z0-9]/g, '');
|
|
2347
|
+
|
|
2348
|
+
// Strict MEWP mode: only explicit MEWP customer-id fields are accepted.
|
|
2349
|
+
// API display name: "Customer ID"
|
|
2350
|
+
// API reference name: "Custom.CustomerID"
|
|
2351
|
+
const customerIdFieldKeys = new Set<string>([
|
|
2161
2352
|
'customerid',
|
|
2162
|
-
'
|
|
2163
|
-
|
|
2164
|
-
'requirementid',
|
|
2165
|
-
'externalid',
|
|
2166
|
-
'srid',
|
|
2167
|
-
'sapwbsid',
|
|
2168
|
-
];
|
|
2169
|
-
for (const [key, value] of entries) {
|
|
2170
|
-
const normalizedKey = String(key || '').toLowerCase();
|
|
2171
|
-
if (!strictHints.some((hint) => normalizedKey.includes(hint))) continue;
|
|
2172
|
-
|
|
2173
|
-
const valueAsString = this.toMewpComparableText(value);
|
|
2174
|
-
if (!valueAsString) continue;
|
|
2175
|
-
const normalized = this.normalizeMewpRequirementCodeWithSuffix(valueAsString);
|
|
2176
|
-
if (normalized) return normalized;
|
|
2177
|
-
}
|
|
2353
|
+
'customcustomerid',
|
|
2354
|
+
]);
|
|
2178
2355
|
|
|
2179
|
-
// Second pass: weaker hints, but still key-based only.
|
|
2180
|
-
const looseHints = ['customer', 'requirement', 'external', 'sapwbs', 'sr'];
|
|
2181
2356
|
for (const [key, value] of entries) {
|
|
2182
|
-
const normalizedKey =
|
|
2183
|
-
if (!
|
|
2357
|
+
const normalizedKey = normalizeFieldKey(key);
|
|
2358
|
+
if (!customerIdFieldKeys.has(normalizedKey)) continue;
|
|
2184
2359
|
|
|
2185
2360
|
const valueAsString = this.toMewpComparableText(value);
|
|
2186
2361
|
if (!valueAsString) continue;
|
|
2187
|
-
const normalized = this.normalizeMewpRequirementCodeWithSuffix(valueAsString);
|
|
2188
|
-
if (normalized) return normalized;
|
|
2189
|
-
}
|
|
2190
2362
|
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
if (titleCode) return titleCode;
|
|
2363
|
+
const normalizedRequirementId = this.normalizeMewpRequirementCodeWithSuffix(valueAsString);
|
|
2364
|
+
if (normalizedRequirementId) return normalizedRequirementId;
|
|
2365
|
+
}
|
|
2195
2366
|
|
|
2196
|
-
return
|
|
2367
|
+
return '';
|
|
2197
2368
|
}
|
|
2198
2369
|
|
|
2199
2370
|
private deriveMewpResponsibility(fields: Record<string, any>): string {
|
|
@@ -1178,9 +1178,9 @@ describe('ResultDataProvider', () => {
|
|
|
1178
1178
|
})
|
|
1179
1179
|
);
|
|
1180
1180
|
|
|
1181
|
-
const covered = result.rows.find((row: any) => row['L2 REQ ID'] === '
|
|
1182
|
-
const inferredByStepText = result.rows.find((row: any) => row['L2 REQ ID'] === '
|
|
1183
|
-
const uncovered = result.rows.find((row: any) => row['L2 REQ ID'] === '
|
|
1181
|
+
const covered = result.rows.find((row: any) => row['L2 REQ ID'] === '5001');
|
|
1182
|
+
const inferredByStepText = result.rows.find((row: any) => row['L2 REQ ID'] === '5002');
|
|
1183
|
+
const uncovered = result.rows.find((row: any) => row['L2 REQ ID'] === '5003');
|
|
1184
1184
|
|
|
1185
1185
|
expect(covered).toEqual(
|
|
1186
1186
|
expect.objectContaining({
|
|
@@ -1297,7 +1297,7 @@ describe('ResultDataProvider', () => {
|
|
|
1297
1297
|
[1]
|
|
1298
1298
|
);
|
|
1299
1299
|
|
|
1300
|
-
const row = result.rows.find((item: any) => item['L2 REQ ID'] === '
|
|
1300
|
+
const row = result.rows.find((item: any) => item['L2 REQ ID'] === '7001');
|
|
1301
1301
|
expect(parseSpy).not.toHaveBeenCalled();
|
|
1302
1302
|
expect(row).toEqual(
|
|
1303
1303
|
expect.objectContaining({
|
|
@@ -1312,11 +1312,10 @@ describe('ResultDataProvider', () => {
|
|
|
1312
1312
|
'System.Description': 'random text with SR9999 that is unrelated',
|
|
1313
1313
|
'Custom.CustomerId': 'customer id unknown',
|
|
1314
1314
|
'System.Title': 'Requirement without explicit SR code',
|
|
1315
|
-
}
|
|
1316
|
-
4321
|
|
1315
|
+
}
|
|
1317
1316
|
);
|
|
1318
1317
|
|
|
1319
|
-
expect(requirementId).toBe('
|
|
1318
|
+
expect(requirementId).toBe('');
|
|
1320
1319
|
});
|
|
1321
1320
|
|
|
1322
1321
|
it('should derive responsibility from Custom.SAPWBS when present', () => {
|