@elisra-devops/docgen-data-provider 1.62.0 → 1.63.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.
@@ -42,6 +42,7 @@ export default class TicketsDataProvider {
42
42
  * @returns linkedMomTree
43
43
  */
44
44
  private fetchLinkedMomQueries;
45
+ private hasAnyQueryTree;
45
46
  /**
46
47
  * Fetches and structures linked queries related to open PCR (Problem Change Request) tests.
47
48
  *
@@ -62,7 +63,6 @@ export default class TicketsDataProvider {
62
63
  * @returns
63
64
  */
64
65
  private fetchTestReporterQueries;
65
- private fetchAnyQueries;
66
66
  private fetchSystemRequirementQueries;
67
67
  private fetchSrsQueries;
68
68
  /**
@@ -84,6 +84,39 @@ export default class TicketsDataProvider {
84
84
  private fetchRequirementsTraceQueriesForFolder;
85
85
  private findQueryFolderByName;
86
86
  private findChildFolderByName;
87
+ /**
88
+ * Performs a breadth-first walk starting at `parent` to locate the nearest folder whose
89
+ * name matches any of the provided candidates (case-insensitive). Exact matches win; if none
90
+ * are found the first partial match encountered is returned. When no candidates are located,
91
+ * the method yields `null`.
92
+ */
93
+ private findChildFolderByPossibleNames;
94
+ /**
95
+ * Executes `fetcher` against `startingFolder` and, if the validator deems the result empty,
96
+ * climbs ancestor folders toward `rootQueries` until a satisfactory result is produced.
97
+ * The first successful folder short-circuits the search; otherwise the final attempt is
98
+ * returned to preserve legacy behavior.
99
+ */
100
+ private fetchWithAncestorFallback;
101
+ /**
102
+ * Applies `fetchWithAncestorFallback` to each configured branch, resolving dedicated folders
103
+ * when available and emitting a map keyed by branch id. Each outcome includes both the
104
+ * resulting payload and the specific folder that satisfied the fallback chain.
105
+ */
106
+ private fetchDocTypeBranches;
107
+ /**
108
+ * Constructs an ordered list of folders to probe during fallback. The sequence starts at
109
+ * `startingFolder` (if provided) and walks upward through ancestors to the root query tree,
110
+ * ensuring no folder id appears twice.
111
+ */
112
+ private buildFallbackChain;
113
+ /**
114
+ * Recursively searches the query tree for the node with the provided id and returns the
115
+ * path (root → target). Nodes are enriched with children on demand and a visited set guards
116
+ * against cycles within malformed data.
117
+ */
118
+ private findPathToNode;
119
+ private getDocTypeRoot;
87
120
  private ensureQueryChildren;
88
121
  GetQueryResultsFromWiql(wiqlHref: string | undefined, displayAsTable: boolean | undefined, testCaseToRelatedWiMap: Map<number, Set<any>>): Promise<any>;
89
122
  private parseDirectLinkedQueryResultForTableFormat;
@@ -107,6 +107,7 @@ class TicketsDataProvider {
107
107
  * @returns
108
108
  */
109
109
  async GetSharedQueries(project, path, docType = '') {
110
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
110
111
  let url;
111
112
  try {
112
113
  if (path === '')
@@ -115,22 +116,166 @@ class TicketsDataProvider {
115
116
  url = `${this.orgUrl}${project}/_apis/wit/queries/${path}?$depth=2&$expand=all`;
116
117
  let queries = await tfs_1.TFSServices.getItemContent(url, this.token);
117
118
  logger_1.default.debug(`doctype: ${docType}`);
118
- switch (docType === null || docType === void 0 ? void 0 : docType.toLowerCase()) {
119
- case 'std':
120
- const reqTestQueries = await this.fetchLinkedReqTestQueries(queries, false);
121
- const linkedMomQueries = await this.fetchLinkedMomQueries(queries);
119
+ const normalizedDocType = (docType || '').toLowerCase();
120
+ const queriesWithChildren = await this.ensureQueryChildren(queries);
121
+ switch (normalizedDocType) {
122
+ case 'std': {
123
+ const { root: stdRoot, found: stdRootFound } = await this.getDocTypeRoot(queriesWithChildren, 'std');
124
+ logger_1.default.debug(`[GetSharedQueries][std] using ${stdRootFound ? 'dedicated folder' : 'root queries'}`);
125
+ // Each branch describes the dedicated folder names, the fetch routine, and how to validate results.
126
+ const stdBranches = await this.fetchDocTypeBranches(queriesWithChildren, stdRoot, [
127
+ {
128
+ id: 'reqToTest',
129
+ label: '[GetSharedQueries][std][req-to-test]',
130
+ folderNames: [
131
+ 'requirement - test',
132
+ 'requirement to test case',
133
+ 'requirement to test',
134
+ 'req to test',
135
+ ],
136
+ fetcher: (folder) => this.fetchLinkedReqTestQueries(folder, false),
137
+ validator: (result) => this.hasAnyQueryTree(result === null || result === void 0 ? void 0 : result.reqTestTree),
138
+ },
139
+ {
140
+ id: 'testToReq',
141
+ label: '[GetSharedQueries][std][test-to-req]',
142
+ folderNames: [
143
+ 'test - requirement',
144
+ 'test to requirement',
145
+ 'test case to requirement',
146
+ 'test to req',
147
+ ],
148
+ fetcher: (folder) => this.fetchLinkedReqTestQueries(folder, true),
149
+ validator: (result) => this.hasAnyQueryTree(result === null || result === void 0 ? void 0 : result.testReqTree),
150
+ },
151
+ {
152
+ id: 'mom',
153
+ label: '[GetSharedQueries][std][mom]',
154
+ folderNames: ['linked mom', 'mom'],
155
+ fetcher: (folder) => this.fetchLinkedMomQueries(folder),
156
+ validator: (result) => this.hasAnyQueryTree(result === null || result === void 0 ? void 0 : result.linkedMomTree),
157
+ },
158
+ ]);
159
+ const reqToTestResult = stdBranches['reqToTest'];
160
+ const testToReqResult = stdBranches['testToReq'];
161
+ const momResult = stdBranches['mom'];
162
+ const reqTestQueries = {
163
+ reqTestTree: (_b = (_a = reqToTestResult === null || reqToTestResult === void 0 ? void 0 : reqToTestResult.result) === null || _a === void 0 ? void 0 : _a.reqTestTree) !== null && _b !== void 0 ? _b : null,
164
+ testReqTree: (_f = (_d = (_c = testToReqResult === null || testToReqResult === void 0 ? void 0 : testToReqResult.result) === null || _c === void 0 ? void 0 : _c.testReqTree) !== null && _d !== void 0 ? _d : (_e = reqToTestResult === null || reqToTestResult === void 0 ? void 0 : reqToTestResult.result) === null || _e === void 0 ? void 0 : _e.testReqTree) !== null && _f !== void 0 ? _f : null,
165
+ };
166
+ const linkedMomQueries = {
167
+ linkedMomTree: (_h = (_g = momResult === null || momResult === void 0 ? void 0 : momResult.result) === null || _g === void 0 ? void 0 : _g.linkedMomTree) !== null && _h !== void 0 ? _h : null,
168
+ };
122
169
  return { reqTestQueries, linkedMomQueries };
123
- case 'str':
124
- const reqTestTrees = await this.fetchLinkedReqTestQueries(queries, false);
125
- const openPcrTestTrees = await this.fetchLinkedOpenPcrTestQueries(queries, false);
170
+ }
171
+ case 'str': {
172
+ const { root: strRoot, found: strRootFound } = await this.getDocTypeRoot(queriesWithChildren, 'str');
173
+ logger_1.default.debug(`[GetSharedQueries][str] using ${strRootFound ? 'dedicated folder' : 'root queries'}`);
174
+ const strBranches = await this.fetchDocTypeBranches(queriesWithChildren, strRoot, [
175
+ {
176
+ id: 'reqToTest',
177
+ label: '[GetSharedQueries][str][req-to-test]',
178
+ folderNames: [
179
+ 'requirement - test',
180
+ 'requirement to test case',
181
+ 'requirement to test',
182
+ 'req to test',
183
+ ],
184
+ fetcher: (folder) => this.fetchLinkedReqTestQueries(folder, false),
185
+ validator: (result) => this.hasAnyQueryTree(result === null || result === void 0 ? void 0 : result.reqTestTree),
186
+ },
187
+ {
188
+ id: 'testToReq',
189
+ label: '[GetSharedQueries][str][test-to-req]',
190
+ folderNames: [
191
+ 'test - requirement',
192
+ 'test to requirement',
193
+ 'test case to requirement',
194
+ 'test to req',
195
+ ],
196
+ fetcher: (folder) => this.fetchLinkedReqTestQueries(folder, true),
197
+ validator: (result) => this.hasAnyQueryTree(result === null || result === void 0 ? void 0 : result.testReqTree),
198
+ },
199
+ {
200
+ id: 'openPcrToTest',
201
+ label: '[GetSharedQueries][str][open-pcr-to-test]',
202
+ folderNames: ['open pcr to test case', 'open pcr to test', 'open pcr - test', 'open pcr'],
203
+ fetcher: (folder) => this.fetchLinkedOpenPcrTestQueries(folder, false),
204
+ validator: (result) => this.hasAnyQueryTree(result === null || result === void 0 ? void 0 : result.OpenPcrToTestTree),
205
+ },
206
+ {
207
+ id: 'testToOpenPcr',
208
+ label: '[GetSharedQueries][str][test-to-open-pcr]',
209
+ folderNames: ['test case to open pcr', 'test to open pcr', 'test - open pcr', 'open pcr'],
210
+ fetcher: (folder) => this.fetchLinkedOpenPcrTestQueries(folder, true),
211
+ validator: (result) => this.hasAnyQueryTree(result === null || result === void 0 ? void 0 : result.TestToOpenPcrTree),
212
+ },
213
+ ]);
214
+ const strReqToTest = strBranches['reqToTest'];
215
+ const strTestToReq = strBranches['testToReq'];
216
+ const strOpenPcrToTest = strBranches['openPcrToTest'];
217
+ const strTestToOpenPcr = strBranches['testToOpenPcr'];
218
+ const reqTestTrees = {
219
+ reqTestTree: (_k = (_j = strReqToTest === null || strReqToTest === void 0 ? void 0 : strReqToTest.result) === null || _j === void 0 ? void 0 : _j.reqTestTree) !== null && _k !== void 0 ? _k : null,
220
+ testReqTree: (_p = (_m = (_l = strTestToReq === null || strTestToReq === void 0 ? void 0 : strTestToReq.result) === null || _l === void 0 ? void 0 : _l.testReqTree) !== null && _m !== void 0 ? _m : (_o = strReqToTest === null || strReqToTest === void 0 ? void 0 : strReqToTest.result) === null || _o === void 0 ? void 0 : _o.testReqTree) !== null && _p !== void 0 ? _p : null,
221
+ };
222
+ const openPcrTestTrees = {
223
+ OpenPcrToTestTree: (_r = (_q = strOpenPcrToTest === null || strOpenPcrToTest === void 0 ? void 0 : strOpenPcrToTest.result) === null || _q === void 0 ? void 0 : _q.OpenPcrToTestTree) !== null && _r !== void 0 ? _r : null,
224
+ TestToOpenPcrTree: (_v = (_t = (_s = strTestToOpenPcr === null || strTestToOpenPcr === void 0 ? void 0 : strTestToOpenPcr.result) === null || _s === void 0 ? void 0 : _s.TestToOpenPcrTree) !== null && _t !== void 0 ? _t : (_u = strOpenPcrToTest === null || strOpenPcrToTest === void 0 ? void 0 : strOpenPcrToTest.result) === null || _u === void 0 ? void 0 : _u.TestToOpenPcrTree) !== null && _v !== void 0 ? _v : null,
225
+ };
126
226
  return { reqTestTrees, openPcrTestTrees };
127
- case 'test-reporter':
128
- const testAssociatedTree = await this.fetchTestReporterQueries(queries);
129
- return { testAssociatedTree };
227
+ }
228
+ case 'test-reporter': {
229
+ const { root: testReporterRoot, found: testReporterFound } = await this.getDocTypeRoot(queriesWithChildren, 'test-reporter');
230
+ logger_1.default.debug(`[GetSharedQueries][test-reporter] using ${testReporterFound ? 'dedicated folder' : 'root queries'}`);
231
+ const testReporterBranches = await this.fetchDocTypeBranches(queriesWithChildren, testReporterRoot, [
232
+ {
233
+ id: 'testReporter',
234
+ label: '[GetSharedQueries][test-reporter]',
235
+ folderNames: ['test reporter', 'test-reporter'],
236
+ fetcher: (folder) => this.fetchTestReporterQueries(folder),
237
+ validator: (result) => this.hasAnyQueryTree(result === null || result === void 0 ? void 0 : result.testAssociatedTree),
238
+ },
239
+ ]);
240
+ const testReporterFetch = testReporterBranches['testReporter'];
241
+ return (_w = testReporterFetch === null || testReporterFetch === void 0 ? void 0 : testReporterFetch.result) !== null && _w !== void 0 ? _w : { testAssociatedTree: null };
242
+ }
130
243
  case 'srs':
131
- return await this.fetchSrsQueries(queries);
132
- case 'svd':
133
- return await this.fetchAnyQueries(queries);
244
+ return await this.fetchSrsQueries(queriesWithChildren);
245
+ case 'svd': {
246
+ const { root: svdRoot, found } = await this.getDocTypeRoot(queriesWithChildren, 'svd');
247
+ if (!found) {
248
+ logger_1.default.debug('[GetSharedQueries][svd] dedicated folder not found, using fallback tree');
249
+ }
250
+ const svdBranches = await this.fetchDocTypeBranches(queriesWithChildren, svdRoot, [
251
+ {
252
+ id: 'systemOverview',
253
+ label: '[GetSharedQueries][svd][system-overview]',
254
+ folderNames: ['system overview'],
255
+ fetcher: async (folder) => {
256
+ const { tree1 } = await this.structureAllQueryPath(folder);
257
+ return tree1;
258
+ },
259
+ validator: (result) => !!result,
260
+ },
261
+ {
262
+ id: 'knownBugs',
263
+ label: '[GetSharedQueries][svd][known-bugs]',
264
+ folderNames: ['known bugs', 'known bug'],
265
+ fetcher: async (folder) => {
266
+ const { tree2 } = await this.structureAllQueryPath(folder);
267
+ return tree2;
268
+ },
269
+ validator: (result) => !!result,
270
+ },
271
+ ]);
272
+ const systemOverviewFetch = svdBranches['systemOverview'];
273
+ const knownBugsFetch = svdBranches['knownBugs'];
274
+ return {
275
+ systemOverviewQueryTree: (_x = systemOverviewFetch === null || systemOverviewFetch === void 0 ? void 0 : systemOverviewFetch.result) !== null && _x !== void 0 ? _x : null,
276
+ knownBugsQueryTree: (_y = knownBugsFetch === null || knownBugsFetch === void 0 ? void 0 : knownBugsFetch.result) !== null && _y !== void 0 ? _y : null,
277
+ };
278
+ }
134
279
  default:
135
280
  break;
136
281
  }
@@ -205,6 +350,30 @@ class TicketsDataProvider {
205
350
  ]);
206
351
  return { linkedMomTree };
207
352
  }
353
+ hasAnyQueryTree(result) {
354
+ const inspect = (value) => {
355
+ if (!value) {
356
+ return false;
357
+ }
358
+ if (Array.isArray(value)) {
359
+ return value.some(inspect);
360
+ }
361
+ if (typeof value === 'object') {
362
+ if (value.isValidQuery || value.wiql || value.queryType) {
363
+ return true;
364
+ }
365
+ if ('roots' in value && Array.isArray(value.roots) && value.roots.length > 0) {
366
+ return true;
367
+ }
368
+ if ('children' in value && Array.isArray(value.children) && value.children.length > 0) {
369
+ return true;
370
+ }
371
+ return Object.values(value).some(inspect);
372
+ }
373
+ return false;
374
+ };
375
+ return inspect(result);
376
+ }
208
377
  /**
209
378
  * Fetches and structures linked queries related to open PCR (Problem Change Request) tests.
210
379
  *
@@ -231,10 +400,6 @@ class TicketsDataProvider {
231
400
  const { tree1: tree1, tree2: testAssociatedTree } = await this.structureFetchedQueries(queries, true, null, ['Requirement', 'Bug', 'Change Request'], ['Test Case']);
232
401
  return { testAssociatedTree };
233
402
  }
234
- async fetchAnyQueries(queries) {
235
- const { tree1: systemOverviewQueryTree, tree2: knownBugsQueryTree } = await this.structureAllQueryPath(queries);
236
- return { systemOverviewQueryTree, knownBugsQueryTree };
237
- }
238
403
  async fetchSystemRequirementQueries(queries, excludedFolderNames = []) {
239
404
  const { tree1: systemRequirementsQueryTree } = await this.structureFetchedQueries(queries, false, null, ['Epic', 'Feature', 'Requirement'], [], undefined, undefined, true, // Enable processing of both tree and direct link queries (excluding flat queries)
240
405
  excludedFolderNames);
@@ -330,6 +495,219 @@ class TicketsDataProvider {
330
495
  const normalizedName = childName.toLowerCase();
331
496
  return (parentWithChildren.children.find((child) => child.isFolder && (child.name || '').toLowerCase() === normalizedName) || null);
332
497
  }
498
+ /**
499
+ * Performs a breadth-first walk starting at `parent` to locate the nearest folder whose
500
+ * name matches any of the provided candidates (case-insensitive). Exact matches win; if none
501
+ * are found the first partial match encountered is returned. When no candidates are located,
502
+ * the method yields `null`.
503
+ */
504
+ async findChildFolderByPossibleNames(parent, possibleNames) {
505
+ var _a, _b, _c, _d;
506
+ if (!parent || !(possibleNames === null || possibleNames === void 0 ? void 0 : possibleNames.length)) {
507
+ return null;
508
+ }
509
+ const normalizedNames = possibleNames.map((name) => name.toLowerCase());
510
+ const isMatch = (candidate, value) => value === candidate;
511
+ const isPartialMatch = (candidate, value) => value.includes(candidate);
512
+ const tryMatch = (folder, matcher) => {
513
+ const folderName = ((folder === null || folder === void 0 ? void 0 : folder.name) || '').toLowerCase();
514
+ return normalizedNames.some((candidate) => matcher(candidate, folderName));
515
+ };
516
+ const parentWithChildren = await this.ensureQueryChildren(parent);
517
+ if (!((_a = parentWithChildren === null || parentWithChildren === void 0 ? void 0 : parentWithChildren.children) === null || _a === void 0 ? void 0 : _a.length)) {
518
+ return null;
519
+ }
520
+ const queue = [];
521
+ const visited = new Set();
522
+ let partialCandidate = null;
523
+ // Seed the queue with direct children so we prefer closer matches before walking deeper.
524
+ for (const child of parentWithChildren.children) {
525
+ if (!(child === null || child === void 0 ? void 0 : child.isFolder)) {
526
+ continue;
527
+ }
528
+ const childId = (_b = child.id) !== null && _b !== void 0 ? _b : `${child.name}-${Math.random()}`;
529
+ queue.push(child);
530
+ visited.add(childId);
531
+ }
532
+ const considerFolder = async (folder) => {
533
+ if (tryMatch(folder, isMatch)) {
534
+ return await this.ensureQueryChildren(folder);
535
+ }
536
+ if (!partialCandidate && tryMatch(folder, isPartialMatch)) {
537
+ partialCandidate = await this.ensureQueryChildren(folder);
538
+ }
539
+ return null;
540
+ };
541
+ for (const child of queue) {
542
+ const match = await considerFolder(child);
543
+ if (match) {
544
+ return match;
545
+ }
546
+ }
547
+ while (queue.length > 0) {
548
+ const current = queue.shift();
549
+ if (!current) {
550
+ continue;
551
+ }
552
+ const currentWithChildren = await this.ensureQueryChildren(current);
553
+ if (!currentWithChildren) {
554
+ continue;
555
+ }
556
+ const match = await considerFolder(currentWithChildren);
557
+ if (match) {
558
+ return match;
559
+ }
560
+ // Breadth-first expansion so we climb the hierarchy gradually.
561
+ if ((_c = currentWithChildren.children) === null || _c === void 0 ? void 0 : _c.length) {
562
+ for (const child of currentWithChildren.children) {
563
+ if (!(child === null || child === void 0 ? void 0 : child.isFolder)) {
564
+ continue;
565
+ }
566
+ const childId = (_d = child.id) !== null && _d !== void 0 ? _d : `${child.name}-${Math.random()}`;
567
+ if (!visited.has(childId)) {
568
+ visited.add(childId);
569
+ queue.push(child);
570
+ }
571
+ }
572
+ }
573
+ }
574
+ return partialCandidate;
575
+ }
576
+ /**
577
+ * Executes `fetcher` against `startingFolder` and, if the validator deems the result empty,
578
+ * climbs ancestor folders toward `rootQueries` until a satisfactory result is produced.
579
+ * The first successful folder short-circuits the search; otherwise the final attempt is
580
+ * returned to preserve legacy behavior.
581
+ */
582
+ async fetchWithAncestorFallback(rootQueries, startingFolder, fetcher, logContext, validator) {
583
+ var _a;
584
+ const rootWithChildren = await this.ensureQueryChildren(rootQueries);
585
+ const candidates = await this.buildFallbackChain(rootWithChildren, startingFolder);
586
+ const evaluate = validator !== null && validator !== void 0 ? validator : ((res) => this.hasAnyQueryTree(res));
587
+ let lastResult = null;
588
+ let lastFolder = startingFolder !== null && startingFolder !== void 0 ? startingFolder : rootWithChildren;
589
+ for (const candidate of candidates) {
590
+ const enrichedCandidate = await this.ensureQueryChildren(candidate);
591
+ const candidateName = (_a = enrichedCandidate === null || enrichedCandidate === void 0 ? void 0 : enrichedCandidate.name) !== null && _a !== void 0 ? _a : '<root>';
592
+ logger_1.default.debug(`${logContext} trying folder: ${candidateName}`);
593
+ lastResult = await fetcher(enrichedCandidate);
594
+ lastFolder = enrichedCandidate;
595
+ if (evaluate(lastResult)) {
596
+ logger_1.default.debug(`${logContext} using folder: ${candidateName}`);
597
+ return { result: lastResult, usedFolder: enrichedCandidate };
598
+ }
599
+ logger_1.default.debug(`${logContext} folder ${candidateName} produced no results, ascending`);
600
+ }
601
+ logger_1.default.debug(`${logContext} no folders yielded results, returning last attempt`);
602
+ return { result: lastResult, usedFolder: lastFolder };
603
+ }
604
+ /**
605
+ * Applies `fetchWithAncestorFallback` to each configured branch, resolving dedicated folders
606
+ * when available and emitting a map keyed by branch id. Each outcome includes both the
607
+ * resulting payload and the specific folder that satisfied the fallback chain.
608
+ */
609
+ async fetchDocTypeBranches(queriesWithChildren, docRoot, branches) {
610
+ var _a, _b, _c, _d, _e, _f;
611
+ const results = {};
612
+ const effectiveDocRoot = docRoot !== null && docRoot !== void 0 ? docRoot : queriesWithChildren;
613
+ for (const branch of branches) {
614
+ const fallbackStart = (_a = branch.fallbackStart) !== null && _a !== void 0 ? _a : effectiveDocRoot;
615
+ let startingFolder = fallbackStart;
616
+ let startingName = (_b = startingFolder === null || startingFolder === void 0 ? void 0 : startingFolder.name) !== null && _b !== void 0 ? _b : '<root>';
617
+ // Attempt to locate a more specific child folder, falling back to the provided root if absent.
618
+ if (((_c = branch.folderNames) === null || _c === void 0 ? void 0 : _c.length) && effectiveDocRoot) {
619
+ const resolvedFolder = await this.findChildFolderByPossibleNames(effectiveDocRoot, branch.folderNames);
620
+ if (resolvedFolder) {
621
+ startingFolder = resolvedFolder;
622
+ startingName = (_d = resolvedFolder === null || resolvedFolder === void 0 ? void 0 : resolvedFolder.name) !== null && _d !== void 0 ? _d : '<root>';
623
+ }
624
+ }
625
+ logger_1.default.debug(`${branch.label} starting folder: ${startingName}`);
626
+ const fetchOutcome = await this.fetchWithAncestorFallback(queriesWithChildren, startingFolder, branch.fetcher, branch.label, branch.validator);
627
+ logger_1.default.debug(`${branch.label} final folder: ${(_f = (_e = fetchOutcome.usedFolder) === null || _e === void 0 ? void 0 : _e.name) !== null && _f !== void 0 ? _f : '<root>'}`);
628
+ results[branch.id] = fetchOutcome;
629
+ }
630
+ return results;
631
+ }
632
+ /**
633
+ * Constructs an ordered list of folders to probe during fallback. The sequence starts at
634
+ * `startingFolder` (if provided) and walks upward through ancestors to the root query tree,
635
+ * ensuring no folder id appears twice.
636
+ */
637
+ async buildFallbackChain(rootQueries, startingFolder) {
638
+ const chain = [];
639
+ const seen = new Set();
640
+ const pushUnique = (node) => {
641
+ var _a;
642
+ if (!node) {
643
+ return;
644
+ }
645
+ const id = (_a = node.id) !== null && _a !== void 0 ? _a : '__root__';
646
+ if (seen.has(id)) {
647
+ return;
648
+ }
649
+ seen.add(id);
650
+ chain.push(node);
651
+ };
652
+ if (startingFolder === null || startingFolder === void 0 ? void 0 : startingFolder.id) {
653
+ const path = await this.findPathToNode(rootQueries, startingFolder.id);
654
+ if (path) {
655
+ for (let i = path.length - 1; i >= 0; i--) {
656
+ pushUnique(path[i]);
657
+ }
658
+ }
659
+ else {
660
+ pushUnique(startingFolder);
661
+ }
662
+ }
663
+ else if (startingFolder) {
664
+ pushUnique(startingFolder);
665
+ }
666
+ pushUnique(rootQueries);
667
+ return chain;
668
+ }
669
+ /**
670
+ * Recursively searches the query tree for the node with the provided id and returns the
671
+ * path (root → target). Nodes are enriched with children on demand and a visited set guards
672
+ * against cycles within malformed data.
673
+ */
674
+ async findPathToNode(currentNode, targetId, visited = new Set()) {
675
+ var _a;
676
+ if (!currentNode) {
677
+ return null;
678
+ }
679
+ const currentId = (_a = currentNode.id) !== null && _a !== void 0 ? _a : '__root__';
680
+ if (visited.has(currentId)) {
681
+ return null;
682
+ }
683
+ visited.add(currentId);
684
+ if (currentNode.id === targetId) {
685
+ return [currentNode];
686
+ }
687
+ const enrichedNode = await this.ensureQueryChildren(currentNode);
688
+ const children = enrichedNode === null || enrichedNode === void 0 ? void 0 : enrichedNode.children;
689
+ if (!(children === null || children === void 0 ? void 0 : children.length)) {
690
+ return null;
691
+ }
692
+ for (const child of children) {
693
+ const path = await this.findPathToNode(child, targetId, visited);
694
+ if (path) {
695
+ return [enrichedNode, ...path];
696
+ }
697
+ }
698
+ return null;
699
+ }
700
+ async getDocTypeRoot(rootQueries, docTypeName) {
701
+ if (!rootQueries) {
702
+ return { root: rootQueries, found: false };
703
+ }
704
+ const docTypeFolder = await this.findQueryFolderByName(rootQueries, docTypeName);
705
+ if (docTypeFolder) {
706
+ const folderWithChildren = await this.ensureQueryChildren(docTypeFolder);
707
+ return { root: folderWithChildren, found: true };
708
+ }
709
+ return { root: rootQueries, found: false };
710
+ }
333
711
  async ensureQueryChildren(node) {
334
712
  if (!node || !node.hasChildren || node.children) {
335
713
  return node;
@@ -410,29 +788,29 @@ class TicketsDataProvider {
410
788
  }
411
789
  }
412
790
  // Step 2: Fetch all work items in parallel with concurrency limit
413
- const allSourcePromises = Array.from(sourceIds).map(id => this.limit(() => {
414
- const relation = workItemRelations.find(r => { var _a; return (!r.source && r.target.id === id) || (((_a = r.source) === null || _a === void 0 ? void 0 : _a.id) === id); });
791
+ const allSourcePromises = Array.from(sourceIds).map((id) => this.limit(() => {
792
+ const relation = workItemRelations.find((r) => { var _a; return (!r.source && r.target.id === id) || ((_a = r.source) === null || _a === void 0 ? void 0 : _a.id) === id; });
415
793
  return this.fetchWIForQueryResult(relation, columnsToShowMap, columnSourceMap, true);
416
794
  }));
417
- const allTargetPromises = Array.from(targetIds).map(id => this.limit(() => {
418
- const relation = workItemRelations.find(r => { var _a; return ((_a = r.target) === null || _a === void 0 ? void 0 : _a.id) === id; });
795
+ const allTargetPromises = Array.from(targetIds).map((id) => this.limit(() => {
796
+ const relation = workItemRelations.find((r) => { var _a; return ((_a = r.target) === null || _a === void 0 ? void 0 : _a.id) === id; });
419
797
  return this.fetchWIForQueryResult(relation, columnsToShowMap, columnTargetsMap, true);
420
798
  }));
421
799
  // Wait for all fetches to complete in parallel (with concurrency control)
422
800
  const [sourceWorkItems, targetWorkItems] = await Promise.all([
423
801
  Promise.all(allSourcePromises),
424
- Promise.all(allTargetPromises)
802
+ Promise.all(allTargetPromises),
425
803
  ]);
426
804
  // Build lookup maps
427
805
  const sourceWorkItemMap = new Map();
428
- sourceWorkItems.forEach(wi => {
806
+ sourceWorkItems.forEach((wi) => {
429
807
  sourceWorkItemMap.set(wi.id, wi);
430
808
  if (!lookupMap.has(wi.id)) {
431
809
  lookupMap.set(wi.id, wi);
432
810
  }
433
811
  });
434
812
  const targetWorkItemMap = new Map();
435
- targetWorkItems.forEach(wi => {
813
+ targetWorkItems.forEach((wi) => {
436
814
  targetWorkItemMap.set(wi.id, wi);
437
815
  if (!lookupMap.has(wi.id)) {
438
816
  lookupMap.set(wi.id, wi);
@@ -510,9 +888,9 @@ class TicketsDataProvider {
510
888
  // Fetch all work items in parallel with concurrency limit
511
889
  const wiSet = new Set();
512
890
  if (workItems) {
513
- const fetchPromises = workItems.map(workItem => this.limit(() => this.fetchWIForQueryResult(workItem, columnsToShowMap, fieldsToIncludeMap, false)));
891
+ const fetchPromises = workItems.map((workItem) => this.limit(() => this.fetchWIForQueryResult(workItem, columnsToShowMap, fieldsToIncludeMap, false)));
514
892
  const fetchedWorkItems = await Promise.all(fetchPromises);
515
- fetchedWorkItems.forEach(wi => wiSet.add(wi));
893
+ fetchedWorkItems.forEach((wi) => wiSet.add(wi));
516
894
  }
517
895
  columnsToShowMap.clear();
518
896
  return {
@@ -1342,8 +1720,7 @@ class TicketsDataProvider {
1342
1720
  continue; // Skip non-requirement work items
1343
1721
  }
1344
1722
  // Get the requirement type field
1345
- const rawRequirementType = fullWi.fields['Microsoft.VSTS.CMMI.RequirementType'] ||
1346
- '';
1723
+ const rawRequirementType = fullWi.fields['Microsoft.VSTS.CMMI.RequirementType'] || '';
1347
1724
  // Normalize and trim the requirement type
1348
1725
  const trimmedType = String(rawRequirementType).trim();
1349
1726
  // Map to the standard category header