@zibby/core 0.1.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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -0
  3. package/package.json +94 -0
  4. package/src/agents/base.js +361 -0
  5. package/src/constants.js +47 -0
  6. package/src/enrichment/base.js +49 -0
  7. package/src/enrichment/enrichers/accessibility-enricher.js +197 -0
  8. package/src/enrichment/enrichers/dom-enricher.js +171 -0
  9. package/src/enrichment/enrichers/page-state-enricher.js +129 -0
  10. package/src/enrichment/enrichers/position-enricher.js +67 -0
  11. package/src/enrichment/index.js +96 -0
  12. package/src/enrichment/mcp-integration.js +149 -0
  13. package/src/enrichment/mcp-ref-enricher.js +78 -0
  14. package/src/enrichment/pipeline.js +192 -0
  15. package/src/enrichment/trace-text-enricher.js +115 -0
  16. package/src/framework/AGENTS.md +98 -0
  17. package/src/framework/agents/base.js +72 -0
  18. package/src/framework/agents/claude-strategy.js +278 -0
  19. package/src/framework/agents/cursor-strategy.js +459 -0
  20. package/src/framework/agents/index.js +105 -0
  21. package/src/framework/agents/utils/cursor-output-formatter.js +67 -0
  22. package/src/framework/agents/utils/openai-proxy-formatter.js +249 -0
  23. package/src/framework/code-generator.js +301 -0
  24. package/src/framework/constants.js +33 -0
  25. package/src/framework/context-loader.js +101 -0
  26. package/src/framework/function-bridge.js +78 -0
  27. package/src/framework/function-skill-registry.js +20 -0
  28. package/src/framework/graph-compiler.js +342 -0
  29. package/src/framework/graph.js +610 -0
  30. package/src/framework/index.js +28 -0
  31. package/src/framework/node-registry.js +163 -0
  32. package/src/framework/node.js +259 -0
  33. package/src/framework/output-parser.js +71 -0
  34. package/src/framework/skill-registry.js +55 -0
  35. package/src/framework/state-utils.js +52 -0
  36. package/src/framework/state.js +67 -0
  37. package/src/framework/tool-resolver.js +65 -0
  38. package/src/index.js +342 -0
  39. package/src/runtime/generation/base.js +46 -0
  40. package/src/runtime/generation/index.js +70 -0
  41. package/src/runtime/generation/mcp-ref-strategy.js +197 -0
  42. package/src/runtime/generation/stable-id-strategy.js +170 -0
  43. package/src/runtime/stable-id-runtime.js +248 -0
  44. package/src/runtime/verification/base.js +44 -0
  45. package/src/runtime/verification/index.js +67 -0
  46. package/src/runtime/verification/playwright-json-strategy.js +119 -0
  47. package/src/runtime/zibby-runtime.js +299 -0
  48. package/src/sync/index.js +2 -0
  49. package/src/sync/uploader.js +29 -0
  50. package/src/tools/run-playwright-test.js +158 -0
  51. package/src/utils/adf-converter.js +68 -0
  52. package/src/utils/ast-utils.js +37 -0
  53. package/src/utils/ci-setup.js +124 -0
  54. package/src/utils/cursor-utils.js +71 -0
  55. package/src/utils/logger.js +144 -0
  56. package/src/utils/mcp-config-writer.js +115 -0
  57. package/src/utils/node-schema-parser.js +522 -0
  58. package/src/utils/post-process-events.js +55 -0
  59. package/src/utils/result-handler.js +102 -0
  60. package/src/utils/ripple-effect.js +84 -0
  61. package/src/utils/selector-generator.js +239 -0
  62. package/src/utils/streaming-parser.js +387 -0
  63. package/src/utils/test-post-processor.js +211 -0
  64. package/src/utils/timeline.js +217 -0
  65. package/src/utils/trace-parser.js +325 -0
  66. package/src/utils/video-organizer.js +91 -0
  67. package/templates/browser-test-automation/README.md +114 -0
  68. package/templates/browser-test-automation/graph.js +54 -0
  69. package/templates/browser-test-automation/nodes/execute-live.js +250 -0
  70. package/templates/browser-test-automation/nodes/generate-script.js +77 -0
  71. package/templates/browser-test-automation/nodes/index.js +3 -0
  72. package/templates/browser-test-automation/nodes/preflight.js +59 -0
  73. package/templates/browser-test-automation/nodes/utils.js +154 -0
  74. package/templates/browser-test-automation/result-handler.js +286 -0
  75. package/templates/code-analysis/graph.js +72 -0
  76. package/templates/code-analysis/index.js +18 -0
  77. package/templates/code-analysis/nodes/analyze-ticket-node.js +204 -0
  78. package/templates/code-analysis/nodes/create-pr-node.js +175 -0
  79. package/templates/code-analysis/nodes/finalize-node.js +118 -0
  80. package/templates/code-analysis/nodes/generate-code-node.js +425 -0
  81. package/templates/code-analysis/nodes/generate-test-cases-node.js +376 -0
  82. package/templates/code-analysis/nodes/services/prMetaService.js +86 -0
  83. package/templates/code-analysis/nodes/setup-node.js +142 -0
  84. package/templates/code-analysis/prompts/analyze-ticket.md +181 -0
  85. package/templates/code-analysis/prompts/generate-code.md +33 -0
  86. package/templates/code-analysis/prompts/generate-test-cases.md +110 -0
  87. package/templates/code-analysis/state.js +40 -0
  88. package/templates/code-implementation/graph.js +35 -0
  89. package/templates/code-implementation/index.js +7 -0
  90. package/templates/code-implementation/state.js +14 -0
  91. package/templates/global-setup.js +56 -0
  92. package/templates/index.js +94 -0
  93. package/templates/register-nodes.js +24 -0
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Execute Live Node
3
+ *
4
+ * Purpose: Execute test in live browser using MCP Playwright tools
5
+ *
6
+ * Configuration:
7
+ * - capabilities: Declares ['browser'] — framework injects the appropriate MCP server
8
+ * - outputSchema: Structured JSON with execution results, actions, assertions
9
+ * - Model: Configured in .zibby.config.js → agent.claude.model or agent.cursor.model
10
+ */
11
+
12
+ import { z, SKILLS } from '@zibby/core';
13
+ import { formatAssertionChecklist } from './utils.js';
14
+
15
+ export const executeLiveNode = {
16
+ name: 'execute_live',
17
+ skills: [SKILLS.BROWSER, SKILLS.MEMORY],
18
+ timeout: 600000,
19
+
20
+ prompt: (state) => {
21
+ const ctx = state.context;
22
+ const contextInfo = ctx ? `
23
+ Domain Knowledge & Environment:
24
+ ${ctx.global || ''}
25
+ ${ctx.pathBased ? `Test-Specific Info:\n${ctx.pathBased}\n` : ''}
26
+ ${ctx.env ? `Environment Config:\n${JSON.stringify(ctx.env, null, 2)}\n` : ''}
27
+ ---
28
+ ` : '';
29
+
30
+ const assertionChecklist = formatAssertionChecklist(state.preflight?.assertions);
31
+
32
+ return `⚠️ CRITICAL: At the END, output ONLY the JSON object. NO explanations after the JSON.
33
+
34
+ Execute this test using ONLY mcp_playwright-official_browser_* tools.
35
+
36
+ 🚨 HONESTY REQUIREMENT (STRICT):
37
+ - If you DO NOT have access to browser tools → return {"success": false, "steps": [], "browserClosed": false, "notes": "No browser tools available"}
38
+ - DO NOT hallucinate or pretend you executed the test
39
+ - DO NOT return success: true unless you ACTUALLY called browser tools
40
+ - BE HONEST - it's better to admit you can't do it than to lie
41
+
42
+ 🎯 YOUR GOAL: Execute the test steps and collect evidence for script generation.
43
+ You don't need perfect verification - just capture the key actions and results.
44
+ The next node will generate the actual test script from your execution.
45
+
46
+ ${contextInfo}
47
+ ${state.testSpec}
48
+ ${assertionChecklist ? `
49
+ ═══════════════════════════════════════════════════
50
+ 🎯 ASSERTION CHECKLIST (MANDATORY - from test spec)
51
+ You MUST include ALL of these in your 'assertions' array.
52
+ Report each as passed: true or passed: false with evidence.
53
+ DO NOT skip any. DO NOT add extras.
54
+
55
+ ${assertionChecklist}
56
+ ═══════════════════════════════════════════════════
57
+ ` : ''}
58
+ ⚠️ CRITICAL RULES (STRICT ENFORCEMENT):
59
+ 1. DO NOT get stuck in read loops - if a snapshot is large, move on
60
+ 2. DO NOT over-analyze - just execute the steps
61
+ 3. **As soon as you complete the test → IMMEDIATELY return JSON**
62
+ 4. **NO screenshots required** - just execute and return JSON
63
+ 5. **If test is done, STOP - don't try to be perfect**
64
+ 6. **USE VALUES FROM THE TEST SPEC** - if the spec provides specific values, use them exactly. Do NOT replace them with random data.
65
+ 7. **USE UNIQUE DATA ONLY when CREATING new resources** (e.g., sign-up forms, new accounts) to avoid "already taken" conflicts:
66
+ - For NEW emails (not provided in test spec): use random digits like "test84729@example.com"
67
+ - For NEW names (not provided in test spec): append random digits like "John84729"
68
+ - This does NOT apply to login credentials or test data explicitly provided in the spec
69
+
70
+ WHEN TO STOP (MANDATORY):
71
+ ✓ You've completed the test steps
72
+ ✓ Test outcome is visible (even briefly)
73
+ → **RETURN JSON IMMEDIATELY - DO NOT make any more tool calls**
74
+
75
+ DO NOT:
76
+ - Navigate to the same URL multiple times
77
+ - Use browser_run_code or browser_evaluate
78
+ - Read large snapshots repeatedly (max 2 snapshots per page)
79
+ - Take screenshots (optional, skip if slowing you down)
80
+ - Try to verify every single detail - focus on the MAIN outcome
81
+ - Click/scroll/interact after seeing the expected result
82
+ - Spend more than 2 minutes on any single page
83
+ - Try to click elements that aren't immediately visible
84
+
85
+ EXECUTION SEQUENCE (MANDATORY - FOLLOW STRICTLY):
86
+ 1. Execute the test steps efficiently (navigate, fill, click)
87
+ - Max 10-15 actions total
88
+ - If stuck, move on to next step
89
+ 2. Quick verification - check if main result is visible
90
+ - **If you see expected result → IMMEDIATELY return JSON**
91
+ - Don't try to make it perfect - good enough is enough
92
+ 3. **RETURN JSON AND STOP COMPLETELY**
93
+ - Format: { "success": true, "steps": ["step 1", ...], "browserClosed": true, "actions": [...] }
94
+ - MUST include: "success" (boolean), "steps" (array), "browserClosed" (boolean)
95
+ - Keep JSON CONCISE - short descriptions, no excessive detail
96
+ - After the closing brace }, DO NOT write ANYTHING
97
+ - NO commentary, NO explanations, NO additional text
98
+ - NO second JSON object
99
+ - Just the JSON, then STOP
100
+
101
+ IMPORTANT for 'actions' array (STRICT 1:1 MAPPING):
102
+ - Each entry MUST match EXACTLY ONE browser tool call.
103
+ - DO NOT group multiple tool calls into one action.
104
+ - DO NOT combine multiple 'fill' calls into one action.
105
+ - If you call browser_type 3 times for 3 fields, you MUST have 3 actions in the array.
106
+ - Include actual values/URLs in descriptions.
107
+ - Keep descriptions SHORT (5-10 words max).
108
+
109
+ IMPORTANT for 'assertions' array (USE THE CHECKLIST ABOVE):
110
+ - Your assertions array MUST match the ASSERTION CHECKLIST exactly - one entry per item
111
+ - If you verified it and it passed → "passed": true
112
+ - If you could NOT verify it or it wasn't found → "passed": false with evidence of what you saw instead
113
+ - Each assertion MUST include 'verifiedAfterAction' (0-based action index after which you checked)
114
+ - Format: {"description": "...", "passed": true/false, "verifiedAfterAction": N, "evidence": "..."}
115
+
116
+ 🔍 CRITICAL: CAPTURE ROBUST SELECTORS (for script generation)
117
+ For EACH action, capture multiple selector strategies in priority order:
118
+
119
+ 1. **Role + Name** (Most Robust - Accessibility-first):
120
+ - Role: button/textbox/link/etc
121
+ - Name: visible text or aria-label
122
+ Example: {"role": "button", "name": "Login"}
123
+
124
+ 2. **Stable Attributes** (Good Stability):
125
+ - name, type, placeholder, aria-label
126
+ Example: {"attributes": {"name": "username", "type": "text", "placeholder": "Enter username"}}
127
+
128
+ 3. **Partial Match** (For Dynamic Elements):
129
+ - Use starts-with for dynamic IDs/classes
130
+ Example: {"partialMatch": {"id": "^user-", "class": "^btn-"}}
131
+
132
+ 4. **Structural** (Fallback):
133
+ - Tag + position relative to stable landmark
134
+ Example: {"structure": "form input[type='text']:nth-of-type(1)"}
135
+
136
+ Format for actions with selectors:
137
+ {
138
+ "description": "Fill username field with 'joe'",
139
+ "reasoning": "Need to authenticate user",
140
+ "type": "fill",
141
+ "selectors": {
142
+ "role": {"role": "textbox", "name": "Username"},
143
+ "attributes": {"name": "username", "type": "text", "placeholder": "请输入账号"},
144
+ "structure": "form input[type='text']:first-of-type"
145
+ },
146
+ "value": "joe"
147
+ }
148
+
149
+ IMPORTANT for 'evidenceScreenshots' (array) - OPTIONAL:
150
+ - Screenshots are OPTIONAL - only take if helpful
151
+ - If you take screenshots, use descriptive filenames
152
+ - Filename pattern: "{step-number}-{action-or-state}.png"
153
+ - Keep it minimal - test execution is more important than documentation
154
+
155
+ ════════════════════════════════════════════════════════════
156
+ 🚨 CRITICAL JSON OUTPUT RULES 🚨
157
+
158
+ YOU MUST OUTPUT JSON USING ONE OF THESE TWO FORMATS:
159
+
160
+ ✅ FORMAT 1 (BEST - Use This!):
161
+ Think/plan/explain first, THEN output ONLY JSON:
162
+
163
+ I'll navigate to the login page and fill the form...
164
+ [... use browser tools ...]
165
+ [... complete test execution ...]
166
+
167
+ {"success": true, "steps": [...], "browserClosed": true}
168
+
169
+ NO TEXT AFTER THE JSON! Stop immediately after }.
170
+
171
+ ✅ FORMAT 2 (If you need to explain after):
172
+ Use delimiters to separate JSON from explanations:
173
+
174
+ I'm executing the test now...
175
+ [... use browser tools ...]
176
+
177
+ ===JSON_START===
178
+ {"success": true, "steps": [...], "browserClosed": true}
179
+ ===JSON_END===
180
+
181
+ Now let me explain what happened...
182
+
183
+ ❌ WRONG - DO NOT DO THIS:
184
+ {"success": true, "steps": [...]} followed by more explanations
185
+
186
+ ❌ WRONG - DO NOT STREAM JSON LETTER BY LETTER:
187
+ The test completed successfully, here's the result: { "success": t
188
+
189
+ ✅ CORRECT - Output complete JSON in one block:
190
+ {"success": true, "steps": ["step 1", "step 2"], "browserClosed": true}
191
+
192
+ REMEMBER: After the final }, you MUST STOP or use ===JSON_END===
193
+ ════════════════════════════════════════════════════════════
194
+ `;
195
+ },
196
+
197
+ outputSchema: z.object({
198
+ success: z.boolean()
199
+ .describe('Whether the test execution completed successfully'),
200
+
201
+ steps: z.array(z.string())
202
+ .describe('Array of test steps executed'),
203
+
204
+ finalUrl: z.string()
205
+ .optional()
206
+ .describe('Final URL after test execution'),
207
+
208
+ actions: z.array(z.any())
209
+ .optional()
210
+ .describe('Detailed array of actions performed with descriptions and reasoning'),
211
+
212
+ assertions: z.array(z.object({
213
+ description: z.string()
214
+ .describe('What was verified'),
215
+ passed: z.boolean()
216
+ .describe('Whether the assertion passed'),
217
+ verifiedAfterAction: z.number()
218
+ .describe('Index of the action after which this was verified (0-based, matches actions array index) - REQUIRED'),
219
+ evidence: z.string()
220
+ .optional()
221
+ .describe('Brief evidence of what was observed')
222
+ }))
223
+ .optional()
224
+ .describe('Array of assertions made during test'),
225
+
226
+ waits: z.array(z.any())
227
+ .optional()
228
+ .describe('Array of waits needed for proper test execution'),
229
+
230
+ evidenceScreenshots: z.array(z.object({
231
+ filename: z.string()
232
+ .describe('Descriptive filename pattern: {step-number}-{action-or-state}.png'),
233
+
234
+ description: z.string()
235
+ .describe('What the screenshot shows and why it is evidence'),
236
+
237
+ verdict: z.enum(['pass', 'fail', 'info'])
238
+ .describe('Test verdict: pass/fail for validation points, info for checkpoints')
239
+ }))
240
+ .optional()
241
+ .describe('Array of screenshots taken at key validation points throughout the test'),
242
+
243
+ browserClosed: z.boolean()
244
+ .describe('Whether the browser was properly closed (should always be true)'),
245
+
246
+ notes: z.string()
247
+ .optional()
248
+ .describe('Additional notes or observations. REQUIRED when success=false to explain why test failed or could not execute')
249
+ })
250
+ };
@@ -0,0 +1,77 @@
1
+ import { z } from '@zibby/core';
2
+ import { formatRecordedActions, formatAssertionsWithResults } from './utils.js';
3
+
4
+ const GenerateScriptOutputSchema = z.object({
5
+ success: z.boolean(),
6
+ scriptPath: z.string(),
7
+ method: z.string()
8
+ });
9
+
10
+ export const generateScriptNode = {
11
+ name: 'generate_script',
12
+ outputSchema: GenerateScriptOutputSchema,
13
+ timeout: 360000,
14
+
15
+ prompt: (state) => {
16
+ const exec = state.execute_live || {};
17
+ const preflight = state.preflight || {};
18
+
19
+ const actionsBlock = formatRecordedActions(state.sessionPath, exec.actions);
20
+ const assertionsBlock = formatAssertionsWithResults(
21
+ preflight.assertions,
22
+ exec.assertions,
23
+ exec.notes,
24
+ exec.finalUrl
25
+ );
26
+
27
+ return `Generate and verify Playwright test at ${state.outputPath}
28
+
29
+ Test Spec:
30
+ ${state.testSpec}
31
+
32
+ Live Execution Summary:
33
+ - Success: ${exec.success}
34
+ - Steps: ${JSON.stringify(exec.steps)}
35
+ - Final URL: ${exec.finalUrl || 'unknown'}
36
+ ${actionsBlock}
37
+ ${assertionsBlock}
38
+
39
+ IMPORTS AND PATTERN:
40
+ \`\`\`javascript
41
+ import { test, expect } from '@playwright/test';
42
+ import { StableIdRuntime } from '@zibby/core';
43
+
44
+ test('Test Name', async ({ page }) => {
45
+ await page.goto('https://...');
46
+ await StableIdRuntime.injectStableIds(page);
47
+ // Elements WITH stable IDs — use StableIdRuntime:
48
+ await StableIdRuntime.fillWithRetry(page, 'zibby-xxxxx', 'value');
49
+ await StableIdRuntime.clickWithRetry(page, 'zibby-xxxxx');
50
+ // Elements WITHOUT stable IDs (NO_STABLE_ID) — use native Playwright selectors:
51
+ await page.getByText('visible text').click();
52
+ await page.getByRole('button', { name: 'Submit' }).click();
53
+ await page.getByPlaceholder('placeholder text').fill('value');
54
+ await expect(page).toHaveURL(/expected-url/);
55
+ });
56
+ \`\`\`
57
+
58
+ RULES:
59
+ 1. First navigate → page.goto(), skip subsequent navigates
60
+ 2. After goto, call StableIdRuntime.injectStableIds(page)
61
+ 3. Use EXACT stable IDs from recorded actions when available
62
+ 4. For [NO_STABLE_ID] actions, use the fallback selector (getByText, getByRole, getByPlaceholder). These are typically non-semantic elements like spans acting as buttons — use the visible text to target them.
63
+ 5. Skip duplicate consecutive clicks on same stableId
64
+ 6. No comments in generated code
65
+ 7. Implement ALL assertions from the list above
66
+ 8. If an assertion fails after retries, comment it out with a TODO (don't delete it)
67
+
68
+ WORKFLOW:
69
+ 1. Write test to ${state.outputPath}
70
+ 2. Run: PLAYWRIGHT_HEADLESS=1 npx playwright test ${state.outputPath} --reporter=line --timeout=30000
71
+ 3. If fails: make ONE targeted fix (longer timeout, different selector, short wait)
72
+ 4. MAX 2 ATTEMPTS then STOP
73
+
74
+ The test runs in: ${state.cwd || 'project root'}
75
+ `;
76
+ },
77
+ };
@@ -0,0 +1,3 @@
1
+ export { preflightNode } from './preflight.js';
2
+ export { executeLiveNode } from './execute-live.js';
3
+ export { generateScriptNode } from './generate-script.js';
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Preflight Node
3
+ *
4
+ * Pattern: Prompt-only node (no tools)
5
+ * Purpose: Analyze test spec and extract title + structured assertion checklist
6
+ *
7
+ * This runs before execution to define:
8
+ * - A concise test title
9
+ * - The complete list of assertions that must be verified
10
+ *
11
+ * Downstream nodes receive this as their contract:
12
+ * - execute_live: must report passed/failed for each assertion
13
+ * - generate_script: must implement each assertion in the test
14
+ */
15
+
16
+ import { z } from '@zibby/core';
17
+ import { writeFileSync } from 'fs';
18
+ import { join } from 'path';
19
+
20
+ const AssertionSchema = z.object({
21
+ description: z.string().describe('What to verify (e.g., "User is redirected to dashboard")'),
22
+ expected: z.string().describe('What the expected outcome looks like (e.g., "URL contains /dashboard")')
23
+ });
24
+
25
+ const PreflightOutputSchema = z.object({
26
+ title: z.string().describe('Concise test title (5-10 words, action-oriented). Prefix with ticket ID if found.'),
27
+ assertions: z.array(AssertionSchema).describe('Every expected result from the spec as a verifiable assertion')
28
+ });
29
+
30
+ export const preflightNode = {
31
+ name: 'preflight',
32
+
33
+ async onComplete(state, result) {
34
+ const sessionPath = state.sessionPath || process.env.ZIBBY_SESSION_PATH;
35
+ if (sessionPath && result.title) {
36
+ try {
37
+ writeFileSync(join(sessionPath, 'title.txt'), result.title, 'utf-8');
38
+ console.log(`Saved title: "${result.title}"`);
39
+ } catch (error) {
40
+ console.warn(`⚠️ Could not save title.txt: ${error.message}`);
41
+ }
42
+ }
43
+ return result;
44
+ },
45
+
46
+ prompt: (state) => `Analyze this test specification and extract:
47
+ 1. A concise test title (5-10 words, action-oriented). If you find a ticket ID (e.g., PROJ-123, ACME-456), prefix the title with it.
48
+ 2. Every expected result as a verifiable assertion. Each assertion must be something the browser can check after execution.
49
+
50
+ Test Spec:
51
+ ${state.testSpec}
52
+
53
+ IMPORTANT: You MUST create ONE assertion for EACH expected result in the spec. Do NOT skip any.
54
+
55
+ Return ONLY this JSON:
56
+ { "title": "TICKET-ID: Short action title", "assertions": [ { "description": "...", "expected": "..." }, ... ] }`,
57
+
58
+ outputSchema: PreflightOutputSchema
59
+ };
@@ -0,0 +1,154 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+
3
+ const ACTIONABLE_EVENTS = ['navigate', 'type', 'fill', 'click', 'select_option', 'select'];
4
+
5
+ /**
6
+ * Load recorded browser events from a session's events.json,
7
+ * filtering to only actionable events (navigate, fill, click, etc.)
8
+ */
9
+ export function loadRecordedActions(sessionPath, nodeName = 'execute_live') {
10
+ const eventsPath = `${sessionPath}/${nodeName}/events.json`;
11
+ if (!existsSync(eventsPath)) return [];
12
+ try {
13
+ return JSON.parse(readFileSync(eventsPath, 'utf-8'))
14
+ .filter(e => ACTIONABLE_EVENTS.includes(e.type))
15
+ .map(e => ({
16
+ type: e.type,
17
+ stableId: e.stableId || e.data?.stableId,
18
+ value: e.data?.text || e.data?.params?.text || e.data?.url || e.data?.params?.url || e.data?.values?.[0] || e.data?.params?.values?.[0],
19
+ element: e.data?.element || e.data?.params?.element,
20
+ reasoning: e.reasoning || e.description
21
+ }));
22
+ } catch (e) {
23
+ console.error('Failed to read events:', e.message);
24
+ return [];
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Index AI-captured selectors by element description (lowercase)
30
+ * for fuzzy matching when building fallback selectors.
31
+ */
32
+ export function buildSelectorMap(actions) {
33
+ const map = {};
34
+ for (const a of actions || []) {
35
+ if (a.selectors?.role) {
36
+ map[(a.description || '').toLowerCase()] = a.selectors;
37
+ }
38
+ }
39
+ return map;
40
+ }
41
+
42
+ /** Extract meaningful tokens from a description string for fuzzy matching. */
43
+ function extractTokens(str) {
44
+ return str.replace(/[-–—]/g, ' ').split(/\s+/).filter(t => t.length > 1);
45
+ }
46
+
47
+ /** Score how well two descriptions match by counting shared tokens. */
48
+ function tokenOverlapScore(a, b) {
49
+ const tokensA = extractTokens(a.toLowerCase());
50
+ const tokensB = extractTokens(b.toLowerCase());
51
+ return tokensA.filter(t => tokensB.some(tb => tb.includes(t) || t.includes(tb))).length;
52
+ }
53
+
54
+ /** Find a fallback selector for an element by fuzzy-matching against the selector map. */
55
+ function findFallbackSelector(elementKey, selectorMap) {
56
+ let bestMatch = null;
57
+ let bestScore = 0;
58
+ for (const [k, selectors] of Object.entries(selectorMap)) {
59
+ const score = tokenOverlapScore(elementKey, k);
60
+ if (score > bestScore) {
61
+ bestScore = score;
62
+ bestMatch = selectors;
63
+ }
64
+ }
65
+ if (!bestMatch?.role || bestScore < 1) return '';
66
+ const { role, name } = bestMatch.role;
67
+ if (role === 'generic' || role === 'none') {
68
+ return name ? ` | fallback: getByText('${name}')` : '';
69
+ }
70
+ return ` | fallback: getByRole('${role}'${name ? `, { name: '${name}' }` : ''})`;
71
+ }
72
+
73
+ /** Extract visible text hint from element description for use as a getByText fallback. */
74
+ function extractTextHint(elementDesc) {
75
+ if (!elementDesc) return '';
76
+ const cnMatch = elementDesc.match(/[\u4e00-\u9fff\u3000-\u303f]+/g);
77
+ if (cnMatch) return cnMatch.join('');
78
+ const afterDash = elementDesc.split(/[-–—]\s*/).pop()?.trim();
79
+ if (afterDash && afterDash !== elementDesc) return afterDash;
80
+ return '';
81
+ }
82
+
83
+ /** Format a single recorded event into a human-readable action line with stableId and fallback selector. */
84
+ export function formatAction(event, index, selectorMap) {
85
+ const num = index + 1;
86
+ const reason = event.reasoning ? ` | reason: "${event.reasoning}"` : '';
87
+ const hasStableId = event.stableId && event.stableId !== 'undefined';
88
+ const fallback = findFallbackSelector((event.element || '').toLowerCase(), selectorMap);
89
+ const idPart = hasStableId ? `[stableId="${event.stableId}"]` : '[NO_STABLE_ID]';
90
+
91
+ let textFallback = '';
92
+ if (!hasStableId && !fallback) {
93
+ const hint = extractTextHint(event.element);
94
+ if (hint) textFallback = ` | fallback: getByText('${hint}')`;
95
+ }
96
+
97
+ switch (event.type) {
98
+ case 'navigate':
99
+ return `${num}. NAVIGATE to: ${event.value}${reason}`;
100
+ case 'click':
101
+ return `${num}. CLICK ${idPart} - ${event.element || 'element'}${fallback || textFallback}${reason}`;
102
+ case 'fill': case 'type':
103
+ return `${num}. FILL ${idPart} with "${event.value}" - ${event.element || 'field'}${fallback || textFallback}${reason}`;
104
+ case 'select': case 'select_option':
105
+ return `${num}. SELECT ${idPart} option "${event.value}" - ${event.element || 'dropdown'}${fallback || textFallback}${reason}`;
106
+ default:
107
+ return `${num}. ${event.type} ${hasStableId ? idPart : ''} ${event.value || ''}${fallback || textFallback}${reason}`;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Build the full RECORDED ACTIONS prompt block by loading events.json
113
+ * and merging with AI-captured selectors from execution output.
114
+ */
115
+ export function formatRecordedActions(sessionPath, executionActions) {
116
+ const recorded = loadRecordedActions(sessionPath);
117
+ if (recorded.length === 0) return '';
118
+ const selectorMap = buildSelectorMap(executionActions);
119
+ return `RECORDED ACTIONS:\n${recorded.map((e, i) => formatAction(e, i, selectorMap)).join('\n')}\n\nGenerate a CLEAN script from these — not a 1:1 replay. If stableId fails, use the fallback selector.`;
120
+ }
121
+
122
+ /** Format preflight assertions into a simple numbered checklist for the execute_live prompt. */
123
+ export function formatAssertionChecklist(assertions) {
124
+ if (!assertions?.length) return '';
125
+ return assertions.map((a, i) =>
126
+ `${i + 1}. ${a.description} → expected: ${a.expected}`
127
+ ).join('\n');
128
+ }
129
+
130
+ /**
131
+ * Merge preflight assertion definitions with execution results (pass/fail/evidence)
132
+ * into an ASSERTIONS TO IMPLEMENT block for the generate_script prompt.
133
+ */
134
+ export function formatAssertionsWithResults(preflightAssertions, executionAssertions, notes, finalUrl) {
135
+ if (!preflightAssertions?.length && !executionAssertions?.length) return '';
136
+
137
+ const lines = preflightAssertions?.length > 0
138
+ ? preflightAssertions.map((a, i) => {
139
+ const exec = executionAssertions?.[i];
140
+ const status = exec ? (exec.passed ? '✅ PASSED' : '❌ FAILED') : '⚠️ NOT CHECKED';
141
+ const evidence = exec?.evidence ? ` | evidence: "${exec.evidence}"` : '';
142
+ return `${i + 1}. [${status}] ${a.description} → expected: ${a.expected}${evidence}`;
143
+ })
144
+ : executionAssertions.map((a, i) =>
145
+ typeof a === 'object'
146
+ ? `${i + 1}. ${a.description || a.type}: expected "${a.expected}", actual "${a.actual}"`
147
+ : `${i + 1}. ${a}`
148
+ );
149
+
150
+ return `ASSERTIONS TO IMPLEMENT:
151
+ ${lines.join('\n')}
152
+
153
+ Final URL: ${finalUrl || 'unknown'}${notes ? `\nAI OBSERVATION: ${notes}` : ''}`;
154
+ }