@elisra-devops/docgen-data-provider 1.101.0 → 1.103.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elisra-devops/docgen-data-provider",
3
- "version": "1.101.0",
3
+ "version": "1.103.0",
4
4
  "description": "A document generator data provider, aimed to retrive data from azure devops",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,6 +28,7 @@ export interface MewpL2RequirementWorkItem {
28
28
  requirementId: string;
29
29
  baseKey: string;
30
30
  title: string;
31
+ owner: string;
31
32
  subSystem: string;
32
33
  responsibility: string;
33
34
  linkedTestCaseIds: number[];
@@ -40,6 +41,7 @@ export interface MewpL2RequirementFamily {
40
41
  requirementId: string;
41
42
  baseKey: string;
42
43
  title: string;
44
+ owner: string;
43
45
  subSystem: string;
44
46
  responsibility: string;
45
47
  linkedTestCaseIds: number[];
@@ -84,7 +86,10 @@ export type MewpCoverageL3L4Cell = MewpL3L4Pair;
84
86
 
85
87
  export interface MewpCoverageRow {
86
88
  'L2 REQ ID': string;
89
+ 'SR #': string;
87
90
  'L2 REQ Title': string;
91
+ 'L2 REQ Full Title'?: string;
92
+ 'L2 Owner': string;
88
93
  'L2 SubSystem': string;
89
94
  'L2 Run Status': MewpRunStatus;
90
95
  'Bug ID': number | '';
@@ -56,10 +56,14 @@ export default class ResultDataProvider {
56
56
  private static readonly MEWP_INTERNAL_VALIDATION_DIAGNOSTICS_TAG =
57
57
  '[MEWP][InternalValidation][Diagnostics]';
58
58
  private static readonly MEWP_INTERNAL_VALIDATION_SUMMARY_TAG = '[MEWP][InternalValidation][Summary]';
59
+ private static readonly MEWP_INTERNAL_VALIDATION_ASSUMPTIONS_REF = 'Assumptions';
60
+ private static readonly MEWP_INTERNAL_VALIDATION_ASSUMPTIONS_HEADER_PATTERN = /assumptions/i;
59
61
 
60
62
  private static readonly MEWP_L2_COVERAGE_COLUMNS = [
61
63
  'L2 REQ ID',
64
+ 'SR #',
62
65
  'L2 REQ Title',
66
+ 'L2 Owner',
63
67
  'L2 SubSystem',
64
68
  'L2 Run Status',
65
69
  'Bug ID',
@@ -806,6 +810,7 @@ export default class ResultDataProvider {
806
810
  const rows: MewpInternalValidationRow[] = [];
807
811
  const stepsXmlByTestCase = this.buildTestCaseStepsXmlMap(testData);
808
812
  const testCaseTitleMap = this.buildMewpTestCaseTitleMap(testData);
813
+ const testCaseDescriptionMap = this.buildMewpTestCaseDescriptionMap(testData);
809
814
  const allTestCaseIds = new Set<number>();
810
815
  for (const suite of testData || []) {
811
816
  const testCasesItems = Array.isArray(suite?.testCasesItems) ? suite.testCasesItems : [];
@@ -861,7 +866,16 @@ export default class ResultDataProvider {
861
866
  : [];
862
867
  const executableSteps = parsedSteps.filter((step) => !step?.isSharedStepTitle);
863
868
  diagnostics.totalParsedSteps += executableSteps.length;
864
- const mentionEntries = this.extractRequirementMentionsFromExpectedSteps(parsedSteps, true);
869
+ // Direction A/B mentions can come from two sources:
870
+ // 1) Expected Result in executable steps.
871
+ // 2) "Assumptions" section in the test-case description.
872
+ const stepMentionEntries = this.extractRequirementMentionsFromExpectedSteps(parsedSteps, true);
873
+ const descriptionText = testCaseDescriptionMap.get(testCaseId) || '';
874
+ const assumptionsMentionEntries = this.extractRequirementMentionsFromAssumptionsDescription(
875
+ descriptionText,
876
+ true
877
+ );
878
+ const mentionEntries = [...stepMentionEntries, ...assumptionsMentionEntries];
865
879
  diagnostics.totalStepsWithMentions += mentionEntries.length;
866
880
  const mentionedL2Only = new Set<string>();
867
881
  const mentionedCodeFirstStep = new Map<string, string>();
@@ -903,24 +917,6 @@ export default class ResultDataProvider {
903
917
  if (!mentionedCodesByBase.has(baseKey)) mentionedCodesByBase.set(baseKey, new Set<string>());
904
918
  mentionedCodesByBase.get(baseKey)!.add(code);
905
919
  }
906
- if (traceCurrentTestCase) {
907
- logger.debug(
908
- this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
909
- event: 'test-case-start',
910
- tc: testCaseId,
911
- parsedSteps: executableSteps.length,
912
- stepsWithMentions: mentionEntries.length,
913
- mentionedCodes:
914
- [...mentionedL2Only].sort((a, b) => this.compareMewpRequirementCodes(a, b)).join('; ') ||
915
- '<none>',
916
- linkedCodesInTestCase:
917
- [...linkedFullCodes]
918
- .sort((a, b) => this.compareMewpRequirementCodes(a, b))
919
- .join('; ') || '<none>',
920
- })
921
- );
922
- }
923
-
924
920
  // Direction A logic:
925
921
  // 1) Base mention ("SR0054") is parent-level and considered covered only when
926
922
  // the whole family is covered across scoped test cases:
@@ -946,38 +942,12 @@ export default class ResultDataProvider {
946
942
 
947
943
  // Base mention ("SR0054") requires full family coverage across selected test cases.
948
944
  if (hasBaseMention) {
949
- const missingRequiredFamilyMembers = requiredFamilyMembers.filter(
950
- (memberCode) => !familyLinkedCodes.has(memberCode)
951
- );
952
945
  const isWholeFamilyCovered = requiredFamilyMembers.every((memberCode) =>
953
946
  familyLinkedCodes.has(memberCode)
954
947
  );
955
948
  if (!isWholeFamilyCovered) {
956
949
  missingBaseWhenFamilyUncovered.add(baseKey);
957
950
  }
958
- if (traceCurrentTestCase) {
959
- logger.debug(
960
- this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
961
- event: 'base-family-coverage',
962
- tc: testCaseId,
963
- base: baseKey,
964
- baseMention: true,
965
- requiredFamily:
966
- requiredFamilyMembers
967
- .sort((a, b) => this.compareMewpRequirementCodes(a, b))
968
- .join('; ') || '<none>',
969
- linkedAcrossScope:
970
- [...familyLinkedCodes]
971
- .sort((a, b) => this.compareMewpRequirementCodes(a, b))
972
- .join('; ') || '<none>',
973
- missingRequired:
974
- missingRequiredFamilyMembers
975
- .sort((a, b) => this.compareMewpRequirementCodes(a, b))
976
- .join('; ') || '<none>',
977
- covered: isWholeFamilyCovered,
978
- })
979
- );
980
- }
981
951
  }
982
952
 
983
953
  // Specific mention ("SR0054-1") validates as exact-match only across scoped test cases.
@@ -987,60 +957,20 @@ export default class ResultDataProvider {
987
957
  for (const code of missingSpecificMembers) {
988
958
  missingSpecificMentionedNoFamily.add(code);
989
959
  }
990
- if (traceCurrentTestCase && mentionedSpecificMembers.length > 0) {
991
- logger.debug(
992
- this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
993
- event: 'specific-members-check',
994
- tc: testCaseId,
995
- base: baseKey,
996
- specificMentioned:
997
- mentionedSpecificMembers
998
- .sort((a, b) => this.compareMewpRequirementCodes(a, b))
999
- .join('; ') || '<none>',
1000
- specificMissing:
1001
- missingSpecificMembers
1002
- .sort((a, b) => this.compareMewpRequirementCodes(a, b))
1003
- .join('; ') || '<none>',
1004
- })
1005
- );
1006
- }
1007
960
  continue;
1008
961
  }
1009
962
 
1010
963
  // Fallback path when family data is unavailable for this base key.
1011
- const fallbackMissingSpecific: string[] = [];
1012
- let fallbackMissingBase = false;
1013
964
  for (const code of mentionedCodes) {
1014
965
  const hasSpecificSuffix = /-\d+$/.test(code);
1015
966
  if (hasSpecificSuffix) {
1016
967
  if (!linkedFullCodesAcrossTestCases.has(code)) {
1017
968
  missingSpecificMentionedNoFamily.add(code);
1018
- fallbackMissingSpecific.push(code);
1019
969
  }
1020
970
  } else if (!linkedBaseKeysAcrossTestCases.has(baseKey)) {
1021
971
  missingBaseWhenFamilyUncovered.add(baseKey);
1022
- fallbackMissingBase = true;
1023
972
  }
1024
973
  }
1025
- if (traceCurrentTestCase) {
1026
- logger.debug(
1027
- this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
1028
- event: 'fallback-path',
1029
- tc: testCaseId,
1030
- base: baseKey,
1031
- fallbackUsed: true,
1032
- mentioned:
1033
- mentionedCodesList
1034
- .sort((a, b) => this.compareMewpRequirementCodes(a, b))
1035
- .join('; ') || '<none>',
1036
- missingSpecific:
1037
- fallbackMissingSpecific
1038
- .sort((a, b) => this.compareMewpRequirementCodes(a, b))
1039
- .join('; ') || '<none>',
1040
- missingBase: fallbackMissingBase,
1041
- })
1042
- );
1043
- }
1044
974
  }
