explorbot 0.1.20 → 0.1.21

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.
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import type { WebPageState } from '../../../src/state-manager.ts';
3
- import type { PageDocumentation } from './ai/documentarian.ts';
3
+ import type { PageDocumentation, StateTransition } from './ai/documentarian.ts';
4
4
 
5
5
  function renderPageDocumentation(state: WebPageState, documentation: PageDocumentation): string {
6
6
  const lines: string[] = [];
@@ -16,6 +16,28 @@ function renderPageDocumentation(state: WebPageState, documentation: PageDocumen
16
16
  lines.push('');
17
17
  lines.push(ensureSentence(documentation.summary));
18
18
  lines.push('');
19
+
20
+ const interactions = documentation.interactions;
21
+ if (interactions && interactions.length > 0) {
22
+ lines.push('## State Transitions');
23
+ lines.push('');
24
+ for (const transition of interactions) {
25
+ lines.push(`### ${transition.action}`);
26
+ lines.push('');
27
+ lines.push(`**Before:** ${transition.before}`);
28
+ lines.push('');
29
+ lines.push(`**After:** ${transition.after}`);
30
+ lines.push('');
31
+ if (transition.newCapabilities && transition.newCapabilities.length > 0) {
32
+ lines.push('**Observed changes:**');
33
+ for (const cap of transition.newCapabilities) {
34
+ lines.push(`- ${cap}`);
35
+ }
36
+ lines.push('');
37
+ }
38
+ }
39
+ }
40
+
19
41
  lines.push('## User Can');
20
42
  lines.push('');
21
43
 
@@ -50,6 +72,16 @@ function renderPageDocumentation(state: WebPageState, documentation: PageDocumen
50
72
  lines.push('');
51
73
  }
52
74
 
75
+ const qualityNotes = documentation.qualityNotes;
76
+ if (qualityNotes && qualityNotes.length > 0) {
77
+ lines.push('## Coverage Notes');
78
+ lines.push('');
79
+ for (const note of qualityNotes) {
80
+ lines.push(`- ${ensureSentence(note)}`);
81
+ }
82
+ lines.push('');
83
+ }
84
+
53
85
  return `${lines.join('\n').trimEnd()}\n`;
54
86
  }
55
87
 
@@ -79,6 +111,9 @@ function renderSpecIndex(outputDir: string, startPath: string, pages: Documented
79
111
  lines.push(`Purpose: ${ensureSentence(page.summary)}`);
80
112
  lines.push(`Proven actions: ${page.canCount}`);
81
113
  lines.push(`Possible actions: ${page.mightCount}`);
114
+ if (page.interactionCount > 0) {
115
+ lines.push(`Interactive transitions: ${page.interactionCount}`);
116
+ }
82
117
  if (page.title) {
83
118
  lines.push(`Title: ${normalizeInlineText(page.title)}`);
84
119
  }
@@ -99,6 +134,22 @@ function renderSpecIndex(outputDir: string, startPath: string, pages: Documented
99
134
  }
100
135
  lines.push('');
101
136
  }
137
+
138
+ if (page.interactionActions.length > 0) {
139
+ lines.push('Interactive Findings:');
140
+ for (const action of page.interactionActions.slice(0, 3)) {
141
+ lines.push(`- ${normalizeInlineText(action)}`);
142
+ }
143
+ lines.push('');
144
+ }
145
+
146
+ if (page.qualityNotes.length > 0) {
147
+ lines.push('Coverage Notes:');
148
+ for (const note of page.qualityNotes) {
149
+ lines.push(`- ${ensureSentence(note)}`);
150
+ }
151
+ lines.push('');
152
+ }
102
153
  }
103
154
 
104
155
  if (skipped.length > 0) {
@@ -173,8 +224,11 @@ interface DocumentedPage {
173
224
  summary: string;
174
225
  canCount: number;
175
226
  mightCount: number;
227
+ interactionCount: number;
176
228
  canActions: string[];
177
229
  mightActions: string[];
230
+ interactionActions: string[];
231
+ qualityNotes: string[];
178
232
  filePath: string;
179
233
  }
180
234
 
@@ -184,4 +238,4 @@ interface SkippedPage {
184
238
  }
185
239
 
186
240
  export { renderPageDocumentation, renderSpecIndex, ensureSentence, normalizeAction };
187
- export type { DocumentedPage, SkippedPage };
241
+ export type { DocumentedPage, SkippedPage, StateTransition };
@@ -266,11 +266,17 @@ addCommonOptions(program.command('plan:load <planfile> [index]').description('Lo
266
266
  process.exit(1);
267
267
  }
268
268
  });
