explorbot 0.1.20 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/explorbot-cli.ts +54 -25
- package/boat/doc-collector/src/ai/documentarian.ts +259 -84
- package/boat/doc-collector/src/ai/tools.ts +544 -0
- package/boat/doc-collector/src/cli.ts +1 -0
- package/boat/doc-collector/src/config.ts +2 -0
- package/boat/doc-collector/src/docbot.ts +64 -5
- package/boat/doc-collector/src/docs-renderer.ts +56 -2
- package/dist/bin/explorbot-cli.js +30 -4
- package/dist/boat/doc-collector/src/ai/documentarian.js +220 -71
- package/dist/boat/doc-collector/src/ai/tools.js +415 -0
- package/dist/boat/doc-collector/src/cli.js +1 -0
- package/dist/boat/doc-collector/src/config.js +1 -0
- package/dist/boat/doc-collector/src/docbot.js +57 -5
- package/dist/boat/doc-collector/src/docs-renderer.js +46 -0
- package/dist/package.json +3 -3
- package/dist/src/action-result.js +12 -0
- package/dist/src/ai/navigator.js +179 -43
- package/dist/src/ai/pilot.js +6 -0
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/init-command.js +9 -0
- package/dist/src/commands/plans-command.js +83 -0
- package/dist/src/commands/test-command.js +14 -8
- package/dist/src/playwright-recorder.js +19 -5
- package/dist/src/reporter.js +5 -7
- package/package.json +3 -3
- package/src/action-result.ts +11 -0
- package/src/ai/navigator.ts +183 -46
- package/src/ai/pilot.ts +6 -0
- package/src/commands/index.ts +2 -0
- package/src/commands/init-command.ts +9 -0
- package/src/commands/plans-command.ts +99 -0
- package/src/commands/test-command.ts +15 -8
- package/src/config.ts +2 -0
- package/src/playwright-recorder.ts +19 -5
- package/src/reporter.ts +4 -7
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { parseResearchSections } from "../../../../src/ai/researcher/parser.js";
|
|
2
|
+
const MAX_PRIMARY_CANDIDATES = 3;
|
|
3
|
+
const MAX_INTERACTIONS = 5;
|
|
4
|
+
const MAX_LINKS = 15;
|
|
5
|
+
const DEFAULT_WAIT_MS = 700;
|
|
6
|
+
const TAB_WAIT_MS = 500;
|
|
7
|
+
export async function collectDocInteractions(explorer, state, research) {
|
|
8
|
+
const sections = parseResearchSections(research);
|
|
9
|
+
const transitions = [];
|
|
10
|
+
const tabGroup = findTabGroup(sections);
|
|
11
|
+
if (tabGroup) {
|
|
12
|
+
transitions.push(...(await exploreTabGroup(explorer, tabGroup, state.url)));
|
|
13
|
+
}
|
|
14
|
+
for (const candidate of findActionCandidates(sections)) {
|
|
15
|
+
if (transitions.length >= MAX_INTERACTIONS) {
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
const transition = await executeInteraction(explorer, candidate, state.url, DEFAULT_WAIT_MS);
|
|
19
|
+
if (!transition) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
transitions.push(transition);
|
|
23
|
+
}
|
|
24
|
+
return transitions;
|
|
25
|
+
}
|
|
26
|
+
export function pickDocActionCandidates(research) {
|
|
27
|
+
return findActionCandidates(parseResearchSections(research)).map((candidate) => ({
|
|
28
|
+
label: candidate.element.name.trim(),
|
|
29
|
+
role: candidate.role,
|
|
30
|
+
section: candidate.sectionName,
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
async function exploreTabGroup(explorer, tabGroup, restoreUrl) {
|
|
34
|
+
const transitions = [];
|
|
35
|
+
for (const element of tabGroup.elements) {
|
|
36
|
+
const transition = await executeInteraction(explorer, {
|
|
37
|
+
element,
|
|
38
|
+
container: tabGroup.container,
|
|
39
|
+
role: 'tab',
|
|
40
|
+
sectionName: tabGroup.sectionName,
|
|
41
|
+
}, restoreUrl, TAB_WAIT_MS);
|
|
42
|
+
if (!transition) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
transitions.push(transition);
|
|
46
|
+
}
|
|
47
|
+
await restoreInteractionState(explorer, restoreUrl, buildPrimaryCommand(tabGroup.elements[0], tabGroup.container));
|
|
48
|
+
return transitions;
|
|
49
|
+
}
|
|
50
|
+
async function executeInteraction(explorer, candidate, restoreUrl, waitMs) {
|
|
51
|
+
const beforeState = explorer.getStateManager().getCurrentState();
|
|
52
|
+
if (!beforeState) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const executed = await attemptInteraction(explorer, candidate);
|
|
56
|
+
if (!executed) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
await wait(waitMs);
|
|
60
|
+
const afterState = explorer.getStateManager().getCurrentState();
|
|
61
|
+
if (!afterState) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const ariaChanges = countAriaChanges(beforeState.ariaSnapshot || '', afterState.ariaSnapshot || '');
|
|
65
|
+
const urlChanged = beforeState.url !== afterState.url;
|
|
66
|
+
const transition = buildTransition(candidate, beforeState, afterState, {
|
|
67
|
+
urlChanged,
|
|
68
|
+
newElements: ariaChanges.newCount,
|
|
69
|
+
removedElements: ariaChanges.removedCount,
|
|
70
|
+
});
|
|
71
|
+
if (urlChanged) {
|
|
72
|
+
await restoreInteractionState(explorer, restoreUrl);
|
|
73
|
+
}
|
|
74
|
+
return transition;
|
|
75
|
+
}
|
|
76
|
+
async function attemptInteraction(explorer, candidate) {
|
|
77
|
+
const action = explorer.createAction();
|
|
78
|
+
for (const command of buildClickCommands(candidate.element, candidate.container)) {
|
|
79
|
+
const success = await action.attempt(command, buildPurpose(candidate), false);
|
|
80
|
+
if (success) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
async function restoreInteractionState(explorer, restoreUrl, primaryCommand) {
|
|
87
|
+
if (primaryCommand) {
|
|
88
|
+
const action = explorer.createAction();
|
|
89
|
+
const restored = await action.attempt(primaryCommand, `Restore initial state on ${restoreUrl}`, false);
|
|
90
|
+
if (restored) {
|
|
91
|
+
await wait(TAB_WAIT_MS);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const action = explorer.createAction();
|
|
96
|
+
await action.attempt(`I.amOnPage(${JSON.stringify(restoreUrl)})`, `Restore page ${restoreUrl}`, false);
|
|
97
|
+
}
|
|
98
|
+
function buildTransition(candidate, beforeState, afterState, changes) {
|
|
99
|
+
const transition = {
|
|
100
|
+
action: describeAction(candidate),
|
|
101
|
+
before: summarizeInteractiveState(beforeState),
|
|
102
|
+
after: summarizeInteractiveState(afterState),
|
|
103
|
+
discoveredUrls: collectLinks(afterState).map((link) => link.url),
|
|
104
|
+
newCapabilities: collectDiscoveryNotes(afterState, changes),
|
|
105
|
+
element: buildInteractionElement(candidate),
|
|
106
|
+
changes,
|
|
107
|
+
};
|
|
108
|
+
if (changes.urlChanged) {
|
|
109
|
+
transition.targetUrl = afterState.url;
|
|
110
|
+
}
|
|
111
|
+
return transition;
|
|
112
|
+
}
|
|
113
|
+
function buildInteractionElement(candidate) {
|
|
114
|
+
const element = {
|
|
115
|
+
role: candidate.role,
|
|
116
|
+
name: candidate.element.name.trim(),
|
|
117
|
+
section: candidate.sectionName,
|
|
118
|
+
};
|
|
119
|
+
if (candidate.container) {
|
|
120
|
+
element.container = candidate.container;
|
|
121
|
+
}
|
|
122
|
+
if (candidate.element.css || candidate.element.xpath) {
|
|
123
|
+
element.locator = candidate.element.css || candidate.element.xpath || undefined;
|
|
124
|
+
}
|
|
125
|
+
return element;
|
|
126
|
+
}
|
|
127
|
+
function collectDiscoveryNotes(state, changes) {
|
|
128
|
+
const notes = [];
|
|
129
|
+
const headings = collectHeadings(state);
|
|
130
|
+
const links = collectLinks(state);
|
|
131
|
+
if (changes.urlChanged) {
|
|
132
|
+
notes.push('URL changed after interaction');
|
|
133
|
+
}
|
|
134
|
+
if (changes.newElements > 0) {
|
|
135
|
+
notes.push(`ARIA snapshot gained ${changes.newElements} elements`);
|
|
136
|
+
}
|
|
137
|
+
if (changes.removedElements > 0) {
|
|
138
|
+
notes.push(`ARIA snapshot removed ${changes.removedElements} elements`);
|
|
139
|
+
}
|
|
140
|
+
if (headings.length > 0) {
|
|
141
|
+
notes.push(`Visible headings after interaction: ${headings.slice(0, 3).join(' | ')}`);
|
|
142
|
+
}
|
|
143
|
+
if (links.length > 0) {
|
|
144
|
+
notes.push(`Visible links after interaction: ${Math.min(links.length, MAX_LINKS)}`);
|
|
145
|
+
}
|
|
146
|
+
return notes;
|
|
147
|
+
}
|
|
148
|
+
function findTabGroup(sections) {
|
|
149
|
+
for (const section of sections) {
|
|
150
|
+
const sectionName = section.name.toLowerCase();
|
|
151
|
+
const container = section.containerCss?.toLowerCase() || '';
|
|
152
|
+
if (isOverlaySection(sectionName, container)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const elements = section.elements.filter((element) => getElementRole(element) === 'tab');
|
|
156
|
+
if (elements.length < 2 || elements.length > 6) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
elements,
|
|
161
|
+
container: section.containerCss || undefined,
|
|
162
|
+
sectionName: section.name,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
function findActionCandidates(sections) {
|
|
168
|
+
const candidates = [];
|
|
169
|
+
const seen = new Set();
|
|
170
|
+
const navigationLabels = collectNavigationLabels(sections);
|
|
171
|
+
for (const section of sections) {
|
|
172
|
+
const sectionName = section.name.toLowerCase();
|
|
173
|
+
const container = section.containerCss?.toLowerCase() || '';
|
|
174
|
+
if (isOverlaySection(sectionName, container)) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (isNavigationSection(sectionName)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
for (const element of section.elements) {
|
|
181
|
+
const candidate = toInteractionCandidate(element, section.name, section.containerCss, navigationLabels);
|
|
182
|
+
if (!candidate) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const key = `${candidate.role}:${normalizeCandidateLabel(candidate.element.name)}`;
|
|
186
|
+
if (seen.has(key)) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
seen.add(key);
|
|
190
|
+
candidates.push(candidate);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return candidates.sort((a, b) => scoreCandidate(b) - scoreCandidate(a)).slice(0, MAX_PRIMARY_CANDIDATES);
|
|
194
|
+
}
|
|
195
|
+
function toInteractionCandidate(element, sectionName, container, navigationLabels) {
|
|
196
|
+
const role = getElementRole(element);
|
|
197
|
+
if (role !== 'link' && role !== 'button' && role !== 'tab') {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
if (!hasUsableName(element)) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
if (isShellLocator(element.css) || isShellLocator(element.xpath) || isShellLocator(container)) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
if (role === 'link' && navigationLabels.has(normalizeCandidateLabel(element.name))) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
element,
|
|
211
|
+
container: container || undefined,
|
|
212
|
+
role,
|
|
213
|
+
sectionName,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function buildClickCommands(element, container) {
|
|
217
|
+
const commands = [];
|
|
218
|
+
if (element.css) {
|
|
219
|
+
if (container && !element.css.startsWith(container)) {
|
|
220
|
+
commands.push(`I.click(${JSON.stringify(element.css)}, ${JSON.stringify(container)})`);
|
|
221
|
+
}
|
|
222
|
+
commands.push(`I.click(${JSON.stringify(element.css)})`);
|
|
223
|
+
}
|
|
224
|
+
if (element.aria) {
|
|
225
|
+
if (container) {
|
|
226
|
+
commands.push(`I.click(${JSON.stringify(element.aria)}, ${JSON.stringify(container)})`);
|
|
227
|
+
}
|
|
228
|
+
commands.push(`I.click(${JSON.stringify(element.aria)})`);
|
|
229
|
+
}
|
|
230
|
+
const xpath = buildXPathLocator(element);
|
|
231
|
+
if (xpath) {
|
|
232
|
+
commands.push(`I.click(${JSON.stringify(xpath)})`);
|
|
233
|
+
}
|
|
234
|
+
return [...new Set(commands)];
|
|
235
|
+
}
|
|
236
|
+
function buildPrimaryCommand(element, container) {
|
|
237
|
+
return buildClickCommands(element, container)[0] || null;
|
|
238
|
+
}
|
|
239
|
+
function buildXPathLocator(element) {
|
|
240
|
+
if (!element.name) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
const text = xpathStringLiteral(element.name.trim());
|
|
244
|
+
const role = getElementRole(element);
|
|
245
|
+
if (role === 'link') {
|
|
246
|
+
return `//a[normalize-space()=${text}]`;
|
|
247
|
+
}
|
|
248
|
+
if (role === 'button' || role === 'tab') {
|
|
249
|
+
return `//*[self::button or @role="button" or @role="tab"][normalize-space()=${text}]`;
|
|
250
|
+
}
|
|
251
|
+
return `//*[normalize-space()=${text}]`;
|
|
252
|
+
}
|
|
253
|
+
function buildPurpose(candidate) {
|
|
254
|
+
return `Explore ${candidate.role} ${candidate.element.name.trim()}`;
|
|
255
|
+
}
|
|
256
|
+
function summarizeAria(aria) {
|
|
257
|
+
const lines = aria.split('\n').filter((line) => line.trim());
|
|
258
|
+
if (lines.length === 0) {
|
|
259
|
+
return 'No elements';
|
|
260
|
+
}
|
|
261
|
+
const roleCounts = {};
|
|
262
|
+
for (const role of lines.map(extractAriaRole).filter((role) => Boolean(role))) {
|
|
263
|
+
roleCounts[role] = (roleCounts[role] || 0) + 1;
|
|
264
|
+
}
|
|
265
|
+
const topRoles = Object.entries(roleCounts)
|
|
266
|
+
.sort((a, b) => b[1] - a[1])
|
|
267
|
+
.slice(0, 5)
|
|
268
|
+
.map(([role, count]) => `${role}:${count}`)
|
|
269
|
+
.join(', ');
|
|
270
|
+
return `${lines.length} elements (${topRoles})`;
|
|
271
|
+
}
|
|
272
|
+
function extractAriaRole(line) {
|
|
273
|
+
const roleMatch = line.match(/\[role: ([\w-]+)\]/);
|
|
274
|
+
if (roleMatch) {
|
|
275
|
+
return roleMatch[1];
|
|
276
|
+
}
|
|
277
|
+
const yamlMatch = line.trim().match(/^- ([\w-]+)(?:\s|$|:)/);
|
|
278
|
+
if (yamlMatch) {
|
|
279
|
+
return yamlMatch[1];
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
function countAriaChanges(before, after) {
|
|
284
|
+
const beforeLines = new Set(before.split('\n').filter((line) => line.trim()));
|
|
285
|
+
const afterLines = new Set(after.split('\n').filter((line) => line.trim()));
|
|
286
|
+
let newCount = 0;
|
|
287
|
+
let removedCount = 0;
|
|
288
|
+
for (const line of afterLines) {
|
|
289
|
+
if (!beforeLines.has(line)) {
|
|
290
|
+
newCount++;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
for (const line of beforeLines) {
|
|
294
|
+
if (!afterLines.has(line)) {
|
|
295
|
+
removedCount++;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return { newCount, removedCount };
|
|
299
|
+
}
|
|
300
|
+
function summarizeInteractiveState(state) {
|
|
301
|
+
const parts = [summarizeAria(state.ariaSnapshot || '')];
|
|
302
|
+
const headings = collectHeadings(state).slice(0, 3);
|
|
303
|
+
const links = collectLinks(state).slice(0, 3);
|
|
304
|
+
if (state.url) {
|
|
305
|
+
parts.push(`URL ${state.url}`);
|
|
306
|
+
}
|
|
307
|
+
if (headings.length > 0) {
|
|
308
|
+
parts.push(`Headings: ${headings.map((heading) => limitInlineText(heading, 90)).join(' | ')}`);
|
|
309
|
+
}
|
|
310
|
+
if (links.length > 0) {
|
|
311
|
+
parts.push(`Links: ${links.map((link) => `${link.title} -> ${link.url}`).join('; ')}`);
|
|
312
|
+
}
|
|
313
|
+
return parts.join('. ');
|
|
314
|
+
}
|
|
315
|
+
function collectHeadings(state) {
|
|
316
|
+
return [state.h1, state.h2, state.h3, state.h4].filter((heading) => Boolean(heading)).map((heading) => heading.trim());
|
|
317
|
+
}
|
|
318
|
+
function collectLinks(state) {
|
|
319
|
+
return (state.links || [])
|
|
320
|
+
.filter((link) => link.url)
|
|
321
|
+
.slice(0, MAX_LINKS)
|
|
322
|
+
.map((link) => ({
|
|
323
|
+
title: link.title || link.url,
|
|
324
|
+
url: link.url,
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
function describeAction(candidate) {
|
|
328
|
+
return `Clicked ${candidate.role}: ${candidate.element.name.trim()}`;
|
|
329
|
+
}
|
|
330
|
+
function hasUsableName(element) {
|
|
331
|
+
const name = element.name.trim();
|
|
332
|
+
if (!name) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
if (name.length < 2) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
function isNavigationSection(sectionName) {
|
|
341
|
+
return /(navigation|menu|header|footer|breadcrumb)/i.test(sectionName);
|
|
342
|
+
}
|
|
343
|
+
function isOverlaySection(sectionName, container) {
|
|
344
|
+
return /(overlay|modal|popup|dialog)/i.test(sectionName) || /(overlay|modal|popup|dialog)/i.test(container);
|
|
345
|
+
}
|
|
346
|
+
function scoreCandidate(candidate) {
|
|
347
|
+
let score = 0;
|
|
348
|
+
if (candidate.role === 'link') {
|
|
349
|
+
score += 50;
|
|
350
|
+
}
|
|
351
|
+
if (candidate.role === 'button') {
|
|
352
|
+
score += 40;
|
|
353
|
+
}
|
|
354
|
+
if (candidate.role === 'tab') {
|
|
355
|
+
score += 30;
|
|
356
|
+
}
|
|
357
|
+
if (candidate.container) {
|
|
358
|
+
score += 10;
|
|
359
|
+
}
|
|
360
|
+
if (candidate.element.css) {
|
|
361
|
+
score += 5;
|
|
362
|
+
}
|
|
363
|
+
if (candidate.element.name.trim().length > 8) {
|
|
364
|
+
score += 5;
|
|
365
|
+
}
|
|
366
|
+
return score;
|
|
367
|
+
}
|
|
368
|
+
function isShellLocator(locator) {
|
|
369
|
+
if (!locator) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
return /(nav\[role="navigation"\]|header|menu|breadcrumb|footer)/i.test(locator);
|
|
373
|
+
}
|
|
374
|
+
function collectNavigationLabels(sections) {
|
|
375
|
+
const labels = new Set();
|
|
376
|
+
for (const section of sections) {
|
|
377
|
+
if (!isNavigationSection(section.name.toLowerCase())) {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
for (const element of section.elements) {
|
|
381
|
+
const label = normalizeCandidateLabel(element.name);
|
|
382
|
+
if (!label) {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
labels.add(label);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return labels;
|
|
389
|
+
}
|
|
390
|
+
function normalizeCandidateLabel(label) {
|
|
391
|
+
return label.trim().toLowerCase();
|
|
392
|
+
}
|
|
393
|
+
function limitInlineText(text, maxLength) {
|
|
394
|
+
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
395
|
+
if (normalized.length <= maxLength) {
|
|
396
|
+
return normalized;
|
|
397
|
+
}
|
|
398
|
+
return `${normalized.slice(0, maxLength - 3).trimEnd()}...`;
|
|
399
|
+
}
|
|
400
|
+
function getElementRole(element) {
|
|
401
|
+
return (element.aria?.role || element.type || '').toLowerCase();
|
|
402
|
+
}
|
|
403
|
+
function xpathStringLiteral(value) {
|
|
404
|
+
if (!value.includes("'")) {
|
|
405
|
+
return `'${value}'`;
|
|
406
|
+
}
|
|
407
|
+
if (!value.includes('"')) {
|
|
408
|
+
return `"${value}"`;
|
|
409
|
+
}
|
|
410
|
+
const parts = value.split("'").map((part) => `'${part}'`);
|
|
411
|
+
return `concat(${parts.join(`, "'", `)})`;
|
|
412
|
+
}
|
|
413
|
+
async function wait(timeout) {
|
|
414
|
+
await new Promise((resolve) => setTimeout(resolve, timeout));
|
|
415
|
+
}
|
|
@@ -37,7 +37,7 @@ class DocBot {
|
|
|
37
37
|
config: this.options.docsConfig,
|
|
38
38
|
path: this.options.path,
|
|
39
39
|
});
|
|
40
|
-
this.documentarian = new Documentarian(this.explorBot.getProvider(), this.config);
|
|
40
|
+
this.documentarian = new Documentarian(this.explorBot.getProvider(), this.config, this.explorBot.getExplorer());
|
|
41
41
|
this.ensureDirectory(this.configParser.getOutputDir());
|
|
42
42
|
this.ensureDirectory(this.getPagesDir());
|
|
43
43
|
}
|
|
@@ -112,17 +112,21 @@ class DocBot {
|
|
|
112
112
|
summary: documentation.summary,
|
|
113
113
|
canCount: documentation.can.length,
|
|
114
114
|
mightCount: documentation.might.length,
|
|
115
|
+
interactionCount: (documentation.interactions || []).length,
|
|
115
116
|
canActions: documentation.can.map((item) => item.action),
|
|
116
117
|
mightActions: documentation.might.map((item) => item.action),
|
|
118
|
+
interactionActions: (documentation.interactions || []).map((item) => item.action),
|
|
119
|
+
qualityNotes: documentation.qualityNotes || [],
|
|
117
120
|
filePath,
|
|
118
121
|
});
|
|
119
122
|
documented.add(pageKey);
|
|
120
|
-
const nextPaths = this.extractNextPaths(state, baseUrl, research);
|
|
123
|
+
const nextPaths = this.extractNextPaths(state, baseUrl, research, documentation);
|
|
124
|
+
const interactionPriorityPaths = new Set(this.extractInteractionPaths(baseUrl, documentation));
|
|
121
125
|
for (const nextPath of nextPaths) {
|
|
122
126
|
if (documented.has(this.getPageKey(nextPath))) {
|
|
123
127
|
continue;
|
|
124
128
|
}
|
|
125
|
-
if (stateManager.hasVisitedState(nextPath)) {
|
|
129
|
+
if (!interactionPriorityPaths.has(nextPath) && stateManager.hasVisitedState(nextPath)) {
|
|
126
130
|
continue;
|
|
127
131
|
}
|
|
128
132
|
this.enqueuePath(nextPath, queue, queued);
|
|
@@ -162,9 +166,16 @@ class DocBot {
|
|
|
162
166
|
}
|
|
163
167
|
return true;
|
|
164
168
|
}
|
|
165
|
-
extractNextPaths(state, baseUrl, research) {
|
|
169
|
+
extractNextPaths(state, baseUrl, research, documentation) {
|
|
166
170
|
const paths = [];
|
|
167
171
|
const seen = new Set();
|
|
172
|
+
for (const interactionPath of this.extractInteractionPaths(baseUrl, documentation)) {
|
|
173
|
+
if (seen.has(interactionPath)) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
seen.add(interactionPath);
|
|
177
|
+
paths.push(interactionPath);
|
|
178
|
+
}
|
|
168
179
|
for (const link of state.links || []) {
|
|
169
180
|
const nextPath = this.resolveLink(link, baseUrl);
|
|
170
181
|
if (!nextPath) {
|
|
@@ -197,10 +208,51 @@ class DocBot {
|
|
|
197
208
|
}
|
|
198
209
|
return paths;
|
|
199
210
|
}
|
|
211
|
+
extractInteractionPaths(baseUrl, documentation) {
|
|
212
|
+
const paths = [];
|
|
213
|
+
const seen = new Set();
|
|
214
|
+
const interactions = documentation?.interactions;
|
|
215
|
+
for (const interaction of interactions || []) {
|
|
216
|
+
if (interaction.targetUrl) {
|
|
217
|
+
const nextPath = this.resolveRawUrl(interaction.targetUrl, baseUrl);
|
|
218
|
+
if (nextPath && this.isEligibleNextPath(nextPath) && !seen.has(nextPath)) {
|
|
219
|
+
seen.add(nextPath);
|
|
220
|
+
paths.push(nextPath);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (const discoveredUrl of interaction.discoveredUrls || []) {
|
|
224
|
+
const discoveredPath = this.resolveRawUrl(discoveredUrl, baseUrl);
|
|
225
|
+
if (!discoveredPath) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (!this.isEligibleNextPath(discoveredPath)) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (seen.has(discoveredPath)) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
seen.add(discoveredPath);
|
|
235
|
+
paths.push(discoveredPath);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return paths;
|
|
239
|
+
}
|
|
240
|
+
isEligibleNextPath(nextPath) {
|
|
241
|
+
if (!shouldCrawlDocPath(nextPath, this.config)) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
if (!this.isInScope(nextPath)) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
200
249
|
resolveLink(link, baseUrl) {
|
|
250
|
+
return this.resolveRawUrl(link.url, baseUrl);
|
|
251
|
+
}
|
|
252
|
+
resolveRawUrl(rawUrl, baseUrl) {
|
|
201
253
|
let resolved;
|
|
202
254
|
try {
|
|
203
|
-
resolved = new URL(
|
|
255
|
+
resolved = new URL(rawUrl, baseUrl);
|
|
204
256
|
}
|
|
205
257
|
catch {
|
|
206
258
|
return null;
|
|
@@ -11,6 +11,26 @@ function renderPageDocumentation(state, documentation) {
|
|
|
11
11
|
lines.push('');
|
|
12
12
|
lines.push(ensureSentence(documentation.summary));
|
|
13
13
|
lines.push('');
|
|
14
|
+
const interactions = documentation.interactions;
|
|
15
|
+
if (interactions && interactions.length > 0) {
|
|
16
|
+
lines.push('## State Transitions');
|
|
17
|
+
lines.push('');
|
|
18
|
+
for (const transition of interactions) {
|
|
19
|
+
lines.push(`### ${transition.action}`);
|
|
20
|
+
lines.push('');
|
|
21
|
+
lines.push(`**Before:** ${transition.before}`);
|
|
22
|
+
lines.push('');
|
|
23
|
+
lines.push(`**After:** ${transition.after}`);
|
|
24
|
+
lines.push('');
|
|
25
|
+
if (transition.newCapabilities && transition.newCapabilities.length > 0) {
|
|
26
|
+
lines.push('**Observed changes:**');
|
|
27
|
+
for (const cap of transition.newCapabilities) {
|
|
28
|
+
lines.push(`- ${cap}`);
|
|
29
|
+
}
|
|
30
|
+
lines.push('');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
14
34
|
lines.push('## User Can');
|
|
15
35
|
lines.push('');
|
|
16
36
|
if (documentation.can.length === 0) {
|
|
@@ -37,6 +57,15 @@ function renderPageDocumentation(state, documentation) {
|
|
|
37
57
|
if (documentation.might.length > 0) {
|
|
38
58
|
lines.push('');
|
|
39
59
|
}
|
|
60
|
+
const qualityNotes = documentation.qualityNotes;
|
|
61
|
+
if (qualityNotes && qualityNotes.length > 0) {
|
|
62
|
+
lines.push('## Coverage Notes');
|
|
63
|
+
lines.push('');
|
|
64
|
+
for (const note of qualityNotes) {
|
|
65
|
+
lines.push(`- ${ensureSentence(note)}`);
|
|
66
|
+
}
|
|
67
|
+
lines.push('');
|
|
68
|
+
}
|
|
40
69
|
return `${lines.join('\n').trimEnd()}\n`;
|
|
41
70
|
}
|
|
42
71
|
function renderSpecIndex(outputDir, startPath, pages, skipped, maxPages) {
|
|
@@ -63,6 +92,9 @@ function renderSpecIndex(outputDir, startPath, pages, skipped, maxPages) {
|
|
|
63
92
|
lines.push(`Purpose: ${ensureSentence(page.summary)}`);
|
|
64
93
|
lines.push(`Proven actions: ${page.canCount}`);
|
|
65
94
|
lines.push(`Possible actions: ${page.mightCount}`);
|
|
95
|
+
if (page.interactionCount > 0) {
|
|
96
|
+
lines.push(`Interactive transitions: ${page.interactionCount}`);
|
|
97
|
+
}
|
|
66
98
|
if (page.title) {
|
|
67
99
|
lines.push(`Title: ${normalizeInlineText(page.title)}`);
|
|
68
100
|
}
|
|
@@ -81,6 +113,20 @@ function renderSpecIndex(outputDir, startPath, pages, skipped, maxPages) {
|
|
|
81
113
|
}
|
|
82
114
|
lines.push('');
|
|
83
115
|
}
|
|
116
|
+
if (page.interactionActions.length > 0) {
|
|
117
|
+
lines.push('Interactive Findings:');
|
|
118
|
+
for (const action of page.interactionActions.slice(0, 3)) {
|
|
119
|
+
lines.push(`- ${normalizeInlineText(action)}`);
|
|
120
|
+
}
|
|
121
|
+
lines.push('');
|
|
122
|
+
}
|
|
123
|
+
if (page.qualityNotes.length > 0) {
|
|
124
|
+
lines.push('Coverage Notes:');
|
|
125
|
+
for (const note of page.qualityNotes) {
|
|
126
|
+
lines.push(`- ${ensureSentence(note)}`);
|
|
127
|
+
}
|
|
128
|
+
lines.push('');
|
|
129
|
+
}
|
|
84
130
|
}
|
|
85
131
|
if (skipped.length > 0) {
|
|
86
132
|
lines.push('## Skipped');
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"@opentelemetry/sdk-trace-base": "^2.2.0",
|
|
83
83
|
"@opentelemetry/semantic-conventions": "^1.38.0",
|
|
84
84
|
"@scalar/openapi-parser": "^0.25.6",
|
|
85
|
-
"@testomatio/reporter": "^2.
|
|
85
|
+
"@testomatio/reporter": "^2.8.4",
|
|
86
86
|
"ai": "^6.0.6",
|
|
87
87
|
"axe-core": "^4.11.1",
|
|
88
88
|
"bash-tool": "^1.3.15",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"micromatch": "^4.0.8",
|
|
109
109
|
"ora-classic": "^5.4.2",
|
|
110
110
|
"parse5": "^8.0.0",
|
|
111
|
-
"playwright": "^1.
|
|
111
|
+
"playwright": "^1.60",
|
|
112
112
|
"react": "^19.1.1",
|
|
113
113
|
"strip-ansi": "^7.1.2",
|
|
114
114
|
"turndown": "^7.2.1",
|
|
@@ -151,6 +151,18 @@ export class ActionResult {
|
|
|
151
151
|
this.verifications ??= {};
|
|
152
152
|
this.verifications[assertion] = passed;
|
|
153
153
|
}
|
|
154
|
+
getVerification(message) {
|
|
155
|
+
if (!this.verifications)
|
|
156
|
+
return null;
|
|
157
|
+
if (typeof message === 'string') {
|
|
158
|
+
return this.verifications[message] ?? null;
|
|
159
|
+
}
|
|
160
|
+
for (const [assertion, passed] of Object.entries(this.verifications)) {
|
|
161
|
+
if (message.test(assertion))
|
|
162
|
+
return passed;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
154
166
|
isSameUrl(state) {
|
|
155
167
|
if (!this.url || this.url === '') {
|
|
156
168
|
return false;
|