1045
975
 
1046
976
  // Direction B is family-based: if any member of a family is mentioned in Expected Result,
@@ -1086,17 +1016,6 @@ export default class ResultDataProvider {
1086
1016
  const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
1087
1017
  appendMentionedButNotLinked(baseKey, stepRef);
1088
1018
  }
1089
- if (traceCurrentTestCase) {
1090
- logger.debug(
1091
- this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
1092
- event: 'direction-a-summary',
1093
- tc: testCaseId,
1094
- missingSpecific: sortedMissingSpecificMentionedNoFamily.join('; ') || '<none>',
1095
- missingBase: sortedMissingBaseWhenFamilyUncovered.join('; ') || '<none>',
1096
- })
1097
- );
1098
- }
1099
-
1100
1019
  const sortedExtraLinked = [...new Set(extraLinked)]
1101
1020
  .map((code) => this.normalizeMewpRequirementCodeWithSuffix(code))
1102
1021
  .filter((code) => !!code)
@@ -1134,21 +1053,23 @@ export default class ResultDataProvider {
1134
1053
  const validationStatus: 'Pass' | 'Fail' =
1135
1054
  mentionedButNotLinked || linkedButNotMentioned ? 'Fail' : 'Pass';
1136
1055
  if (validationStatus === 'Fail') diagnostics.failingRows += 1;
1137
- logger.debug(
1138
- this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_DIAGNOSTICS_TAG, {
1139
- testCaseId,
1140
- parsedSteps: executableSteps.length,
1141
- stepsWithMentions: mentionEntries.length,
1142
- customerIdsFound: mentionedL2Only.size,
1143
- linkedRequirements: linkedFullCodes.size,
1144
- mentionedButNotLinked:
1145
- sortedMissingSpecificMentionedNoFamily.length +
1146
- sortedMissingBaseWhenFamilyUncovered.length,
1147
- linkedButNotMentioned: sortedExtraLinked.length,
1148
- status: validationStatus,
1149
- customerIdSample: [...mentionedL2Only].slice(0, 5).join(', '),
1150
- })
1151
- );
1056
+ if (traceCurrentTestCase) {
1057
+ logger.debug(
1058
+ this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_DIAGNOSTICS_TAG, {
1059
+ testCaseId,
1060
+ parsedSteps: executableSteps.length,
1061
+ stepsWithMentions: mentionEntries.length,
1062
+ customerIdsFound: mentionedL2Only.size,
1063
+ linkedRequirements: linkedFullCodes.size,
1064
+ mentionedButNotLinked:
1065
+ sortedMissingSpecificMentionedNoFamily.length +
1066
+ sortedMissingBaseWhenFamilyUncovered.length,
1067
+ linkedButNotMentioned: sortedExtraLinked.length,
1068
+ status: validationStatus,
1069
+ customerIdSample: [...mentionedL2Only].slice(0, 5).join(', '),
1070
+ })
1071
+ );
1072
+ }
1152
1073
 