269
- addCommonOptions(program.command('test <planfile> [index]').description('Execute tests from a plan file. Index: 1, 1,3, 1-5, *, all').option('--grep <pattern>', 'Run tests matching pattern')).action(async (planfile, index, options) => {
269
+ addCommonOptions(program.command('test <planfile> [index]').description('Execute tests from a plan file. Index: 1, 1,3, 1-5, *, all').option('--grep <pattern>', 'Run tests matching pattern').option('--from-plan <file>', 'Load plan file when the first argument is a test index')).action(async (planfile, index, options) => {
270
270
  try {
271
271
  const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
272
272
  await explorBot.start();
273
- const plan = explorBot.loadPlan(planfile);
273
+ let planfileArg = planfile;
274
+ let indexArg = index;
275
+ if (options.fromPlan) {
276
+ planfileArg = options.fromPlan;
277
+ indexArg = planfile;
278
+ }
279
+ const plan = explorBot.loadPlan(planfileArg);
274
280
  const pending = plan.getPendingTests();
275
281
  log(`Plan loaded: "${plan.title}" (${plan.tests.length} tests, ${pending.length} pending)`);
276
282
  const startUrl = plan.url || pending[0]?.startUrl;
@@ -280,8 +286,8 @@ addCommonOptions(program.command('test <planfile> [index]').description('Execute
280
286
  log(`Navigating to ${startUrl}`);
281
287
  await explorBot.visit(startUrl);
282
288
  let args = '';
283
- if (index)
284
- args = index;
289
+ if (indexArg)
290
+ args = indexArg;
285
291
  else if (options.grep)
286
292
  args = options.grep;
287
293
  const { TestCommand } = await import('../src/commands/test-command.js');
@@ -315,6 +321,26 @@ program
315
321
  process.exit(1);
316
322
  }
317
323
  });
324
+ program
325
+ .command('plans [plan]')
326
+ .description('List saved plans, or show tests for a specific plan')
327
+ .option('-p, --path <path>', 'Working directory path')
328
+ .option('-c, --config <path>', 'Path to configuration file')
329
+ .action(async (plan, options) => {
330
+ try {
331
+ await ConfigParser.getInstance().loadConfig({
332
+ config: options.config,
333
+ path: options.path || process.cwd(),
334
+ });
335
+ const explorBot = new ExplorBot({ path: options.path });
336
+ const { PlansCommand } = await import('../src/commands/plans-command.js');
337
+ await new PlansCommand(explorBot).execute(plan || '');
338
+ }
339
+ catch (error) {
340
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
341
+ process.exit(1);
342
+ }
343
+ });
318
344
  addCommonOptions(program.command('rerun <filename> [index]').description('Re-run generated tests with AI auto-healing')).action(async (filename, index, options) => {
319
345
  try {
320
346
  const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
@@ -1,13 +1,26 @@
1
1
  import dedent from 'dedent';
2
2
  import { z } from 'zod';
3
+ import { tag } from "../../../../src/utils/logger.js";
4
+ import { collectDocInteractions } from "./tools.js";
3
5
  class Documentarian {
4
6
  provider;
5
7
  config;
6
- constructor(provider, config = {}) {
8
+ explorer;
9
+ constructor(provider, config = {}, explorer) {
7
10
  this.provider = provider;
8
11
  this.config = config;
12
+ this.explorer = explorer;
9
13
  }
10
14
  async document(state, research) {
15
+ const interactiveEnabled = this.config.docs?.interactive === true && this.explorer;
16
+ if (!interactiveEnabled) {
17
+ tag('info').log('Documentarian: Using static mode (interactive disabled or no explorer)');
18
+ return this.documentStatic(state, research);
19
+ }
20
+ tag('info').log('Documentarian: Using interactive mode with tools');
21
+ return this.documentWithInteraction(state, research);
22
+ }
23
+ async documentStatic(state, research) {
11
24
  try {
12
25
  return await this.generateDocumentation(state, research);
13
26
  }
@@ -18,39 +31,110 @@ class Documentarian {
18
31
  return this.generateDocumentation(state, this.sanitizeResearch(research), true);
19
32
  }
20
33
  }
34
+ async documentWithInteraction(state, research) {
35
+ try {
36
+ tag('info').log('Starting interactive exploration...');
37
+ const deterministicInteractions = await collectDocInteractions(this.explorer, state, research);
38
+ const meaningfulInteractions = this.getMeaningfulInteractions(deterministicInteractions);
39
+ if (meaningfulInteractions.length > 0) {
40
+ tag('success').log(`Collected ${meaningfulInteractions.length} deterministic interactions`);
41
+ return await this.generateDocumentationWithInteractions(state, research, meaningfulInteractions);
42
+ }
43
+ if (deterministicInteractions.length > 0) {
44
+ tag('info').log('Interactive exploration found only low-value navigation changes. Using static documentation.');
45
+ }
46
+ else {
47
+ tag('info').log('Interactive exploration found no reliable deterministic interactions. Using static documentation.');
48
+ }
49
+ return this.documentStatic(state, research);
50
+ }
51
+ catch (error) {
52
+ const message = error instanceof Error ? error.message : String(error);
53
+ tag('warning').log(`Interactive documentation failed: ${message}. Falling back to static.`);
54
+ return this.documentStatic(state, research);
55
+ }
56
+ }
57
+ getMeaningfulInteractions(interactions) {
58
+ return interactions.filter((interaction) => {
59
+ if (interaction.targetUrl) {
60
+ return true;
61
+ }
62
+ if (interaction.changes?.urlChanged) {
63
+ return true;
64
+ }
65
+ if ((interaction.changes?.newElements || 0) > 0) {
66
+ return true;
67
+ }
68
+ return (interaction.discoveredUrls || []).length > 0;
69
+ });
70
+ }
71
+ async generateDocumentationWithInteractions(state, research, interactions) {
72
+ const messages = [
73
+ {
74
+ role: 'system',
75
+ content: this.getSystemPrompt(),
76
+ },
77
+ {
78
+ role: 'user',
79
+ content: this.buildPrompt(state, `${research}${this.buildInteractionContext(interactions)}`),
80
+ },
81
+ ];
82
+ const response = await this.provider.generateObject(messages, pageDocumentationSchema, undefined, {
83
+ agentName: 'documentarian',
84
+ });
85
+ return this.normalizeDocumentation({
86
+ ...response.object,
87
+ interactions,
88
+ }, state, research);
89
+ }
90
+ async generateDocumentation(state, research, simplified = false) {
91
+ const messages = [
92
+ {
93
+ role: 'system',
94
+ content: this.getSystemPrompt(),
95
+ },
96
+ {
97
+ role: 'user',
98
+ content: this.buildPrompt(state, research, simplified),
99
+ },
100
+ ];
101
+ const response = await this.provider.generateObject(messages, pageDocumentationSchema, undefined, {
102
+ agentName: 'documentarian',
103
+ });
104
+ return this.normalizeDocumentation(response.object, state, research);
105
+ }
21
106
  getSystemPrompt() {
22
- const customPrompt = this.config.docs?.prompt;
23
107
  let promptSuffix = '';
24
- if (customPrompt) {
25
- promptSuffix = customPrompt;
108
+ if (this.config.docs?.prompt) {
109
+ promptSuffix = this.config.docs.prompt;
26
110
  }
27
111
  return dedent `
28
- <role>
29
- You are a product analyst preparing functional website documentation from UI research.
30
- </role>
112
+ <role>
113
+ You are a product analyst preparing functional website documentation from UI research.
114
+ </role>
31
115
 
32
- <task>
33
- Convert exploratory UI research into a precise spec of what users can do on the current page.
34
- Distinguish proven capabilities from assumptions.
35
- Prefer accuracy over coverage.
36
- </task>
116
+ <task>
117
+ Convert exploratory UI research into a precise spec of what users can do on the current page.
118
+ Distinguish proven capabilities from assumptions.
119
+ Prefer accuracy over coverage.
120
+ </task>
37
121
 
38
- <rules>
39
- Only list capabilities that are grounded in the provided page research.
40
- Put actions into "can" only when there is direct evidence in the page context.
41
- Put actions into "might" only when the UI strongly suggests a capability but proof is incomplete.
42
- Describe each action from the end-user perspective.
43
- Be explicit about scope:
44
- - one item
45
- - list of items
46
- - bulk operations
47
- - all items
48
- - page-level
49
- Avoid implementation details, selectors, and QA wording.
50
- Avoid duplicate actions with different phrasing.
51
- </rules>
122
+ <rules>
123
+ Only list capabilities that are grounded in the provided page research.
124
+ Put actions into "can" only when there is direct evidence in the page context.
125
+ Put actions into "might" only when the UI strongly suggests a capability but proof is incomplete.
126
+ Describe each action from the end-user perspective.
127
+ Be explicit about scope:
128
+ - one item
129
+ - list of items
130
+ - bulk operations
131
+ - all items
132
+ - page-level
133
+ Avoid implementation details, selectors, and QA wording.
134
+ Avoid duplicate actions with different phrasing.
135
+ </rules>
52
136
 
53
- ${promptSuffix}
137
+ ${promptSuffix}
54
138
  `;
55
139
  }
56
140
  buildPrompt(state, research, simplified = false) {
@@ -59,68 +143,89 @@ class Documentarian {
59
143
  .slice(0, 50)
60
144
  .map((link) => `- ${link.title}: ${link.url}`)
61
145
  .join('\n');
62
- const simplificationNote = simplified
63
- ? dedent `
146
+ let simplificationNote = '';
147
+ if (simplified) {
148
+ simplificationNote = dedent `
64
149
  <fallback_mode>
65
150
  The research text was simplified because the original formatting was noisy.
66
151
  Ignore malformed table syntax and rely only on clear, repeated signals.
67
152
  Prefer fewer actions over speculative coverage.
68
153
  </fallback_mode>
69
- `
70
- : '';
154
+ `;
155
+ }
71
156
  return dedent `
72
- <page>
73
- URL: ${state.url}
74
- Title: ${state.title || ''}
75
- Headings: ${headings}
76
- </page>
157
+ <page>
158
+ URL: ${state.url}
159
+ Title: ${state.title || ''}
160
+ Headings: ${headings}
161
+ </page>
77
162
 
78
- <navigation_links>
79
- ${links}
80
- </navigation_links>
163
+ <navigation_links>
164
+ ${links}
165
+ </navigation_links>
81
166
 
82
- <research>
83
- ${research}
84
- </research>
167
+ <research>
168
+ ${research}
169
+ </research>
85
170
 
86
- ${simplificationNote}
171
+ ${simplificationNote}
87
172
 
88
- <output_requirements>
89
- Return structured data.
90
- summary: short page purpose statement.
91
- can: actions you are 100% sure are available on page.
92
- might: actions that look possible but are not fully proven.
93
- For each action provide:
94
- - action: concise user-facing capability phrased as "user can ..."
95
- - scope: one of one item, list of items, bulk operations, all items, page-level
96
- - evidence: short reason based on visible UI or research
97
- </output_requirements>
173
+ <output_requirements>
174
+ Return structured data.
175
+ summary: short page purpose statement.
176
+ can: actions you are 100% sure are available on page.
177
+ might: actions that look possible but are not fully proven.
178
+ For each action provide:
179
+ - action: concise user-facing capability phrased as "user can ..."
180
+ - scope: one of one item, list of items, bulk operations, all items, page-level
181
+ - evidence: short reason based on visible UI or research
182
+ </output_requirements>
98
183
  `;
99
184
  }
100
- async generateDocumentation(state, research, simplified = false) {
101
- const messages = [
102
- {
103
- role: 'system',
104
- content: this.getSystemPrompt(),
105
- },
106
- {
107
- role: 'user',
108
- content: this.buildPrompt(state, research, simplified),
109
- },
110
- ];
111
- const response = await this.provider.generateObject(messages, pageDocumentationSchema, undefined, {
112
- agentName: 'documentarian',
113
- });
114
- return response.object;
185
+ buildInteractionContext(interactions) {
186
+ const lines = interactions
187
+ .map((interaction) => {
188
+ const parts = [`Action: ${interaction.action}`, `Element: ${this.formatInteractionElement(interaction)}`, `Before: ${interaction.before}`, `After: ${interaction.after}`, `Changes: ${this.formatInteractionChanges(interaction)}`];
189
+ if (interaction.targetUrl) {
190
+ parts.push(`Target URL: ${interaction.targetUrl}`);
191
+ }
192
+ if (interaction.discoveredUrls && interaction.discoveredUrls.length > 0) {
193
+ parts.push(`Discovered URLs: ${interaction.discoveredUrls.join(', ')}`);
194
+ }
195
+ return `- ${parts.join('\n ')}`;
196
+ })
197
+ .join('\n');
198
+ return dedent `
199
+
200
+ <interaction_observations>
201
+ These are raw observations collected after interacting with visible controls. They are not semantic conclusions.
202
+ Classify them yourself as proven user capabilities, possible capabilities, navigation, page-state changes, or noise.
203
+ Do not trust the action label as a capability category; use the before/after state, element metadata, and URL changes as evidence.
204
+ ${lines}
205
+ </interaction_observations>
206
+ `;
115
207
  }
116
208
  shouldRetryWithSanitizedResearch(error) {
117
209
  const message = error instanceof Error ? error.message : String(error);
118
- return message.includes('Failed to generate JSON') || message.includes('failed_generation');
210
+ return message.includes('Failed to generate JSON') || message.includes('Failed to validate JSON') || message.includes('failed_generation') || message.includes('No object generated') || message.includes('response did not match schema');
211
+ }
212
+ normalizeDocumentation(documentation, _state, _research) {
213
+ const qualityNotes = this.evaluateDocumentationQuality(documentation);
214
+ return {
215
+ ...documentation,
216
+ qualityNotes,
217
+ };
218
+ }
219
+ evaluateDocumentationQuality(documentation) {
220
+ const notes = [];
221
+ if ((documentation.interactions || []).length === 0 && this.config.docs?.interactive) {
222
+ notes.push('Interactive exploration did not produce any reliable page-specific transitions for this page.');
223
+ }
224
+ return notes;
119
225
  }
120
226
  sanitizeResearch(research) {
121
- const lines = research.split('\n');
122
227
  const sanitized = [];
123
- for (const line of lines) {
228
+ for (const line of research.split('\n')) {
124
229
  if (!line.trim()) {
125
230
  sanitized.push(line);
126
231
  continue;
@@ -143,15 +248,59 @@ class Documentarian {
143
248
  }
144
249
  return sanitized.join('\n');
145
250
  }
251
+ formatInteractionElement(interaction) {
252
+ if (!interaction.element) {
253
+ return 'unknown element';
254
+ }
255
+ const parts = [`role=${interaction.element.role}`, `name=${interaction.element.name}`, `section=${interaction.element.section}`];
256
+ if (interaction.element.container) {
257
+ parts.push(`container=${interaction.element.container}`);
258
+ }
259
+ if (interaction.element.locator) {
260
+ parts.push(`locator=${interaction.element.locator}`);
261
+ }
262
+ return parts.join(', ');
263
+ }
264
+ formatInteractionChanges(interaction) {
265
+ if (!interaction.changes) {
266
+ return 'unknown changes';
267
+ }
268
+ return `urlChanged=${interaction.changes.urlChanged}, newElements=${interaction.changes.newElements}, removedElements=${interaction.changes.removedElements}`;
269
+ }
146
270
  }
147
271
  const capabilitySchema = z.object({
148
272
  action: z.string(),
149
273
  scope: z.enum(['one item', 'list of items', 'bulk operations', 'all items', 'page-level']),
150
274
  evidence: z.string(),
151
275
  });
276
+ const stateTransitionSchema = z.object({
277
+ action: z.string(),
278
+ before: z.string(),
279
+ after: z.string(),
280
+ targetUrl: z.string().optional(),
281
+ discoveredUrls: z.array(z.string()).optional(),
282
+ newCapabilities: z.array(z.string()).optional(),
283
+ element: z
284
+ .object({
285
+ role: z.string(),
286
+ name: z.string(),
287
+ section: z.string(),
288
+ container: z.string().optional(),
289
+ locator: z.string().optional(),
290
+ })
291
+ .optional(),
292
+ changes: z
293
+ .object({
294
+ urlChanged: z.boolean(),
295
+ newElements: z.number(),
296
+ removedElements: z.number(),
297
+ })
298
+ .optional(),
299
+ });
152
300
  const pageDocumentationSchema = z.object({
153
301
  summary: z.string(),
154
302
  can: z.array(capabilitySchema),
155
303
  might: z.array(capabilitySchema),
304
+ interactions: z.array(stateTransitionSchema).optional(),
156
305
  });
157
306
  export { Documentarian };