@browserflow-ai/exploration 0.0.6
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/dist/adapters/claude-cli.d.ts +57 -0
- package/dist/adapters/claude-cli.d.ts.map +1 -0
- package/dist/adapters/claude-cli.js +195 -0
- package/dist/adapters/claude-cli.js.map +1 -0
- package/dist/adapters/claude.d.ts +54 -0
- package/dist/adapters/claude.d.ts.map +1 -0
- package/dist/adapters/claude.js +160 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/index.d.ts +6 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +4 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/types.d.ts +196 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +3 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/agent-browser-session.d.ts +62 -0
- package/dist/agent-browser-session.d.ts.map +1 -0
- package/dist/agent-browser-session.js +272 -0
- package/dist/agent-browser-session.js.map +1 -0
- package/dist/evidence.d.ts +111 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +144 -0
- package/dist/evidence.js.map +1 -0
- package/dist/explorer.d.ts +180 -0
- package/dist/explorer.d.ts.map +1 -0
- package/dist/explorer.js +393 -0
- package/dist/explorer.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/locator-candidates.d.ts +127 -0
- package/dist/locator-candidates.d.ts.map +1 -0
- package/dist/locator-candidates.js +358 -0
- package/dist/locator-candidates.js.map +1 -0
- package/dist/step-executor.d.ts +99 -0
- package/dist/step-executor.d.ts.map +1 -0
- package/dist/step-executor.js +646 -0
- package/dist/step-executor.js.map +1 -0
- package/package.json +34 -0
- package/src/adapters/claude-cli.test.ts +134 -0
- package/src/adapters/claude-cli.ts +240 -0
- package/src/adapters/claude.test.ts +195 -0
- package/src/adapters/claude.ts +190 -0
- package/src/adapters/index.ts +21 -0
- package/src/adapters/types.ts +207 -0
- package/src/agent-browser-session.test.ts +369 -0
- package/src/agent-browser-session.ts +349 -0
- package/src/evidence.test.ts +239 -0
- package/src/evidence.ts +203 -0
- package/src/explorer.test.ts +321 -0
- package/src/explorer.ts +565 -0
- package/src/index.ts +51 -0
- package/src/locator-candidates.test.ts +602 -0
- package/src/locator-candidates.ts +441 -0
- package/src/step-executor.test.ts +696 -0
- package/src/step-executor.ts +783 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
// @browserflow-ai/exploration - Locator candidate generation
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A locator candidate with confidence score
|
|
5
|
+
*/
|
|
6
|
+
export interface LocatorCandidate {
|
|
7
|
+
locator: string;
|
|
8
|
+
type: 'ref' | 'css' | 'xpath' | 'text' | 'role' | 'testid';
|
|
9
|
+
confidence: number;
|
|
10
|
+
description?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Element information from browser snapshot
|
|
15
|
+
*/
|
|
16
|
+
export interface ElementInfo {
|
|
17
|
+
ref: string;
|
|
18
|
+
tag: string;
|
|
19
|
+
role?: string;
|
|
20
|
+
text?: string;
|
|
21
|
+
ariaLabel?: string;
|
|
22
|
+
testId?: string;
|
|
23
|
+
className?: string;
|
|
24
|
+
id?: string;
|
|
25
|
+
attributes?: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Configuration for the locator generator
|
|
30
|
+
*/
|
|
31
|
+
export interface LocatorCandidateGeneratorConfig {
|
|
32
|
+
preferredStrategies?: ('ref' | 'testid' | 'role' | 'text' | 'css' | 'xpath')[];
|
|
33
|
+
maxCandidates?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Implicit ARIA roles for common HTML elements
|
|
38
|
+
*/
|
|
39
|
+
const IMPLICIT_ROLES: Record<string, string> = {
|
|
40
|
+
a: 'link',
|
|
41
|
+
button: 'button',
|
|
42
|
+
input: 'textbox',
|
|
43
|
+
select: 'combobox',
|
|
44
|
+
textarea: 'textbox',
|
|
45
|
+
img: 'img',
|
|
46
|
+
nav: 'navigation',
|
|
47
|
+
main: 'main',
|
|
48
|
+
header: 'banner',
|
|
49
|
+
footer: 'contentinfo',
|
|
50
|
+
aside: 'complementary',
|
|
51
|
+
article: 'article',
|
|
52
|
+
section: 'region',
|
|
53
|
+
form: 'form',
|
|
54
|
+
table: 'table',
|
|
55
|
+
ul: 'list',
|
|
56
|
+
ol: 'list',
|
|
57
|
+
li: 'listitem',
|
|
58
|
+
dialog: 'dialog',
|
|
59
|
+
menu: 'menu',
|
|
60
|
+
option: 'option',
|
|
61
|
+
progress: 'progressbar',
|
|
62
|
+
meter: 'meter',
|
|
63
|
+
h1: 'heading',
|
|
64
|
+
h2: 'heading',
|
|
65
|
+
h3: 'heading',
|
|
66
|
+
h4: 'heading',
|
|
67
|
+
h5: 'heading',
|
|
68
|
+
h6: 'heading',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Maximum text length for text-based locators
|
|
73
|
+
*/
|
|
74
|
+
const MAX_TEXT_LENGTH = 50;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* LocatorCandidateGenerator - Generates ranked locator options for elements
|
|
78
|
+
*
|
|
79
|
+
* Uses multiple strategies to find elements:
|
|
80
|
+
* - Element refs from agent-browser snapshots
|
|
81
|
+
* - Test IDs (data-testid, data-test, etc.)
|
|
82
|
+
* - ARIA roles and labels
|
|
83
|
+
* - Text content matching
|
|
84
|
+
* - CSS selectors
|
|
85
|
+
* - XPath expressions
|
|
86
|
+
*/
|
|
87
|
+
export class LocatorCandidateGenerator {
|
|
88
|
+
private preferredStrategies: string[];
|
|
89
|
+
private maxCandidates: number;
|
|
90
|
+
|
|
91
|
+
constructor(config: LocatorCandidateGeneratorConfig = {}) {
|
|
92
|
+
this.preferredStrategies = config.preferredStrategies ?? [
|
|
93
|
+
'ref',
|
|
94
|
+
'testid',
|
|
95
|
+
'role',
|
|
96
|
+
'text',
|
|
97
|
+
'css',
|
|
98
|
+
];
|
|
99
|
+
this.maxCandidates = config.maxCandidates ?? 5;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Generate locator candidates for an element based on natural language query
|
|
104
|
+
*
|
|
105
|
+
* @param query - Natural language description (e.g., "Submit button in the form")
|
|
106
|
+
* @param snapshot - Browser snapshot containing element refs
|
|
107
|
+
* @returns Promise resolving to ranked list of locator strings
|
|
108
|
+
*/
|
|
109
|
+
async generateCandidates(
|
|
110
|
+
query: string,
|
|
111
|
+
snapshot: Record<string, unknown>
|
|
112
|
+
): Promise<string[]> {
|
|
113
|
+
const candidates = await this.findCandidates(query, snapshot);
|
|
114
|
+
return candidates.slice(0, this.maxCandidates).map((c) => c.locator);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Generate detailed locator candidates with metadata
|
|
119
|
+
*
|
|
120
|
+
* @param query - Natural language description
|
|
121
|
+
* @param snapshot - Browser snapshot
|
|
122
|
+
* @returns Promise resolving to ranked list of candidates with scores
|
|
123
|
+
*/
|
|
124
|
+
async generateDetailedCandidates(
|
|
125
|
+
query: string,
|
|
126
|
+
snapshot: Record<string, unknown>
|
|
127
|
+
): Promise<LocatorCandidate[]> {
|
|
128
|
+
const candidates = await this.findCandidates(query, snapshot);
|
|
129
|
+
return candidates.slice(0, this.maxCandidates);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Generate all applicable locator candidates for a known element
|
|
134
|
+
*
|
|
135
|
+
* @param element - Element information
|
|
136
|
+
* @returns Promise resolving to ranked list of candidates
|
|
137
|
+
*/
|
|
138
|
+
async generateCandidatesForElement(element: ElementInfo): Promise<LocatorCandidate[]> {
|
|
139
|
+
const candidates: LocatorCandidate[] = [];
|
|
140
|
+
|
|
141
|
+
// 1. Ref locator (highest confidence - direct reference)
|
|
142
|
+
candidates.push(this.generateRefLocator(element.ref));
|
|
143
|
+
|
|
144
|
+
// 2. Test ID locator (if present)
|
|
145
|
+
const testId = element.testId || element.attributes?.['data-testid'] || element.attributes?.['data-test'];
|
|
146
|
+
if (testId) {
|
|
147
|
+
candidates.push(this.generateTestIdLocator(testId));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 3. Role locator (explicit or implicit)
|
|
151
|
+
const role = element.role || IMPLICIT_ROLES[element.tag];
|
|
152
|
+
if (role) {
|
|
153
|
+
const name = element.ariaLabel || (element.text && element.text.length <= MAX_TEXT_LENGTH ? element.text : undefined);
|
|
154
|
+
candidates.push(this.generateRoleLocator(role, name));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 4. Text locator (if text is short enough)
|
|
158
|
+
if (element.text && element.text.length > 0 && element.text.length <= MAX_TEXT_LENGTH) {
|
|
159
|
+
// Use exact match for shorter text
|
|
160
|
+
const exact = element.text.length <= 30;
|
|
161
|
+
candidates.push(this.generateTextLocator(element.text, exact));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 5. CSS selector (always as fallback)
|
|
165
|
+
const cssSelector = this.generateCssSelectorForElement(element);
|
|
166
|
+
candidates.push({
|
|
167
|
+
locator: cssSelector,
|
|
168
|
+
type: 'css',
|
|
169
|
+
confidence: this.calculateCssConfidence(element),
|
|
170
|
+
description: 'CSS selector',
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Sort by confidence (highest first)
|
|
174
|
+
return candidates.sort((a, b) => b.confidence - a.confidence);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Find candidate locators matching a query
|
|
179
|
+
*/
|
|
180
|
+
private async findCandidates(
|
|
181
|
+
query: string,
|
|
182
|
+
snapshot: Record<string, unknown>
|
|
183
|
+
): Promise<LocatorCandidate[]> {
|
|
184
|
+
// Handle empty query
|
|
185
|
+
if (!query || query.trim() === '') {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const normalizedQuery = query.toLowerCase().trim();
|
|
190
|
+
const refs = (snapshot.refs || {}) as Record<string, Record<string, unknown>>;
|
|
191
|
+
|
|
192
|
+
// Handle empty snapshot
|
|
193
|
+
if (Object.keys(refs).length === 0) {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Find matching elements
|
|
198
|
+
const matchedElements: Array<{ element: ElementInfo; score: number }> = [];
|
|
199
|
+
|
|
200
|
+
for (const [ref, refData] of Object.entries(refs)) {
|
|
201
|
+
const element = this.parseElementFromRef(ref, refData);
|
|
202
|
+
const matchScore = this.calculateMatchScore(element, normalizedQuery);
|
|
203
|
+
|
|
204
|
+
if (matchScore > 0) {
|
|
205
|
+
matchedElements.push({ element, score: matchScore });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Sort by match score (best match first)
|
|
210
|
+
matchedElements.sort((a, b) => b.score - a.score);
|
|
211
|
+
|
|
212
|
+
// Take the best matching element and generate candidates for it
|
|
213
|
+
if (matchedElements.length === 0) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const bestMatch = matchedElements[0];
|
|
218
|
+
return this.generateCandidatesForElement(bestMatch.element);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Parse element info from snapshot ref data
|
|
223
|
+
*/
|
|
224
|
+
private parseElementFromRef(ref: string, refData: Record<string, unknown>): ElementInfo {
|
|
225
|
+
return {
|
|
226
|
+
ref,
|
|
227
|
+
tag: (refData.tag as string) || 'div',
|
|
228
|
+
role: refData.role as string | undefined,
|
|
229
|
+
text: refData.text as string | undefined,
|
|
230
|
+
ariaLabel: refData.ariaLabel as string | undefined,
|
|
231
|
+
testId: refData.testId as string | undefined,
|
|
232
|
+
className: refData.className as string | undefined,
|
|
233
|
+
id: refData.id as string | undefined,
|
|
234
|
+
attributes: refData.attributes as Record<string, string> | undefined,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Calculate how well an element matches the query
|
|
240
|
+
* Returns a score from 0 (no match) to 1 (perfect match)
|
|
241
|
+
*/
|
|
242
|
+
private calculateMatchScore(element: ElementInfo, normalizedQuery: string): number {
|
|
243
|
+
let score = 0;
|
|
244
|
+
const queryTerms = normalizedQuery.split(/\s+/);
|
|
245
|
+
|
|
246
|
+
// Match by text content
|
|
247
|
+
if (element.text) {
|
|
248
|
+
const normalizedText = element.text.toLowerCase();
|
|
249
|
+
if (normalizedText === normalizedQuery) {
|
|
250
|
+
score += 1.0; // Exact match
|
|
251
|
+
} else if (normalizedText.includes(normalizedQuery)) {
|
|
252
|
+
score += 0.8; // Contains query
|
|
253
|
+
} else {
|
|
254
|
+
// Check if all query terms are in the text
|
|
255
|
+
const matchedTerms = queryTerms.filter((term) => normalizedText.includes(term));
|
|
256
|
+
score += (matchedTerms.length / queryTerms.length) * 0.6;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Match by aria-label
|
|
261
|
+
if (element.ariaLabel) {
|
|
262
|
+
const normalizedLabel = element.ariaLabel.toLowerCase();
|
|
263
|
+
if (normalizedLabel === normalizedQuery) {
|
|
264
|
+
score += 0.9;
|
|
265
|
+
} else if (normalizedLabel.includes(normalizedQuery)) {
|
|
266
|
+
score += 0.7;
|
|
267
|
+
} else {
|
|
268
|
+
const matchedTerms = queryTerms.filter((term) => normalizedLabel.includes(term));
|
|
269
|
+
score += (matchedTerms.length / queryTerms.length) * 0.5;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Match by tag name
|
|
274
|
+
const normalizedTag = element.tag.toLowerCase();
|
|
275
|
+
if (queryTerms.includes(normalizedTag)) {
|
|
276
|
+
score += 0.4;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Match by role
|
|
280
|
+
if (element.role) {
|
|
281
|
+
const normalizedRole = element.role.toLowerCase();
|
|
282
|
+
if (queryTerms.includes(normalizedRole)) {
|
|
283
|
+
score += 0.5;
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
// Check implicit role
|
|
287
|
+
const implicitRole = IMPLICIT_ROLES[element.tag];
|
|
288
|
+
if (implicitRole && queryTerms.includes(implicitRole)) {
|
|
289
|
+
score += 0.4;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Match by test ID
|
|
294
|
+
if (element.testId) {
|
|
295
|
+
const normalizedTestId = element.testId.toLowerCase().replace(/[-_]/g, ' ');
|
|
296
|
+
const matchedTerms = queryTerms.filter((term) => normalizedTestId.includes(term));
|
|
297
|
+
score += (matchedTerms.length / queryTerms.length) * 0.3;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Match by class name
|
|
301
|
+
if (element.className) {
|
|
302
|
+
const normalizedClasses = element.className.toLowerCase().replace(/[-_]/g, ' ');
|
|
303
|
+
const matchedTerms = queryTerms.filter((term) => normalizedClasses.includes(term));
|
|
304
|
+
score += (matchedTerms.length / queryTerms.length) * 0.2;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return Math.min(score, 1.0); // Cap at 1.0
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Generate a CSS selector for an element
|
|
312
|
+
*/
|
|
313
|
+
private generateCssSelectorForElement(element: ElementInfo): string {
|
|
314
|
+
// Prefer ID if available (most specific)
|
|
315
|
+
if (element.id) {
|
|
316
|
+
return `#${this.escapeCssSelector(element.id)}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Build selector from tag and classes
|
|
320
|
+
let selector = element.tag;
|
|
321
|
+
|
|
322
|
+
if (element.className) {
|
|
323
|
+
const classes = element.className.split(/\s+/).filter((c) => c.length > 0);
|
|
324
|
+
if (classes.length > 0) {
|
|
325
|
+
// Use first 2 classes for specificity without being too brittle
|
|
326
|
+
const selectedClasses = classes.slice(0, 2);
|
|
327
|
+
selector += selectedClasses.map((c) => `.${this.escapeCssSelector(c)}`).join('');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return selector;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Escape special characters in CSS selectors
|
|
336
|
+
*/
|
|
337
|
+
private escapeCssSelector(value: string): string {
|
|
338
|
+
return value.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Calculate CSS selector confidence based on element attributes
|
|
343
|
+
*/
|
|
344
|
+
private calculateCssConfidence(element: ElementInfo): number {
|
|
345
|
+
// ID-based selectors are more stable
|
|
346
|
+
if (element.id) {
|
|
347
|
+
return 0.75;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Class-based selectors are moderately stable
|
|
351
|
+
if (element.className) {
|
|
352
|
+
return 0.6;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Tag-only selectors are least stable
|
|
356
|
+
return 0.4;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Generate a locator from element ref
|
|
361
|
+
*/
|
|
362
|
+
generateRefLocator(ref: string): LocatorCandidate {
|
|
363
|
+
return {
|
|
364
|
+
locator: `@${ref}`,
|
|
365
|
+
type: 'ref',
|
|
366
|
+
confidence: 1.0,
|
|
367
|
+
description: 'Element reference from snapshot',
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Generate a locator from test ID
|
|
373
|
+
*/
|
|
374
|
+
generateTestIdLocator(testId: string): LocatorCandidate {
|
|
375
|
+
return {
|
|
376
|
+
locator: `[data-testid="${testId}"]`,
|
|
377
|
+
type: 'testid',
|
|
378
|
+
confidence: 0.95,
|
|
379
|
+
description: 'Test ID selector',
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Generate a locator from ARIA role
|
|
385
|
+
*/
|
|
386
|
+
generateRoleLocator(role: string, name?: string): LocatorCandidate {
|
|
387
|
+
const locator = name ? `role=${role}[name="${name}"]` : `role=${role}`;
|
|
388
|
+
return {
|
|
389
|
+
locator,
|
|
390
|
+
type: 'role',
|
|
391
|
+
confidence: name ? 0.9 : 0.85,
|
|
392
|
+
description: `ARIA role: ${role}${name ? ` with name "${name}"` : ''}`,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Generate a locator from text content
|
|
398
|
+
*/
|
|
399
|
+
generateTextLocator(text: string, exact: boolean = false): LocatorCandidate {
|
|
400
|
+
const locator = exact ? `text="${text}"` : `text~="${text}"`;
|
|
401
|
+
return {
|
|
402
|
+
locator,
|
|
403
|
+
type: 'text',
|
|
404
|
+
confidence: exact ? 0.85 : 0.7,
|
|
405
|
+
description: `Text content${exact ? ' (exact)' : ' (partial)'}`,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Generate a CSS selector locator
|
|
411
|
+
*/
|
|
412
|
+
generateCssLocator(selector: string): LocatorCandidate {
|
|
413
|
+
return {
|
|
414
|
+
locator: selector,
|
|
415
|
+
type: 'css',
|
|
416
|
+
confidence: 0.8,
|
|
417
|
+
description: 'CSS selector',
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Get the maximum number of candidates to return
|
|
423
|
+
*/
|
|
424
|
+
getMaxCandidates(): number {
|
|
425
|
+
return this.maxCandidates;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Set the maximum number of candidates to return
|
|
430
|
+
*/
|
|
431
|
+
setMaxCandidates(max: number): void {
|
|
432
|
+
this.maxCandidates = max;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Get the preferred strategies in order
|
|
437
|
+
*/
|
|
438
|
+
getPreferredStrategies(): string[] {
|
|
439
|
+
return [...this.preferredStrategies];
|
|
440
|
+
}
|
|
441
|
+
}
|