1153
1074
  rows.push({
1154
1075
  'Test Case ID': testCaseId,
@@ -1297,7 +1218,7 @@ export default class ResultDataProvider {
1297
1218
  private createMewpCoverageRow(
1298
1219
  requirement: Pick<
1299
1220
  MewpL2RequirementFamily,
1300
- 'workItemId' | 'requirementId' | 'title' | 'subSystem' | 'responsibility'
1221
+ 'workItemId' | 'requirementId' | 'title' | 'owner' | 'subSystem' | 'responsibility'
1301
1222
  >,
1302
1223
  runStatus: MewpRunStatus,
1303
1224
  bug: MewpCoverageBugCell,
@@ -1305,12 +1226,18 @@ export default class ResultDataProvider {
1305
1226
  ): MewpCoverageRow {
1306
1227
  const l2ReqIdNumeric = Number(requirement?.workItemId || 0);
1307
1228
  const l2ReqId = l2ReqIdNumeric > 0 ? String(l2ReqIdNumeric) : '';
1229
+ const srNumber = this.normalizeMewpRequirementCodeWithSuffix(requirement?.requirementId || '');
1308
1230
  const l2ReqTitle = this.toMewpComparableText(requirement.title);
1231
+ const reqName = this.deriveMewpRequirementDisplayName(srNumber, l2ReqTitle);
1232
+ const l2Owner = this.toMewpComparableText(requirement.owner);
1309
1233
  const l2SubSystem = this.toMewpComparableText(requirement.subSystem);
1310
1234
 
1311
1235
  return {
1312
1236
  'L2 REQ ID': l2ReqId,
1313
- 'L2 REQ Title': l2ReqTitle,
1237
+ 'SR #': srNumber,
1238
+ 'L2 REQ Title': reqName,
1239
+ 'L2 REQ Full Title': l2ReqTitle,
1240
+ 'L2 Owner': l2Owner,
1314
1241
  'L2 SubSystem': l2SubSystem,
1315
1242
  'L2 Run Status': runStatus,
1316
1243
  'Bug ID': Number.isFinite(Number(bug?.id)) && Number(bug?.id) > 0 ? Number(bug?.id) : '',
@@ -1323,6 +1250,20 @@ export default class ResultDataProvider {
1323
1250
  };
1324
1251
  }
1325
1252
 
1253
+ private deriveMewpRequirementDisplayName(requirementCode: string, title: string): string {
1254
+ const normalizedTitle = this.toMewpComparableText(title);
1255
+ if (!normalizedTitle) return '';
1256
+
1257
+ const normalizedCode = this.normalizeMewpRequirementCodeWithSuffix(requirementCode || '');
1258
+ if (!normalizedCode) return normalizedTitle;
1259
+
1260
+ const escapedCode = normalizedCode.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1261
+ const codePrefixPattern = new RegExp(`^${escapedCode}(?:\\s*[:\\-–—]\\s*|\\s+)`, 'i');
1262
+ const withoutCodePrefix = normalizedTitle.replace(codePrefixPattern, '').trim();
1263
+ if (!withoutCodePrefix) return normalizedTitle;
1264
+ return withoutCodePrefix;
1265
+ }
1266
+
1326
1267
  private createEmptyMewpCoverageBugCell(): MewpCoverageBugCell {
1327
1268
  return { id: '' as '', title: '', responsibility: '' };
1328
1269
  }
@@ -1643,14 +1584,17 @@ export default class ResultDataProvider {
1643
1584
  }
1644
1585
 
1645
1586
  private buildRequirementSapWbsByBaseKey(
1646
- requirements: Array<Pick<MewpL2RequirementWorkItem, 'baseKey' | 'responsibility'>>
1587
+ requirements: Array<Pick<MewpL2RequirementWorkItem, 'baseKey' | 'owner' | 'responsibility'>>
1647
1588
  ): Map<string, string> {
1648
1589
  const out = new Map<string, string>();
1649
1590
  for (const requirement of requirements || []) {
1650
1591
  const baseKey = String(requirement?.baseKey || '').trim();
1651
1592
  if (!baseKey) continue;
1652
1593
 
1653
- const normalized = this.resolveMewpResponsibility(this.toMewpComparableText(requirement?.responsibility));
1594
+ const rawOwner = this.toMewpComparableText(requirement?.owner);
1595
+ const normalized =
1596
+ this.resolveMewpResponsibility(rawOwner) ||
1597
+ this.resolveMewpResponsibility(this.toMewpComparableText(requirement?.responsibility));
1654
1598
  if (!normalized) continue;
1655
1599
 
1656
1600
  const existing = out.get(baseKey) || '';
@@ -1752,6 +1696,54 @@ export default class ResultDataProvider {
1752
1696
  return map;
1753
1697
  }
1754
1698
 
1699
+ /**
1700
+ * Builds a lookup of test case id -> test case description using suite payload data.
1701
+ *
1702
+ * Resolution order:
1703
+ * 1) direct description fields on test-case payload (`testCase.description` / `workItem.description`)
1704
+ * 2) `System.Description` / `Description` in work-item fields map
1705
+ * 3) `System.Description` / `Description` from work-item field list (`workItemFields`)
1706
+ */
1707
+ private buildMewpTestCaseDescriptionMap(testData: any[]): Map<number, string> {
1708
+ const map = new Map<number, string>();
1709
+
1710
+ const readDescriptionFromFields = (fields: Record<string, any>): string => {
1711
+ const value =
1712
+ this.getFieldValueByName(fields, 'System.Description') ??
1713
+ this.getFieldValueByName(fields, 'Description');
1714
+ return this.toMewpComparableText(value);
1715
+ };
1716
+
1717
+ for (const suite of testData || []) {
1718
+ const testCasesItems = Array.isArray(suite?.testCasesItems) ? suite.testCasesItems : [];
1719
+ for (const testCase of testCasesItems) {
1720
+ const id = Number(testCase?.workItem?.id || testCase?.testCaseId || testCase?.id);
1721
+ if (!Number.isFinite(id) || id <= 0 || map.has(id)) continue;
1722
+
1723
+ const fromDirect = this.toMewpComparableText(testCase?.description || testCase?.workItem?.description);
1724
+ if (fromDirect) {
1725
+ map.set(id, fromDirect);
1726
+ continue;
1727
+ }
1728
+
1729
+ const fieldsFromMap = testCase?.workItem?.fields || {};
1730
+ const fromFieldsMap = readDescriptionFromFields(fieldsFromMap);
1731
+ if (fromFieldsMap) {
1732
+ map.set(id, fromFieldsMap);
1733
+ continue;
1734
+ }
1735
+
1736
+ const fieldsFromList = this.extractWorkItemFieldsMap(testCase?.workItem?.workItemFields);
1737
+ const fromFieldsList = readDescriptionFromFields(fieldsFromList);
1738
+ if (fromFieldsList) {
1739
+ map.set(id, fromFieldsList);
1740
+ }
1741
+ }
1742
+ }
1743
+
1744
+ return map;
1745
+ }
1746
+
1755
1747
  private extractMewpTestCaseId(runResult: any): number {
1756
1748
  const testCaseId = Number(runResult?.testCaseId || runResult?.testCase?.id || 0);
1757
1749
  return Number.isFinite(testCaseId) ? testCaseId : 0;
@@ -1956,6 +1948,107 @@ export default class ResultDataProvider {
1956
1948
  return out;
1957
1949
  }
1958
1950
 
1951
+ /**
1952
+ * Extracts SR requirement mentions from the "assumptions" section inside a test-case description.
1953
+ *
1954
+ * Notes:
1955
+ * - Heading detection is case-insensitive and keyed by "assumptions".
1956
+ * - Parsing is scoped to that section only and stops when a likely next section heading is reached
1957
+ * after at least one requirement-bearing line was collected.
1958
+ * - Returned stepRef is a synthetic marker ("Assumptions") so downstream discrepancy output can
1959
+ * clearly attribute source to description-level assumptions.
1960
+ */
1961
+ private extractRequirementMentionsFromAssumptionsDescription(
1962
+ description: string,
1963
+ includeSuffix: boolean
1964
+ ): Array<{ stepRef: string; codes: Set<string> }> {
1965
+ const lines = this.normalizeMewpDescriptionLines(description);
1966
+ if (lines.length === 0) return [];
1967
+
1968
+ const assumptionsHeaderIndex = lines.findIndex((line) =>
1969
+ ResultDataProvider.MEWP_INTERNAL_VALIDATION_ASSUMPTIONS_HEADER_PATTERN.test(line)
1970
+ );
1971
+ if (assumptionsHeaderIndex < 0) return [];
1972
+
1973
+ const assumptionCodes = new Set<string>();
1974
+ let hasCollectedRequirementCodes = false;
1975
+ for (let index = assumptionsHeaderIndex + 1; index < lines.length; index += 1) {
1976
+ const rawLine = String(lines[index] || '').trim();
1977
+ if (!rawLine) continue;
1978
+
1979
+ const line = rawLine.replace(/^[-*•]+\s*/, '').trim();
1980
+ if (!line) continue;
1981
+
1982
+ const lineCodes = this.extractRequirementCodesFromExpectedText(line, includeSuffix);
1983
+ if (lineCodes.size > 0) {
1984
+ for (const code of lineCodes) assumptionCodes.add(code);
1985
+ hasCollectedRequirementCodes = true;
1986
+ continue;
1987
+ }
1988
+
1989
+ if (hasCollectedRequirementCodes && this.isLikelyMewpDescriptionSectionHeading(line)) {
1990
+ break;
1991
+ }
1992
+ }
1993
+
1994
+ if (assumptionCodes.size === 0) return [];
1995
+ return [{ stepRef: ResultDataProvider.MEWP_INTERNAL_VALIDATION_ASSUMPTIONS_REF, codes: assumptionCodes }];
1996
+ }
1997
+
1998
+ /**
1999
+ * Normalizes raw HTML/plain description content into comparable text lines.
2000
+ * This makes section/title heuristics resilient to formatting differences.
2001
+ */
2002
+ private normalizeMewpDescriptionLines(description: string): string[] {
2003
+ const raw = String(description || '');
2004
+ if (!raw) return [];
2005
+
2006
+ const normalized = raw
2007
+ .replace(/\r\n?/g, '\n')
2008
+ .replace(/<\s*br\s*\/?>/gi, '\n')
2009
+ // Underlined sections are commonly used as inline HTML titles in MEWP test-case descriptions.
2010
+ // Treat underline tags as boundaries so compact forms like <u><b>Title</b></u><p>... preserve section split.
2011
+ .replace(/<\s*u\b[^>]*>/gi, '\n')
2012
+ .replace(/<\/\s*u\s*>/gi, '\n')
2013
+ .replace(/<\s*li\b[^>]*>/gi, '\n- ')
2014
+ .replace(/<\/\s*(p|div|li|ul|ol|tr|td|h[1-6])\s*>/gi, '\n')
2015
+ .replace(/&nbsp;|&#160;|&#xA0;/gi, ' ')
2016
+ .replace(/&lt;/gi, '<')
2017
+ .replace(/&gt;/gi, '>')
2018
+ .replace(/&amp;/gi, '&')
2019
+ .replace(/&quot;/gi, '"')
2020
+ .replace(/&#39;|&apos;/gi, "'")
2021
+ .replace(/<[^>]*>/g, ' ')
2022
+ .replace(/[\u200B-\u200D\uFEFF]/g, '')
2023
+ .replace(/[ \t]+/g, ' ');
2024
+
2025
+ return normalized
2026
+ .split('\n')
2027
+ .map((line) => String(line || '').trim())
2028
+ .filter((line) => !!line);
2029
+ }
2030
+
2031
+ /**
2032
+ * Heuristic detector for description section headings used to stop assumptions parsing.
2033
+ * A line is considered a heading when it is short/title-like and does not itself contain SR codes.
2034
+ */
2035
+ private isLikelyMewpDescriptionSectionHeading(line: string): boolean {
2036
+ const normalized = String(line || '')
2037
+ .replace(/^[-*•]+\s*/, '')
2038
+ .trim();
2039
+ if (!normalized) return false;
2040
+ if (ResultDataProvider.MEWP_INTERNAL_VALIDATION_ASSUMPTIONS_HEADER_PATTERN.test(normalized))
2041
+ return false;
2042
+ if (this.extractRequirementCodesFromExpectedText(normalized, true).size > 0) return false;
2043
+
2044
+ const compact = normalized.replace(/[.:;,\-–—]+$/, '').trim();
2045
+ if (!compact) return false;
2046
+ const wordCount = compact.split(/\s+/).filter((item) => !!item).length;
2047
+ if (wordCount === 0 || wordCount > 12) return false;
2048
+ if (/[.;!?]/.test(compact)) return false;
2049
+ return /^[a-z0-9\s()&/+_'-]+$/i.test(compact);
2050
+ }
2051
+
1959
2052
  private extractRequirementCodesFromExpectedText(text: string, includeSuffix: boolean): Set<string> {
1960
2053
  const out = new Set<string>();
1961
2054
  const source = this.normalizeRequirementStepText(text);
@@ -2138,6 +2231,7 @@ export default class ResultDataProvider {
2138
2231
  requirementId,
2139
2232
  baseKey: this.toRequirementKey(requirementId),
2140
2233
  title: this.toMewpComparableText(fields?.['System.Title'] || wi?.title),
2234
+ owner: this.deriveMewpRequirementOwner(fields),
2141
2235
  subSystem: this.deriveMewpSubSystem(fields),
2142
2236
  responsibility: this.deriveMewpResponsibility(fields),
2143
2237
  linkedTestCaseIds: this.extractLinkedTestCaseIdsFromRequirement(wi?.relations || []),
@@ -2187,6 +2281,7 @@ export default class ResultDataProvider {
2187
2281
  if (areaPath.includes('\\customer requirements\\level 2')) score += 3;
2188
2282
  if (!areaPath.includes('\\mop')) score += 2;
2189
2283
  if (String(item?.title || '').trim()) score += 1;
2284
+ if (String(item?.owner || '').trim()) score += 1;
2190
2285
  if (String(item?.subSystem || '').trim()) score += 1;
2191
2286
  if (String(item?.responsibility || '').trim()) score += 1;
2192
2287
  return score;
@@ -2223,6 +2318,7 @@ export default class ResultDataProvider {
2223
2318
  requirementId: String(family?.representative?.requirementId || baseKey),
2224
2319
  baseKey,
2225
2320
  title: String(family?.representative?.title || ''),
2321
+ owner: String(family?.representative?.owner || ''),
2226
2322
  subSystem: String(family?.representative?.subSystem || ''),
2227
2323
  responsibility: String(family?.representative?.responsibility || ''),
2228
2324
  linkedTestCaseIds: [...family.linkedTestCaseIds].sort((a, b) => a - b),
@@ -2674,6 +2770,24 @@ export default class ResultDataProvider {
2674
2770
  return '';
2675
2771
  }
2676
2772
 
2773
+ // L2 owner is sourced only from requirement SAPWBS fields.
2774
+ private deriveMewpRequirementOwner(fields: Record<string, any>): string {
2775
+ const directCandidates = [fields?.['Custom.SAPWBS'], fields?.['SAPWBS']];
2776
+ for (const candidate of directCandidates) {
2777
+ const normalized = this.toMewpComparableText(candidate);
2778
+ if (normalized) return normalized;
2779
+ }
2780
+
2781
+ for (const [key, value] of Object.entries(fields || {})) {
2782
+ const normalizedKey = String(key || '').toLowerCase();
2783
+ if (!normalizedKey.includes('sapwbs')) continue;
2784
+ const normalizedValue = this.toMewpComparableText(value);
2785
+ if (normalizedValue) return normalizedValue;
2786
+ }
2787
+
2788
+ return '';
2789
+ }
2790
+
2677
2791
  // Test-case responsibility must come from test-case path context (not SAPWBS).
2678
2792
  private deriveMewpTestCaseResponsibility(fields: Record<string, any>): string {
2679
2793
  const areaPathCandidates = [