@elisra-devops/docgen-data-provider 1.62.0 → 1.63.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.
@@ -10,6 +10,23 @@ import { value } from '../models/tfs-data';
10
10
  import logger from '../utils/logger';
11
11
  const pLimit = require('p-limit');
12
12
 
13
+ type FallbackFetchOutcome = {
14
+ result: any;
15
+ usedFolder: any;
16
+ };
17
+
18
+ type DocTypeBranchConfig = {
19
+ id: string;
20
+ label: string;
21
+ // Candidate folder names (case-insensitive). If none resolve, fallback starts from `fallbackStart`.
22
+ folderNames?: string[];
23
+ fetcher: (folder: any) => Promise<any>;
24
+ // Optional validator to determine whether the fetch produced any usable queries.
25
+ validator?: (result: any) => boolean;
26
+ // Optional explicit starting folder for the fallback chain.
27
+ fallbackStart?: any;
28
+ };
29
+
13
30
  export default class TicketsDataProvider {
14
31
  orgUrl: string = '';
15
32
  token: string = '';
@@ -120,22 +137,197 @@ export default class TicketsDataProvider {
120
137
  else url = `${this.orgUrl}${project}/_apis/wit/queries/${path}?$depth=2&$expand=all`;
121
138
  let queries: any = await TFSServices.getItemContent(url, this.token);
122
139
  logger.debug(`doctype: ${docType}`);
123
- switch (docType?.toLowerCase()) {
124
- case 'std':
125
- const reqTestQueries = await this.fetchLinkedReqTestQueries(queries, false);
126
- const linkedMomQueries = await this.fetchLinkedMomQueries(queries);
140
+ const normalizedDocType = (docType || '').toLowerCase();
141
+ const queriesWithChildren = await this.ensureQueryChildren(queries);
142
+
143
+ switch (normalizedDocType) {
144
+ case 'std': {
145
+ const { root: stdRoot, found: stdRootFound } = await this.getDocTypeRoot(
146
+ queriesWithChildren,
147
+ 'std'
148
+ );
149
+ logger.debug(`[GetSharedQueries][std] using ${stdRootFound ? 'dedicated folder' : 'root queries'}`);
150
+ // Each branch describes the dedicated folder names, the fetch routine, and how to validate results.
151
+ const stdBranches = await this.fetchDocTypeBranches(queriesWithChildren, stdRoot, [
152
+ {
153
+ id: 'reqToTest',
154
+ label: '[GetSharedQueries][std][req-to-test]',
155
+ folderNames: [
156
+ 'requirement - test',
157
+ 'requirement to test case',
158
+ 'requirement to test',
159
+ 'req to test',
160
+ ],
161
+ fetcher: (folder: any) => this.fetchLinkedReqTestQueries(folder, false),
162
+ validator: (result: any) => this.hasAnyQueryTree(result?.reqTestTree),
163
+ },
164
+ {
165
+ id: 'testToReq',
166
+ label: '[GetSharedQueries][std][test-to-req]',
167
+ folderNames: [
168
+ 'test - requirement',
169
+ 'test to requirement',
170
+ 'test case to requirement',
171
+ 'test to req',
172
+ ],
173
+ fetcher: (folder: any) => this.fetchLinkedReqTestQueries(folder, true),
174
+ validator: (result: any) => this.hasAnyQueryTree(result?.testReqTree),
175
+ },
176
+ {
177
+ id: 'mom',
178
+ label: '[GetSharedQueries][std][mom]',
179
+ folderNames: ['linked mom', 'mom'],
180
+ fetcher: (folder: any) => this.fetchLinkedMomQueries(folder),
181
+ validator: (result: any) => this.hasAnyQueryTree(result?.linkedMomTree),
182
+ },
183
+ ]);
184
+
185
+ const reqToTestResult = stdBranches['reqToTest'];
186
+ const testToReqResult = stdBranches['testToReq'];
187
+ const momResult = stdBranches['mom'];
188
+
189
+ const reqTestQueries = {
190
+ reqTestTree: reqToTestResult?.result?.reqTestTree ?? null,
191
+ testReqTree: testToReqResult?.result?.testReqTree ?? reqToTestResult?.result?.testReqTree ?? null,
192
+ };
193
+
194
+ const linkedMomQueries = {
195
+ linkedMomTree: momResult?.result?.linkedMomTree ?? null,
196
+ };
197
+
127
198
  return { reqTestQueries, linkedMomQueries };
128
- case 'str':
129
- const reqTestTrees = await this.fetchLinkedReqTestQueries(queries, false);
130
- const openPcrTestTrees = await this.fetchLinkedOpenPcrTestQueries(queries, false);
199
+ }
200
+ case 'str': {
201
+ const { root: strRoot, found: strRootFound } = await this.getDocTypeRoot(
202
+ queriesWithChildren,
203
+ 'str'
204
+ );
205
+ logger.debug(`[GetSharedQueries][str] using ${strRootFound ? 'dedicated folder' : 'root queries'}`);
206
+ const strBranches = await this.fetchDocTypeBranches(queriesWithChildren, strRoot, [
207
+ {
208
+ id: 'reqToTest',
209
+ label: '[GetSharedQueries][str][req-to-test]',
210
+ folderNames: [
211
+ 'requirement - test',
212
+ 'requirement to test case',
213
+ 'requirement to test',
214
+ 'req to test',
215
+ ],
216
+ fetcher: (folder: any) => this.fetchLinkedReqTestQueries(folder, false),
217
+ validator: (result: any) => this.hasAnyQueryTree(result?.reqTestTree),
218
+ },
219
+ {
220
+ id: 'testToReq',
221
+ label: '[GetSharedQueries][str][test-to-req]',
222
+ folderNames: [
223
+ 'test - requirement',
224
+ 'test to requirement',
225
+ 'test case to requirement',
226
+ 'test to req',
227
+ ],
228
+ fetcher: (folder: any) => this.fetchLinkedReqTestQueries(folder, true),
229
+ validator: (result: any) => this.hasAnyQueryTree(result?.testReqTree),
230
+ },
231
+ {
232
+ id: 'openPcrToTest',
233
+ label: '[GetSharedQueries][str][open-pcr-to-test]',
234
+ folderNames: ['open pcr to test case', 'open pcr to test', 'open pcr - test', 'open pcr'],
235
+ fetcher: (folder: any) => this.fetchLinkedOpenPcrTestQueries(folder, false),
236
+ validator: (result: any) => this.hasAnyQueryTree(result?.OpenPcrToTestTree),
237
+ },
238
+ {
239
+ id: 'testToOpenPcr',
240
+ label: '[GetSharedQueries][str][test-to-open-pcr]',
241
+ folderNames: ['test case to open pcr', 'test to open pcr', 'test - open pcr', 'open pcr'],
242
+ fetcher: (folder: any) => this.fetchLinkedOpenPcrTestQueries(folder, true),
243
+ validator: (result: any) => this.hasAnyQueryTree(result?.TestToOpenPcrTree),
244
+ },
245
+ ]);
246
+
247
+ const strReqToTest = strBranches['reqToTest'];
248
+ const strTestToReq = strBranches['testToReq'];
249
+ const strOpenPcrToTest = strBranches['openPcrToTest'];
250
+ const strTestToOpenPcr = strBranches['testToOpenPcr'];
251
+
252
+ const reqTestTrees = {
253
+ reqTestTree: strReqToTest?.result?.reqTestTree ?? null,
254
+ testReqTree: strTestToReq?.result?.testReqTree ?? strReqToTest?.result?.testReqTree ?? null,
255
+ };
256
+
257
+ const openPcrTestTrees = {
258
+ OpenPcrToTestTree: strOpenPcrToTest?.result?.OpenPcrToTestTree ?? null,
259
+ TestToOpenPcrTree:
260
+ strTestToOpenPcr?.result?.TestToOpenPcrTree ??
261
+ strOpenPcrToTest?.result?.TestToOpenPcrTree ??
262
+ null,
263
+ };
264
+
131
265
  return { reqTestTrees, openPcrTestTrees };
132
- case 'test-reporter':
133
- const testAssociatedTree = await this.fetchTestReporterQueries(queries);
134
- return { testAssociatedTree };
266
+ }
267
+ case 'test-reporter': {
268
+ const { root: testReporterRoot, found: testReporterFound } = await this.getDocTypeRoot(
269
+ queriesWithChildren,
270
+ 'test-reporter'
271
+ );
272
+ logger.debug(
273
+ `[GetSharedQueries][test-reporter] using ${
274
+ testReporterFound ? 'dedicated folder' : 'root queries'
275
+ }`
276
+ );
277
+ const testReporterBranches = await this.fetchDocTypeBranches(
278
+ queriesWithChildren,
279
+ testReporterRoot,
280
+ [
281
+ {
282
+ id: 'testReporter',
283
+ label: '[GetSharedQueries][test-reporter]',
284
+ folderNames: ['test reporter', 'test-reporter'],
285
+ fetcher: (folder: any) => this.fetchTestReporterQueries(folder),
286
+ validator: (result: any) => this.hasAnyQueryTree(result?.testAssociatedTree),
287
+ },
288
+ ]
289
+ );
290
+ const testReporterFetch = testReporterBranches['testReporter'];
291
+ return testReporterFetch?.result ?? { testAssociatedTree: null };
292
+ }
135
293
  case 'srs':
136
- return await this.fetchSrsQueries(queries);
137
- case 'svd':
138
- return await this.fetchAnyQueries(queries);
294
+ return await this.fetchSrsQueries(queriesWithChildren);
295
+ case 'svd': {
296
+ const { root: svdRoot, found } = await this.getDocTypeRoot(queriesWithChildren, 'svd');
297
+ if (!found) {
298
+ logger.debug('[GetSharedQueries][svd] dedicated folder not found, using fallback tree');
299
+ }
300
+ const svdBranches = await this.fetchDocTypeBranches(queriesWithChildren, svdRoot, [
301
+ {
302
+ id: 'systemOverview',
303
+ label: '[GetSharedQueries][svd][system-overview]',
304
+ folderNames: ['system overview'],
305
+ fetcher: async (folder: any) => {
306
+ const { tree1 } = await this.structureAllQueryPath(folder);
307
+ return tree1;
308
+ },
309
+ validator: (result: any) => !!result,
310
+ },
311
+ {
312
+ id: 'knownBugs',
313
+ label: '[GetSharedQueries][svd][known-bugs]',
314
+ folderNames: ['known bugs', 'known bug'],
315
+ fetcher: async (folder: any) => {
316
+ const { tree2 } = await this.structureAllQueryPath(folder);
317
+ return tree2;
318
+ },
319
+ validator: (result: any) => !!result,
320
+ },
321
+ ]);
322
+
323
+ const systemOverviewFetch = svdBranches['systemOverview'];
324
+ const knownBugsFetch = svdBranches['knownBugs'];
325
+
326
+ return {
327
+ systemOverviewQueryTree: systemOverviewFetch?.result ?? null,
328
+ knownBugsQueryTree: knownBugsFetch?.result ?? null,
329
+ };
330
+ }
139
331
  default:
140
332
  break;
141
333
  }
@@ -231,6 +423,38 @@ export default class TicketsDataProvider {
231
423
  return { linkedMomTree };
232
424
  }
233
425
 
426
+ private hasAnyQueryTree(result: any): boolean {
427
+ const inspect = (value: any): boolean => {
428
+ if (!value) {
429
+ return false;
430
+ }
431
+
432
+ if (Array.isArray(value)) {
433
+ return value.some(inspect);
434
+ }
435
+
436
+ if (typeof value === 'object') {
437
+ if (value.isValidQuery || value.wiql || value.queryType) {
438
+ return true;
439
+ }
440
+
441
+ if ('roots' in value && Array.isArray(value.roots) && value.roots.length > 0) {
442
+ return true;
443
+ }
444
+
445
+ if ('children' in value && Array.isArray(value.children) && value.children.length > 0) {
446
+ return true;
447
+ }
448
+
449
+ return Object.values(value).some(inspect);
450
+ }
451
+
452
+ return false;
453
+ };
454
+
455
+ return inspect(result);
456
+ }
457
+
234
458
  /**
235
459
  * Fetches and structures linked queries related to open PCR (Problem Change Request) tests.
236
460
  *
@@ -271,13 +495,6 @@ export default class TicketsDataProvider {
271
495
  return { testAssociatedTree };
272
496
  }
273
497
 
274
- private async fetchAnyQueries(queries: any) {
275
- const { tree1: systemOverviewQueryTree, tree2: knownBugsQueryTree } = await this.structureAllQueryPath(
276
- queries
277
- );
278
- return { systemOverviewQueryTree, knownBugsQueryTree };
279
- }
280
-
281
498
  private async fetchSystemRequirementQueries(queries: any, excludedFolderNames: string[] = []) {
282
499
  const { tree1: systemRequirementsQueryTree } = await this.structureFetchedQueries(
283
500
  queries,
@@ -429,6 +646,278 @@ export default class TicketsDataProvider {
429
646
  );
430
647
  }
431
648
 
649
+ /**
650
+ * Performs a breadth-first walk starting at `parent` to locate the nearest folder whose
651
+ * name matches any of the provided candidates (case-insensitive). Exact matches win; if none
652
+ * are found the first partial match encountered is returned. When no candidates are located,
653
+ * the method yields `null`.
654
+ */
655
+ private async findChildFolderByPossibleNames(parent: any, possibleNames: string[]): Promise<any | null> {
656
+ if (!parent || !possibleNames?.length) {
657
+ return null;
658
+ }
659
+
660
+ const normalizedNames = possibleNames.map((name) => name.toLowerCase());
661
+
662
+ const isMatch = (candidate: string, value: string) => value === candidate;
663
+ const isPartialMatch = (candidate: string, value: string) => value.includes(candidate);
664
+
665
+ const tryMatch = (folder: any, matcher: (candidate: string, value: string) => boolean) => {
666
+ const folderName = (folder?.name || '').toLowerCase();
667
+ return normalizedNames.some((candidate) => matcher(candidate, folderName));
668
+ };
669
+
670
+ const parentWithChildren = await this.ensureQueryChildren(parent);
671
+ if (!parentWithChildren?.children?.length) {
672
+ return null;
673
+ }
674
+
675
+ const queue: any[] = [];
676
+ const visited = new Set<string>();
677
+ let partialCandidate: any = null;
678
+
679
+ // Seed the queue with direct children so we prefer closer matches before walking deeper.
680
+ for (const child of parentWithChildren.children) {
681
+ if (!child?.isFolder) {
682
+ continue;
683
+ }
684
+ const childId = child.id ?? `${child.name}-${Math.random()}`;
685
+ queue.push(child);
686
+ visited.add(childId);
687
+ }
688
+
689
+ const considerFolder = async (folder: any): Promise<any | null> => {
690
+ if (tryMatch(folder, isMatch)) {
691
+ return await this.ensureQueryChildren(folder);
692
+ }
693
+
694
+ if (!partialCandidate && tryMatch(folder, isPartialMatch)) {
695
+ partialCandidate = await this.ensureQueryChildren(folder);
696
+ }
697
+
698
+ return null;
699
+ };
700
+
701
+ for (const child of queue) {
702
+ const match = await considerFolder(child);
703
+ if (match) {
704
+ return match;
705
+ }
706
+ }
707
+
708
+ while (queue.length > 0) {
709
+ const current = queue.shift();
710
+ if (!current) {
711
+ continue;
712
+ }
713
+
714
+ const currentWithChildren = await this.ensureQueryChildren(current);
715
+ if (!currentWithChildren) {
716
+ continue;
717
+ }
718
+
719
+ const match = await considerFolder(currentWithChildren);
720
+ if (match) {
721
+ return match;
722
+ }
723
+
724
+ // Breadth-first expansion so we climb the hierarchy gradually.
725
+ if (currentWithChildren.children?.length) {
726
+ for (const child of currentWithChildren.children) {
727
+ if (!child?.isFolder) {
728
+ continue;
729
+ }
730
+ const childId = child.id ?? `${child.name}-${Math.random()}`;
731
+ if (!visited.has(childId)) {
732
+ visited.add(childId);
733
+ queue.push(child);
734
+ }
735
+ }
736
+ }
737
+ }
738
+
739
+ return partialCandidate;
740
+ }
741
+
742
+ /**
743
+ * Executes `fetcher` against `startingFolder` and, if the validator deems the result empty,
744
+ * climbs ancestor folders toward `rootQueries` until a satisfactory result is produced.
745
+ * The first successful folder short-circuits the search; otherwise the final attempt is
746
+ * returned to preserve legacy behavior.
747
+ */
748
+ private async fetchWithAncestorFallback(
749
+ rootQueries: any,
750
+ startingFolder: any,
751
+ fetcher: (folder: any) => Promise<any>,
752
+ logContext: string,
753
+ validator?: (result: any) => boolean
754
+ ): Promise<{ result: any; usedFolder: any }> {
755
+ const rootWithChildren = await this.ensureQueryChildren(rootQueries);
756
+ const candidates = await this.buildFallbackChain(rootWithChildren, startingFolder);
757
+ const evaluate = validator ?? ((res: any) => this.hasAnyQueryTree(res));
758
+
759
+ let lastResult: any = null;
760
+ let lastFolder: any = startingFolder ?? rootWithChildren;
761
+
762
+ for (const candidate of candidates) {
763
+ const enrichedCandidate = await this.ensureQueryChildren(candidate);
764
+ const candidateName = enrichedCandidate?.name ?? '<root>';
765
+ logger.debug(`${logContext} trying folder: ${candidateName}`);
766
+ lastResult = await fetcher(enrichedCandidate);
767
+ lastFolder = enrichedCandidate;
768
+ if (evaluate(lastResult)) {
769
+ logger.debug(`${logContext} using folder: ${candidateName}`);
770
+ return { result: lastResult, usedFolder: enrichedCandidate };
771
+ }
772
+ logger.debug(`${logContext} folder ${candidateName} produced no results, ascending`);
773
+ }
774
+
775
+ logger.debug(`${logContext} no folders yielded results, returning last attempt`);
776
+ return { result: lastResult, usedFolder: lastFolder };
777
+ }
778
+
779
+ /**
780
+ * Applies `fetchWithAncestorFallback` to each configured branch, resolving dedicated folders
781
+ * when available and emitting a map keyed by branch id. Each outcome includes both the
782
+ * resulting payload and the specific folder that satisfied the fallback chain.
783
+ */
784
+ private async fetchDocTypeBranches(
785
+ queriesWithChildren: any,
786
+ docRoot: any,
787
+ branches: DocTypeBranchConfig[]
788
+ ): Promise<Record<string, FallbackFetchOutcome>> {
789
+ const results: Record<string, FallbackFetchOutcome> = {};
790
+ const effectiveDocRoot = docRoot ?? queriesWithChildren;
791
+
792
+ for (const branch of branches) {
793
+ const fallbackStart = branch.fallbackStart ?? effectiveDocRoot;
794
+ let startingFolder = fallbackStart;
795
+ let startingName = startingFolder?.name ?? '<root>';
796
+
797
+ // Attempt to locate a more specific child folder, falling back to the provided root if absent.
798
+ if (branch.folderNames?.length && effectiveDocRoot) {
799
+ const resolvedFolder = await this.findChildFolderByPossibleNames(
800
+ effectiveDocRoot,
801
+ branch.folderNames
802
+ );
803
+ if (resolvedFolder) {
804
+ startingFolder = resolvedFolder;
805
+ startingName = resolvedFolder?.name ?? '<root>';
806
+ }
807
+ }
808
+
809
+ logger.debug(`${branch.label} starting folder: ${startingName}`);
810
+
811
+ const fetchOutcome = await this.fetchWithAncestorFallback(
812
+ queriesWithChildren,
813
+ startingFolder,
814
+ branch.fetcher,
815
+ branch.label,
816
+ branch.validator
817
+ );
818
+
819
+ logger.debug(`${branch.label} final folder: ${fetchOutcome.usedFolder?.name ?? '<root>'}`);
820
+
821
+ results[branch.id] = fetchOutcome;
822
+ }
823
+
824
+ return results;
825
+ }
826
+
827
+ /**
828
+ * Constructs an ordered list of folders to probe during fallback. The sequence starts at
829
+ * `startingFolder` (if provided) and walks upward through ancestors to the root query tree,
830
+ * ensuring no folder id appears twice.
831
+ */
832
+ private async buildFallbackChain(rootQueries: any, startingFolder: any): Promise<any[]> {
833
+ const chain: any[] = [];
834
+ const seen = new Set<string>();
835
+ const pushUnique = (node: any) => {
836
+ if (!node) {
837
+ return;
838
+ }
839
+ const id = node.id ?? '__root__';
840
+ if (seen.has(id)) {
841
+ return;
842
+ }
843
+ seen.add(id);
844
+ chain.push(node);
845
+ };
846
+
847
+ if (startingFolder?.id) {
848
+ const path = await this.findPathToNode(rootQueries, startingFolder.id);
849
+ if (path) {
850
+ for (let i = path.length - 1; i >= 0; i--) {
851
+ pushUnique(path[i]);
852
+ }
853
+ } else {
854
+ pushUnique(startingFolder);
855
+ }
856
+ } else if (startingFolder) {
857
+ pushUnique(startingFolder);
858
+ }
859
+
860
+ pushUnique(rootQueries);
861
+ return chain;
862
+ }
863
+
864
+ /**
865
+ * Recursively searches the query tree for the node with the provided id and returns the
866
+ * path (root → target). Nodes are enriched with children on demand and a visited set guards
867
+ * against cycles within malformed data.
868
+ */
869
+ private async findPathToNode(
870
+ currentNode: any,
871
+ targetId: string,
872
+ visited: Set<string> = new Set<string>()
873
+ ): Promise<any[] | null> {
874
+ if (!currentNode) {
875
+ return null;
876
+ }
877
+
878
+ const currentId = currentNode.id ?? '__root__';
879
+ if (visited.has(currentId)) {
880
+ return null;
881
+ }
882
+ visited.add(currentId);
883
+
884
+ if (currentNode.id === targetId) {
885
+ return [currentNode];
886
+ }
887
+
888
+ const enrichedNode = await this.ensureQueryChildren(currentNode);
889
+ const children = enrichedNode?.children;
890
+ if (!children?.length) {
891
+ return null;
892
+ }
893
+
894
+ for (const child of children) {
895
+ const path = await this.findPathToNode(child, targetId, visited);
896
+ if (path) {
897
+ return [enrichedNode, ...path];
898
+ }
899
+ }
900
+
901
+ return null;
902
+ }
903
+
904
+ private async getDocTypeRoot(
905
+ rootQueries: any,
906
+ docTypeName: string
907
+ ): Promise<{ root: any; found: boolean }> {
908
+ if (!rootQueries) {
909
+ return { root: rootQueries, found: false };
910
+ }
911
+
912
+ const docTypeFolder = await this.findQueryFolderByName(rootQueries, docTypeName);
913
+ if (docTypeFolder) {
914
+ const folderWithChildren = await this.ensureQueryChildren(docTypeFolder);
915
+ return { root: folderWithChildren, found: true };
916
+ }
917
+
918
+ return { root: rootQueries, found: false };
919
+ }
920
+
432
921
  private async ensureQueryChildren(node: any): Promise<any> {
433
922
  if (!node || !node.hasChildren || node.children) {
434
923
  return node;
@@ -507,12 +996,12 @@ export default class TicketsDataProvider {
507
996
  // Initialize maps
508
997
  const sourceTargetsMap: Map<any, any[]> = new Map();
509
998
  const lookupMap: Map<number, any> = new Map();
510
-
999
+
511
1000
  if (workItemRelations) {
512
1001
  // Step 1: Collect all unique work item IDs that need to be fetched
513
1002
  const sourceIds = new Set<number>();
514
1003
  const targetIds = new Set<number>();
515
-
1004
+
516
1005
  for (const relation of workItemRelations) {
517
1006
  if (!relation.source) {
518
1007
  // Root link - target is actually the source
@@ -526,18 +1015,18 @@ export default class TicketsDataProvider {
526
1015
  }
527
1016
 
528
1017
  // Step 2: Fetch all work items in parallel with concurrency limit
529
- const allSourcePromises = Array.from(sourceIds).map(id =>
1018
+ const allSourcePromises = Array.from(sourceIds).map((id) =>
530
1019
  this.limit(() => {
531
- const relation = workItemRelations.find(r =>
532
- (!r.source && r.target.id === id) || (r.source?.id === id)
1020
+ const relation = workItemRelations.find(
1021
+ (r) => (!r.source && r.target.id === id) || r.source?.id === id
533
1022
  );
534
1023
  return this.fetchWIForQueryResult(relation, columnsToShowMap, columnSourceMap, true);
535
1024
  })
536
1025
  );
537
1026
 
538
- const allTargetPromises = Array.from(targetIds).map(id =>
1027
+ const allTargetPromises = Array.from(targetIds).map((id) =>
539
1028
  this.limit(() => {
540
- const relation = workItemRelations.find(r => r.target?.id === id);
1029
+ const relation = workItemRelations.find((r) => r.target?.id === id);
541
1030
  return this.fetchWIForQueryResult(relation, columnsToShowMap, columnTargetsMap, true);
542
1031
  })
543
1032
  );
@@ -545,12 +1034,12 @@ export default class TicketsDataProvider {
545
1034
  // Wait for all fetches to complete in parallel (with concurrency control)
546
1035
  const [sourceWorkItems, targetWorkItems] = await Promise.all([
547
1036
  Promise.all(allSourcePromises),
548
- Promise.all(allTargetPromises)
1037
+ Promise.all(allTargetPromises),
549
1038
  ]);
550
1039
 
551
1040
  // Build lookup maps
552
1041
  const sourceWorkItemMap = new Map<number, any>();
553
- sourceWorkItems.forEach(wi => {
1042
+ sourceWorkItems.forEach((wi) => {
554
1043
  sourceWorkItemMap.set(wi.id, wi);
555
1044
  if (!lookupMap.has(wi.id)) {
556
1045
  lookupMap.set(wi.id, wi);
@@ -558,7 +1047,7 @@ export default class TicketsDataProvider {
558
1047
  });
559
1048
 
560
1049
  const targetWorkItemMap = new Map<number, any>();
561
- targetWorkItems.forEach(wi => {
1050
+ targetWorkItems.forEach((wi) => {
562
1051
  targetWorkItemMap.set(wi.id, wi);
563
1052
  if (!lookupMap.has(wi.id)) {
564
1053
  lookupMap.set(wi.id, wi);
@@ -595,7 +1084,7 @@ export default class TicketsDataProvider {
595
1084
 
596
1085
  // In case of target is a test case
597
1086
  this.mapTestCaseToRelatedItem(targetWi, sourceWorkItem, testCaseToRelatedWiMap);
598
-
1087
+
599
1088
  const targets: any = sourceTargetsMap.get(sourceWorkItem) || [];
600
1089
  targets.push(targetWi);
601
1090
  sourceTargetsMap.set(sourceWorkItem, targets);
@@ -654,14 +1143,12 @@ export default class TicketsDataProvider {
654
1143
  // Fetch all work items in parallel with concurrency limit
655
1144
  const wiSet: Set<any> = new Set();
656
1145
  if (workItems) {
657
- const fetchPromises = workItems.map(workItem =>
658
- this.limit(() =>
659
- this.fetchWIForQueryResult(workItem, columnsToShowMap, fieldsToIncludeMap, false)
660
- )
1146
+ const fetchPromises = workItems.map((workItem) =>
1147
+ this.limit(() => this.fetchWIForQueryResult(workItem, columnsToShowMap, fieldsToIncludeMap, false))
661
1148
  );
662
-
1149
+
663
1150
  const fetchedWorkItems = await Promise.all(fetchPromises);
664
- fetchedWorkItems.forEach(wi => wiSet.add(wi));
1151
+ fetchedWorkItems.forEach((wi) => wiSet.add(wi));
665
1152
  }
666
1153
 
667
1154
  columnsToShowMap.clear();
@@ -1647,9 +2134,7 @@ export default class TicketsDataProvider {
1647
2134
  }
1648
2135
 
1649
2136
  // Get the requirement type field
1650
- const rawRequirementType: string =
1651
- fullWi.fields['Microsoft.VSTS.CMMI.RequirementType'] ||
1652
- '';
2137
+ const rawRequirementType: string = fullWi.fields['Microsoft.VSTS.CMMI.RequirementType'] || '';
1653
2138
 
1654
2139
  // Normalize and trim the requirement type
1655
2140
  const trimmedType = String(rawRequirementType).trim();