nodebench-mcp 2.20.1 → 2.21.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.
@@ -0,0 +1,614 @@
1
+ /**
2
+ * UI/UX Full Dive v2 — Advanced Tools
3
+ *
4
+ * Deep interaction testing, screenshot capture, design auditing,
5
+ * backend context linking, changelog tracking, and walkthrough generation.
6
+ *
7
+ * These tools complement the base dive tools (uiUxDiveTools.ts) and work
8
+ * with the MCP Bridge (playwright-mcp) for browser automation.
9
+ *
10
+ * Architecture:
11
+ * - Agent uses MCP Bridge to drive the browser (navigate, click, type, screenshot)
12
+ * - These tools provide structured storage and analysis on top of bridge actions
13
+ * - Screenshots are saved to disk + thumbnail stored in DB for fast retrieval
14
+ * - Interaction tests define preconditions → steps → expected/actual per step
15
+ * - Design audits compare computed styles across components for inconsistencies
16
+ * - Backend links connect UI components to API endpoints, Convex functions, DB tables
17
+ * - Changelogs track before/after with screenshots when fixes are applied
18
+ */
19
+ import { mkdirSync, writeFileSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { homedir } from "node:os";
22
+ import { getDb } from "../db.js";
23
+ function genId(prefix) {
24
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
25
+ }
26
+ function screenshotDir() {
27
+ const dir = join(homedir(), ".nodebench", "dive-screenshots");
28
+ mkdirSync(dir, { recursive: true });
29
+ return dir;
30
+ }
31
+ export const uiUxDiveAdvancedTools = [
32
+ // ── 1. Save a labeled screenshot ──────────────────────────────────────
33
+ {
34
+ name: "dive_save_screenshot",
35
+ description: "Save a screenshot during a dive session. Pass base64 image data (from bridge's browser_take_screenshot) or a file path. The screenshot is stored on disk and indexed in the DB with labels, route, component, and test references. Returns a screenshot_id you can link to bugs, test steps, design issues, and changelogs. This creates the visual evidence trail for the entire dive.",
36
+ inputSchema: {
37
+ type: "object",
38
+ properties: {
39
+ sessionId: { type: "string", description: "Dive session ID" },
40
+ label: { type: "string", description: "Human-readable label (e.g. 'Login form - initial state', 'After clicking submit')" },
41
+ base64Data: { type: "string", description: "Base64-encoded image data (from browser_take_screenshot)" },
42
+ filePath: { type: "string", description: "Alternative: path to an existing screenshot file" },
43
+ componentId: { type: "string", description: "Component this screenshot is for (optional)" },
44
+ route: { type: "string", description: "Current route/URL (optional)" },
45
+ testId: { type: "string", description: "Interaction test this belongs to (optional)" },
46
+ stepIndex: { type: "number", description: "Step index within a test (optional)" },
47
+ metadata: { type: "object", description: "Additional metadata (optional)" },
48
+ },
49
+ required: ["sessionId", "label"],
50
+ },
51
+ handler: async (args) => {
52
+ const { sessionId, label, base64Data, filePath, componentId, route, testId, stepIndex, metadata } = args;
53
+ const db = getDb();
54
+ const session = db.prepare("SELECT id FROM ui_dive_sessions WHERE id = ?").get(sessionId);
55
+ if (!session)
56
+ return { error: true, message: `Session not found: ${sessionId}` };
57
+ const id = genId("ss");
58
+ let savedPath = filePath ?? null;
59
+ // Save base64 data to disk
60
+ if (base64Data && !filePath) {
61
+ const dir = screenshotDir();
62
+ const filename = `${sessionId}_${id}.png`;
63
+ savedPath = join(dir, filename);
64
+ try {
65
+ const buffer = Buffer.from(base64Data, "base64");
66
+ writeFileSync(savedPath, buffer);
67
+ }
68
+ catch (e) {
69
+ return { error: true, message: `Failed to save screenshot: ${e.message}` };
70
+ }
71
+ }
72
+ // Store a small thumbnail (first 500 chars of base64 for quick preview)
73
+ const thumbnail = base64Data ? base64Data.slice(0, 500) : null;
74
+ db.prepare(`INSERT INTO ui_dive_screenshots (id, session_id, component_id, test_id, step_index, label, route, file_path, base64_thumbnail, metadata)
75
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, sessionId, componentId ?? null, testId ?? null, stepIndex ?? null, label, route ?? null, savedPath, thumbnail, metadata ? JSON.stringify(metadata) : null);
76
+ return {
77
+ screenshotId: id,
78
+ label,
79
+ filePath: savedPath,
80
+ componentId: componentId ?? null,
81
+ route: route ?? null,
82
+ _hint: `Screenshot saved. Reference it in bugs: tag_ui_bug({ screenshotRef: "${id}" }), test steps, design issues, or changelogs.`,
83
+ };
84
+ },
85
+ },
86
+ // ── 2. Run a structured interaction test ──────────────────────────────
87
+ {
88
+ name: "dive_interaction_test",
89
+ description: "Define and track a structured interaction test for a component. Provide preconditions and a sequence of test steps (action, target, expected outcome). The agent executes each step via the MCP Bridge (browser_click, browser_type, etc.), takes screenshots, and records actual results here. Each step gets pass/fail status. The test aggregates into an overall result. This creates the detailed walkthrough with preconditions, steps, expected vs actual, and visual evidence at each step.",
90
+ inputSchema: {
91
+ type: "object",
92
+ properties: {
93
+ sessionId: { type: "string", description: "Dive session ID" },
94
+ componentId: { type: "string", description: "Component being tested" },
95
+ testName: { type: "string", description: "Test name (e.g. 'Login form submission', 'Dark mode toggle')" },
96
+ description: { type: "string", description: "What this test validates" },
97
+ preconditions: {
98
+ type: "array",
99
+ description: "List of preconditions (e.g. ['User is logged out', 'Browser at /login', 'Dark mode is off'])",
100
+ items: { type: "string" },
101
+ },
102
+ steps: {
103
+ type: "array",
104
+ description: "Test steps to execute and track",
105
+ items: {
106
+ type: "object",
107
+ properties: {
108
+ action: { type: "string", description: "Action: click, type, navigate, hover, scroll, assert, wait, screenshot" },
109
+ target: { type: "string", description: "CSS selector, URL, or description" },
110
+ inputValue: { type: "string", description: "Value to type/enter (for type action)" },
111
+ expected: { type: "string", description: "Expected outcome (e.g. 'Form submits', 'Error message appears', 'Redirects to /dashboard')" },
112
+ screenshotLabel: { type: "string", description: "Label for the screenshot at this step (optional)" },
113
+ },
114
+ required: ["action", "expected"],
115
+ },
116
+ },
117
+ metadata: { type: "object", description: "Optional metadata" },
118
+ },
119
+ required: ["sessionId", "componentId", "testName", "steps"],
120
+ },
121
+ handler: async (args) => {
122
+ const { sessionId, componentId, testName, description, preconditions, steps, metadata } = args;
123
+ const db = getDb();
124
+ const session = db.prepare("SELECT id FROM ui_dive_sessions WHERE id = ?").get(sessionId);
125
+ if (!session)
126
+ return { error: true, message: `Session not found: ${sessionId}` };
127
+ const comp = db.prepare("SELECT id FROM ui_dive_components WHERE id = ?").get(componentId);
128
+ if (!comp)
129
+ return { error: true, message: `Component not found: ${componentId}` };
130
+ const testId = genId("test");
131
+ db.prepare(`INSERT INTO ui_dive_interaction_tests (id, session_id, component_id, test_name, description, preconditions, steps_total, metadata)
132
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(testId, sessionId, componentId, testName, description ?? null, preconditions ? JSON.stringify(preconditions) : null, steps.length, metadata ? JSON.stringify(metadata) : null);
133
+ // Create step rows
134
+ const stepIds = [];
135
+ for (let i = 0; i < steps.length; i++) {
136
+ const s = steps[i];
137
+ const stepId = genId("step");
138
+ db.prepare(`INSERT INTO ui_dive_test_steps (id, test_id, step_index, action, target, input_value, expected)
139
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run(stepId, testId, i, s.action, s.target ?? null, s.inputValue ?? null, s.expected);
140
+ stepIds.push(stepId);
141
+ }
142
+ return {
143
+ testId,
144
+ testName,
145
+ componentId,
146
+ stepsTotal: steps.length,
147
+ stepIds,
148
+ status: "pending",
149
+ _workflow: [
150
+ "For each step, the agent should:",
151
+ "1. Execute the action via MCP Bridge (browser_click, browser_type, etc.)",
152
+ "2. Take a screenshot via bridge (browser_take_screenshot)",
153
+ "3. Save it: dive_save_screenshot({ testId, stepIndex, label, base64Data })",
154
+ "4. Record result: dive_record_test_step({ testId, stepIndex, actual, status, screenshotId })",
155
+ "5. After all steps: dive completes the test automatically",
156
+ ],
157
+ _hint: `Test created with ${steps.length} steps. Execute each step and record results with dive_record_test_step.`,
158
+ };
159
+ },
160
+ },
161
+ // ── 3. Record a test step result ──────────────────────────────────────
162
+ {
163
+ name: "dive_record_test_step",
164
+ description: "Record the actual result of a test step after executing it via the MCP Bridge. Compare expected vs actual, attach a screenshot, and mark pass/fail. When all steps are recorded, the test is automatically completed with an overall status.",
165
+ inputSchema: {
166
+ type: "object",
167
+ properties: {
168
+ testId: { type: "string", description: "Interaction test ID from dive_interaction_test" },
169
+ stepIndex: { type: "number", description: "0-based step index" },
170
+ actual: { type: "string", description: "What actually happened" },
171
+ status: {
172
+ type: "string",
173
+ description: "Step result: passed, failed, skipped, blocked",
174
+ enum: ["passed", "failed", "skipped", "blocked"],
175
+ },
176
+ screenshotId: { type: "string", description: "Screenshot ID from dive_save_screenshot (optional)" },
177
+ observation: { type: "string", description: "Additional notes about this step" },
178
+ durationMs: { type: "number", description: "How long the step took" },
179
+ },
180
+ required: ["testId", "stepIndex", "status", "actual"],
181
+ },
182
+ handler: async (args) => {
183
+ const { testId, stepIndex, actual, status, screenshotId, observation, durationMs } = args;
184
+ const db = getDb();
185
+ const step = db.prepare("SELECT id, expected FROM ui_dive_test_steps WHERE test_id = ? AND step_index = ?").get(testId, stepIndex);
186
+ if (!step)
187
+ return { error: true, message: `Step not found: test=${testId}, index=${stepIndex}` };
188
+ db.prepare("UPDATE ui_dive_test_steps SET actual = ?, status = ?, screenshot_id = ?, observation = ?, duration_ms = ? WHERE id = ?").run(actual, status, screenshotId ?? null, observation ?? null, durationMs ?? null, step.id);
189
+ // Check if all steps are done → auto-complete the test
190
+ const test = db.prepare("SELECT steps_total FROM ui_dive_interaction_tests WHERE id = ?").get(testId);
191
+ const completed = db.prepare("SELECT COUNT(*) as c FROM ui_dive_test_steps WHERE test_id = ? AND status != 'pending'").get(testId);
192
+ const passed = db.prepare("SELECT COUNT(*) as c FROM ui_dive_test_steps WHERE test_id = ? AND status = 'passed'").get(testId);
193
+ const failed = db.prepare("SELECT COUNT(*) as c FROM ui_dive_test_steps WHERE test_id = ? AND status = 'failed'").get(testId);
194
+ const allDone = completed.c >= test.steps_total;
195
+ if (allDone) {
196
+ const overallStatus = failed.c > 0 ? "failed" : "passed";
197
+ db.prepare("UPDATE ui_dive_interaction_tests SET status = ?, steps_passed = ?, steps_failed = ?, completed_at = datetime('now') WHERE id = ?").run(overallStatus, passed.c, failed.c, testId);
198
+ }
199
+ else {
200
+ db.prepare("UPDATE ui_dive_interaction_tests SET steps_passed = ?, steps_failed = ? WHERE id = ?").run(passed.c, failed.c, testId);
201
+ }
202
+ return {
203
+ stepId: step.id,
204
+ stepIndex,
205
+ expected: step.expected,
206
+ actual,
207
+ status,
208
+ match: status === "passed",
209
+ screenshotId: screenshotId ?? null,
210
+ testProgress: `${completed.c}/${test.steps_total}`,
211
+ testComplete: allDone,
212
+ ...(allDone ? { testStatus: failed.c > 0 ? "failed" : "passed" } : {}),
213
+ _hint: allDone
214
+ ? `Test complete: ${passed.c} passed, ${failed.c} failed.`
215
+ : `Step ${stepIndex} recorded. ${test.steps_total - completed.c} steps remaining.`,
216
+ };
217
+ },
218
+ },
219
+ // ── 4. Tag a design inconsistency ─────────────────────────────────────
220
+ {
221
+ name: "dive_design_issue",
222
+ description: "Tag a design inconsistency found during the dive. Covers visual problems like color mismatches, spacing deviations, font inconsistencies, alignment issues, contrast failures, responsive breakage, missing hover/focus states, and more. Link to a screenshot and the specific element. The agent uses bridge's browser_evaluate to extract computed styles and compare across components.",
223
+ inputSchema: {
224
+ type: "object",
225
+ properties: {
226
+ sessionId: { type: "string", description: "Dive session ID" },
227
+ componentId: { type: "string", description: "Component with the issue (optional)" },
228
+ issueType: {
229
+ type: "string",
230
+ description: "Type: color, spacing, font, alignment, contrast, responsive, hover_state, focus_state, animation, icon, border, shadow, z_index, overflow, consistency",
231
+ },
232
+ severity: {
233
+ type: "string",
234
+ description: "Severity: critical (broken UX), high (obvious visual bug), medium (noticeable deviation), low (minor polish)",
235
+ enum: ["critical", "high", "medium", "low"],
236
+ },
237
+ title: { type: "string", description: "Short description (e.g. 'Button color mismatch between header and sidebar')" },
238
+ description: { type: "string", description: "Detailed explanation" },
239
+ elementSelector: { type: "string", description: "CSS selector of the affected element" },
240
+ expectedValue: { type: "string", description: "What the design should be (e.g. '#3B82F6', '16px', 'Inter')" },
241
+ actualValue: { type: "string", description: "What was actually found (e.g. '#2563EB', '12px', 'system-ui')" },
242
+ screenshotId: { type: "string", description: "Screenshot showing the issue" },
243
+ route: { type: "string", description: "Route where the issue was found" },
244
+ metadata: { type: "object", description: "Additional context (e.g. { breakpoint: '768px', theme: 'dark' })" },
245
+ },
246
+ required: ["sessionId", "issueType", "title"],
247
+ },
248
+ handler: async (args) => {
249
+ const { sessionId, componentId, issueType, severity, title, description, elementSelector, expectedValue, actualValue, screenshotId, route, metadata } = args;
250
+ const db = getDb();
251
+ const session = db.prepare("SELECT id FROM ui_dive_sessions WHERE id = ?").get(sessionId);
252
+ if (!session)
253
+ return { error: true, message: `Session not found: ${sessionId}` };
254
+ const id = genId("design");
255
+ db.prepare(`INSERT INTO ui_dive_design_issues (id, session_id, component_id, issue_type, severity, title, description, element_selector, expected_value, actual_value, screenshot_id, route, metadata)
256
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, sessionId, componentId ?? null, issueType, severity ?? "medium", title, description ?? null, elementSelector ?? null, expectedValue ?? null, actualValue ?? null, screenshotId ?? null, route ?? null, metadata ? JSON.stringify(metadata) : null);
257
+ return {
258
+ designIssueId: id,
259
+ issueType,
260
+ severity: severity ?? "medium",
261
+ title,
262
+ expectedValue: expectedValue ?? null,
263
+ actualValue: actualValue ?? null,
264
+ _hint: `Design issue tagged. View all issues in the dive report. Fix it, then track with dive_changelog.`,
265
+ };
266
+ },
267
+ },
268
+ // ── 5. Link UI component to backend context ───────────────────────────
269
+ {
270
+ name: "dive_link_backend",
271
+ description: "Link a UI component to its backend dependencies. Connect components to API endpoints, Convex queries/mutations/actions, database tables, auth guards, WebSocket channels, or external services. This creates the full-stack traceability map — when a UI bug is found, you can immediately see which backend code is involved.",
272
+ inputSchema: {
273
+ type: "object",
274
+ properties: {
275
+ sessionId: { type: "string", description: "Dive session ID" },
276
+ componentId: { type: "string", description: "Component to link" },
277
+ links: {
278
+ type: "array",
279
+ description: "Backend references to link",
280
+ items: {
281
+ type: "object",
282
+ properties: {
283
+ linkType: {
284
+ type: "string",
285
+ description: "Type: convex_query, convex_mutation, convex_action, api_endpoint, db_table, auth_guard, websocket, external_service, env_var, cron_job",
286
+ },
287
+ path: { type: "string", description: "Path/identifier (e.g. 'api.domains.documents.documents.getSidebar', '/api/users', 'documents' table)" },
288
+ description: { type: "string", description: "What this backend dependency does for the component" },
289
+ method: { type: "string", description: "HTTP method for API endpoints (GET, POST, etc.)" },
290
+ },
291
+ required: ["linkType", "path"],
292
+ },
293
+ },
294
+ },
295
+ required: ["sessionId", "componentId", "links"],
296
+ },
297
+ handler: async (args) => {
298
+ const { sessionId, componentId, links } = args;
299
+ const db = getDb();
300
+ const session = db.prepare("SELECT id FROM ui_dive_sessions WHERE id = ?").get(sessionId);
301
+ if (!session)
302
+ return { error: true, message: `Session not found: ${sessionId}` };
303
+ const comp = db.prepare("SELECT id, name FROM ui_dive_components WHERE id = ?").get(componentId);
304
+ if (!comp)
305
+ return { error: true, message: `Component not found: ${componentId}` };
306
+ const ids = [];
307
+ for (const link of links) {
308
+ const id = genId("blink");
309
+ db.prepare(`INSERT INTO ui_dive_backend_links (id, session_id, component_id, link_type, path, description, method)
310
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, sessionId, componentId, link.linkType, link.path, link.description ?? null, link.method ?? null);
311
+ ids.push(id);
312
+ }
313
+ return {
314
+ componentId,
315
+ componentName: comp.name,
316
+ linksCreated: ids.length,
317
+ links: links.map((l, i) => ({ linkId: ids[i], ...l })),
318
+ _hint: `${ids.length} backend link(s) created for ${comp.name}. These will appear in the dive report and walkthrough.`,
319
+ };
320
+ },
321
+ },
322
+ // ── 6. Track a change (changelog entry) ───────────────────────────────
323
+ {
324
+ name: "dive_changelog",
325
+ description: "Record a change made to fix a bug, design issue, or improve a component. Links before/after screenshots to show what changed visually. Optionally references git commits and changed files. When the dive is re-run after fixes, the changelog provides a clear audit trail of what was wrong, what was changed, and how it looks now.",
326
+ inputSchema: {
327
+ type: "object",
328
+ properties: {
329
+ sessionId: { type: "string", description: "Dive session ID" },
330
+ componentId: { type: "string", description: "Component that was changed (optional)" },
331
+ changeType: {
332
+ type: "string",
333
+ description: "Type: bugfix, design_fix, feature, refactor, accessibility, performance, content, responsive",
334
+ },
335
+ description: { type: "string", description: "What was changed and why" },
336
+ beforeScreenshotId: { type: "string", description: "Screenshot before the change (from dive_save_screenshot)" },
337
+ afterScreenshotId: { type: "string", description: "Screenshot after the change" },
338
+ filesChanged: {
339
+ type: "array",
340
+ description: "List of files that were modified",
341
+ items: { type: "string" },
342
+ },
343
+ gitCommit: { type: "string", description: "Git commit hash (optional)" },
344
+ metadata: { type: "object", description: "Additional context (e.g. { bugId: '...', designIssueId: '...' })" },
345
+ },
346
+ required: ["sessionId", "changeType", "description"],
347
+ },
348
+ handler: async (args) => {
349
+ const { sessionId, componentId, changeType, description, beforeScreenshotId, afterScreenshotId, filesChanged, gitCommit, metadata } = args;
350
+ const db = getDb();
351
+ const session = db.prepare("SELECT id FROM ui_dive_sessions WHERE id = ?").get(sessionId);
352
+ if (!session)
353
+ return { error: true, message: `Session not found: ${sessionId}` };
354
+ const id = genId("chg");
355
+ db.prepare(`INSERT INTO ui_dive_changelogs (id, session_id, component_id, change_type, description, before_screenshot_id, after_screenshot_id, files_changed, git_commit, metadata)
356
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, sessionId, componentId ?? null, changeType, description, beforeScreenshotId ?? null, afterScreenshotId ?? null, filesChanged ? JSON.stringify(filesChanged) : null, gitCommit ?? null, metadata ? JSON.stringify(metadata) : null);
357
+ return {
358
+ changelogId: id,
359
+ changeType,
360
+ description,
361
+ beforeScreenshotId: beforeScreenshotId ?? null,
362
+ afterScreenshotId: afterScreenshotId ?? null,
363
+ filesChanged: filesChanged ?? [],
364
+ gitCommit: gitCommit ?? null,
365
+ _hint: "Changelog entry recorded. It will appear in the dive report and walkthrough.",
366
+ };
367
+ },
368
+ },
369
+ // ── 7. Generate a complete walkthrough ────────────────────────────────
370
+ {
371
+ name: "dive_walkthrough",
372
+ description: "Generate a comprehensive page-by-page, component-by-component walkthrough document for a dive session. Includes: route map with source files, component tree, interaction test results with pass/fail per step, screenshots referenced at each point, design issues found, backend dependencies, console errors, and changelog entries. This is the final deliverable — a complete QA document that an agent or human can review.",
373
+ inputSchema: {
374
+ type: "object",
375
+ properties: {
376
+ sessionId: { type: "string", description: "Dive session ID" },
377
+ format: {
378
+ type: "string",
379
+ description: "Output format: markdown (readable), json (structured), summary (condensed)",
380
+ enum: ["markdown", "json", "summary"],
381
+ },
382
+ includeScreenshotPaths: {
383
+ type: "boolean",
384
+ description: "Include file paths to screenshots (default: true)",
385
+ },
386
+ },
387
+ required: ["sessionId"],
388
+ },
389
+ handler: async (args) => {
390
+ const { sessionId, format, includeScreenshotPaths } = args;
391
+ const db = getDb();
392
+ const session = db.prepare("SELECT * FROM ui_dive_sessions WHERE id = ?").get(sessionId);
393
+ if (!session)
394
+ return { error: true, message: `Session not found: ${sessionId}` };
395
+ const components = db.prepare("SELECT * FROM ui_dive_components WHERE session_id = ? ORDER BY created_at").all(sessionId);
396
+ const bugs = db.prepare("SELECT * FROM ui_dive_bugs WHERE session_id = ? ORDER BY severity, created_at").all(sessionId);
397
+ const screenshots = db.prepare("SELECT * FROM ui_dive_screenshots WHERE session_id = ? ORDER BY created_at").all(sessionId);
398
+ const tests = db.prepare("SELECT * FROM ui_dive_interaction_tests WHERE session_id = ? ORDER BY created_at").all(sessionId);
399
+ const designIssues = db.prepare("SELECT * FROM ui_dive_design_issues WHERE session_id = ? ORDER BY severity, created_at").all(sessionId);
400
+ const backendLinks = db.prepare("SELECT * FROM ui_dive_backend_links WHERE session_id = ? ORDER BY component_id").all(sessionId);
401
+ const changelogs = db.prepare("SELECT * FROM ui_dive_changelogs WHERE session_id = ? ORDER BY created_at").all(sessionId);
402
+ // Load test steps for each test
403
+ const testSteps = {};
404
+ for (const test of tests) {
405
+ testSteps[test.id] = db.prepare("SELECT * FROM ui_dive_test_steps WHERE test_id = ? ORDER BY step_index").all(test.id);
406
+ }
407
+ // Group components by route (from metadata)
408
+ const routeGroups = new Map();
409
+ for (const comp of components) {
410
+ const meta = comp.metadata ? JSON.parse(comp.metadata) : {};
411
+ const route = meta.route ?? "(unrouted)";
412
+ if (!routeGroups.has(route))
413
+ routeGroups.set(route, []);
414
+ routeGroups.get(route).push({ ...comp, _meta: meta });
415
+ }
416
+ if (format === "json") {
417
+ return {
418
+ session: {
419
+ id: session.id,
420
+ appUrl: session.app_url,
421
+ appName: session.app_name,
422
+ status: session.status,
423
+ createdAt: session.created_at,
424
+ },
425
+ stats: {
426
+ routes: routeGroups.size,
427
+ components: components.length,
428
+ bugs: bugs.length,
429
+ screenshots: screenshots.length,
430
+ tests: tests.length,
431
+ testsPassed: tests.filter((t) => t.status === "passed").length,
432
+ testsFailed: tests.filter((t) => t.status === "failed").length,
433
+ designIssues: designIssues.length,
434
+ backendLinks: backendLinks.length,
435
+ changelogs: changelogs.length,
436
+ },
437
+ routes: Object.fromEntries([...routeGroups.entries()].map(([route, comps]) => [
438
+ route,
439
+ {
440
+ components: comps.map(c => ({
441
+ id: c.id,
442
+ name: c.name,
443
+ type: c.component_type,
444
+ status: c.status,
445
+ sourceFiles: c._meta.sourceFiles ?? [],
446
+ bugs: bugs.filter(b => b.component_id === c.id).map(b => ({ id: b.id, severity: b.severity, title: b.title })),
447
+ backendLinks: backendLinks.filter(l => l.component_id === c.id).map(l => ({ type: l.link_type, path: l.path })),
448
+ tests: tests.filter(t => t.component_id === c.id).map(t => ({
449
+ id: t.id,
450
+ name: t.test_name,
451
+ status: t.status,
452
+ passed: t.steps_passed,
453
+ failed: t.steps_failed,
454
+ total: t.steps_total,
455
+ steps: (testSteps[t.id] ?? []).map(s => ({
456
+ index: s.step_index,
457
+ action: s.action,
458
+ expected: s.expected,
459
+ actual: s.actual,
460
+ status: s.status,
461
+ screenshotId: s.screenshot_id,
462
+ })),
463
+ })),
464
+ })),
465
+ designIssues: designIssues.filter(d => comps.some(c => c.id === d.component_id)).map(d => ({
466
+ id: d.id,
467
+ type: d.issue_type,
468
+ severity: d.severity,
469
+ title: d.title,
470
+ expected: d.expected_value,
471
+ actual: d.actual_value,
472
+ })),
473
+ },
474
+ ])),
475
+ changelogs: changelogs.map(c => ({
476
+ id: c.id,
477
+ type: c.change_type,
478
+ description: c.description,
479
+ filesChanged: c.files_changed ? JSON.parse(c.files_changed) : [],
480
+ gitCommit: c.git_commit,
481
+ })),
482
+ screenshots: includeScreenshotPaths !== false
483
+ ? screenshots.map(s => ({ id: s.id, label: s.label, filePath: s.file_path, route: s.route }))
484
+ : undefined,
485
+ };
486
+ }
487
+ // Markdown format
488
+ const lines = [];
489
+ lines.push(`# UI/UX Dive Walkthrough: ${session.app_name ?? session.app_url}`);
490
+ lines.push(`**Session:** ${session.id} `);
491
+ lines.push(`**URL:** ${session.app_url} `);
492
+ lines.push(`**Date:** ${session.created_at} `);
493
+ lines.push(`**Status:** ${session.status}\n`);
494
+ // Stats
495
+ lines.push("## Summary\n");
496
+ lines.push(`| Metric | Value |`);
497
+ lines.push(`|--------|-------|`);
498
+ lines.push(`| Routes | ${routeGroups.size} |`);
499
+ lines.push(`| Components | ${components.length} |`);
500
+ lines.push(`| Interaction Tests | ${tests.length} (${tests.filter((t) => t.status === "passed").length} passed, ${tests.filter((t) => t.status === "failed").length} failed) |`);
501
+ lines.push(`| Bugs | ${bugs.length} |`);
502
+ lines.push(`| Design Issues | ${designIssues.length} |`);
503
+ lines.push(`| Screenshots | ${screenshots.length} |`);
504
+ lines.push(`| Backend Links | ${backendLinks.length} |`);
505
+ lines.push(`| Changelogs | ${changelogs.length} |`);
506
+ lines.push("");
507
+ // Route-by-route walkthrough
508
+ lines.push("## Route-by-Route Walkthrough\n");
509
+ for (const [route, comps] of routeGroups) {
510
+ const sourceFiles = comps[0]?._meta?.sourceFiles ?? [];
511
+ lines.push(`### ${route}\n`);
512
+ if (sourceFiles.length > 0)
513
+ lines.push(`**Source files:** ${sourceFiles.join(", ")} `);
514
+ lines.push(`**Components:** ${comps.length}\n`);
515
+ for (const comp of comps) {
516
+ lines.push(`#### ${comp.name} (${comp.component_type})`);
517
+ lines.push(`- **Status:** ${comp.status}`);
518
+ lines.push(`- **Interactions:** ${comp.interaction_count}`);
519
+ // Backend links
520
+ const compLinks = backendLinks.filter(l => l.component_id === comp.id);
521
+ if (compLinks.length > 0) {
522
+ lines.push(`- **Backend dependencies:**`);
523
+ for (const link of compLinks) {
524
+ lines.push(` - \`[${link.link_type}]\` ${link.path}${link.description ? ` -- ${link.description}` : ""}`);
525
+ }
526
+ }
527
+ // Tests for this component
528
+ const compTests = tests.filter(t => t.component_id === comp.id);
529
+ if (compTests.length > 0) {
530
+ lines.push(`\n**Interaction Tests:**\n`);
531
+ for (const test of compTests) {
532
+ const icon = test.status === "passed" ? "PASS" : test.status === "failed" ? "FAIL" : "PENDING";
533
+ lines.push(`##### [${icon}] ${test.test_name}`);
534
+ if (test.description)
535
+ lines.push(`${test.description}`);
536
+ if (test.preconditions) {
537
+ const preconds = JSON.parse(test.preconditions);
538
+ lines.push(`\n**Preconditions:**`);
539
+ for (const p of preconds)
540
+ lines.push(`- ${p}`);
541
+ }
542
+ lines.push(`\n| Step | Action | Expected | Actual | Status | Screenshot |`);
543
+ lines.push(`|------|--------|----------|--------|--------|------------|`);
544
+ for (const step of (testSteps[test.id] ?? [])) {
545
+ const stepIcon = step.status === "passed" ? "PASS" : step.status === "failed" ? "FAIL" : step.status;
546
+ const ssRef = step.screenshot_id ?? "-";
547
+ lines.push(`| ${step.step_index} | ${step.action} ${step.target ?? ""} | ${step.expected ?? ""} | ${step.actual ?? "-"} | ${stepIcon} | ${ssRef} |`);
548
+ }
549
+ lines.push("");
550
+ }
551
+ }
552
+ // Bugs
553
+ const compBugs = bugs.filter(b => b.component_id === comp.id);
554
+ if (compBugs.length > 0) {
555
+ lines.push(`\n**Bugs:**\n`);
556
+ for (const bug of compBugs) {
557
+ lines.push(`- **[${bug.severity.toUpperCase()}]** ${bug.title}`);
558
+ if (bug.description)
559
+ lines.push(` ${bug.description}`);
560
+ if (bug.screenshot_ref)
561
+ lines.push(` Screenshot: ${bug.screenshot_ref}`);
562
+ }
563
+ }
564
+ lines.push("");
565
+ }
566
+ // Design issues for this route
567
+ const routeDesignIssues = designIssues.filter(d => d.route === route);
568
+ if (routeDesignIssues.length > 0) {
569
+ lines.push(`**Design Issues on ${route}:**\n`);
570
+ for (const issue of routeDesignIssues) {
571
+ lines.push(`- **[${issue.severity.toUpperCase()}] ${issue.issue_type}:** ${issue.title}`);
572
+ if (issue.expected_value || issue.actual_value) {
573
+ lines.push(` Expected: ${issue.expected_value ?? "?"} | Actual: ${issue.actual_value ?? "?"}`);
574
+ }
575
+ }
576
+ lines.push("");
577
+ }
578
+ }
579
+ // Changelog
580
+ if (changelogs.length > 0) {
581
+ lines.push("## Changelog\n");
582
+ for (const chg of changelogs) {
583
+ lines.push(`### [${chg.change_type}] ${chg.description}`);
584
+ if (chg.files_changed) {
585
+ const files = JSON.parse(chg.files_changed);
586
+ lines.push(`**Files changed:** ${files.join(", ")}`);
587
+ }
588
+ if (chg.git_commit)
589
+ lines.push(`**Commit:** ${chg.git_commit}`);
590
+ if (chg.before_screenshot_id || chg.after_screenshot_id) {
591
+ lines.push(`**Before:** ${chg.before_screenshot_id ?? "-"} | **After:** ${chg.after_screenshot_id ?? "-"}`);
592
+ }
593
+ lines.push("");
594
+ }
595
+ }
596
+ const markdown = lines.join("\n");
597
+ return {
598
+ format: "markdown",
599
+ walkthrough: format === "summary" ? markdown.slice(0, 3000) : markdown,
600
+ stats: {
601
+ routes: routeGroups.size,
602
+ components: components.length,
603
+ tests: tests.length,
604
+ bugs: bugs.length,
605
+ designIssues: designIssues.length,
606
+ screenshots: screenshots.length,
607
+ backendLinks: backendLinks.length,
608
+ changelogs: changelogs.length,
609
+ },
610
+ };
611
+ },
612
+ },
613
+ ];
614
+ //# sourceMappingURL=uiUxDiveAdvancedTools.js.map