@bbearai/mcp-server 0.3.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.
package/src/index.ts ADDED
@@ -0,0 +1,4084 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * BugBear MCP Server
4
+ * Allows Claude Code to query and manage bug reports via Model Context Protocol
5
+ */
6
+
7
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
8
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
+ import {
10
+ CallToolRequestSchema,
11
+ ListToolsRequestSchema,
12
+ ListResourcesRequestSchema,
13
+ ReadResourceRequestSchema,
14
+ ListPromptsRequestSchema,
15
+ GetPromptRequestSchema,
16
+ } from '@modelcontextprotocol/sdk/types.js';
17
+ import { createClient, SupabaseClient } from '@supabase/supabase-js';
18
+
19
+ // Configuration from environment
20
+ const SUPABASE_URL = process.env.SUPABASE_URL || 'https://kyxgzjnqgvapvlnvqawz.supabase.co';
21
+ const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || '';
22
+ const PROJECT_ID = process.env.BUGBEAR_PROJECT_ID || '';
23
+
24
+ // Initialize Supabase client
25
+ let supabase: SupabaseClient;
26
+
27
+ function validateConfig() {
28
+ const errors: string[] = [];
29
+
30
+ if (!SUPABASE_ANON_KEY) {
31
+ errors.push('SUPABASE_ANON_KEY environment variable is required');
32
+ }
33
+
34
+ if (!PROJECT_ID) {
35
+ errors.push('BUGBEAR_PROJECT_ID environment variable is required');
36
+ }
37
+
38
+ // Basic UUID format validation for project ID
39
+ if (PROJECT_ID && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(PROJECT_ID)) {
40
+ errors.push('BUGBEAR_PROJECT_ID must be a valid UUID');
41
+ }
42
+
43
+ if (errors.length > 0) {
44
+ console.error('BugBear MCP Server configuration errors:');
45
+ errors.forEach(e => console.error(` - ${e}`));
46
+ process.exit(1);
47
+ }
48
+ }
49
+
50
+ function initSupabase() {
51
+ validateConfig();
52
+ supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
53
+ }
54
+
55
+ // Security helpers
56
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
57
+
58
+ function isValidUUID(str: string | undefined): boolean {
59
+ return typeof str === 'string' && UUID_REGEX.test(str);
60
+ }
61
+
62
+ function sanitizeSearchQuery(query: string | undefined): string | undefined {
63
+ if (!query) return undefined;
64
+ // Remove characters that could cause issues in LIKE queries
65
+ // Supabase escapes properly but we add an extra layer
66
+ return query
67
+ .replace(/[%_\\]/g, '') // Remove LIKE wildcards and escape char
68
+ .slice(0, 500); // Limit length
69
+ }
70
+
71
+ // Tool definitions
72
+ const tools = [
73
+ {
74
+ name: 'list_reports',
75
+ description: 'List recent bug reports for the project. Returns the most recent reports with their details.',
76
+ inputSchema: {
77
+ type: 'object' as const,
78
+ properties: {
79
+ limit: {
80
+ type: 'number',
81
+ description: 'Maximum number of reports to return (default: 10, max: 50)',
82
+ },
83
+ status: {
84
+ type: 'string',
85
+ enum: ['new', 'triaging', 'confirmed', 'in_progress', 'fixed', 'resolved', 'verified', 'wont_fix', 'duplicate'],
86
+ description: 'Filter by report status',
87
+ },
88
+ severity: {
89
+ type: 'string',
90
+ enum: ['critical', 'high', 'medium', 'low'],
91
+ description: 'Filter by severity',
92
+ },
93
+ type: {
94
+ type: 'string',
95
+ enum: ['bug', 'test_fail', 'feedback', 'suggestion'],
96
+ description: 'Filter by report type',
97
+ },
98
+ },
99
+ },
100
+ },
101
+ {
102
+ name: 'get_report',
103
+ description: 'Get detailed information about a specific bug report by ID',
104
+ inputSchema: {
105
+ type: 'object' as const,
106
+ properties: {
107
+ report_id: {
108
+ type: 'string',
109
+ description: 'The UUID of the report to retrieve',
110
+ },
111
+ },
112
+ required: ['report_id'],
113
+ },
114
+ },
115
+ {
116
+ name: 'search_reports',
117
+ description: 'Search bug reports by route, description, or other criteria',
118
+ inputSchema: {
119
+ type: 'object' as const,
120
+ properties: {
121
+ query: {
122
+ type: 'string',
123
+ description: 'Search query to match against report descriptions',
124
+ },
125
+ route: {
126
+ type: 'string',
127
+ description: 'Filter by specific route/path where the bug occurred',
128
+ },
129
+ },
130
+ },
131
+ },
132
+ {
133
+ name: 'update_report_status',
134
+ description: 'Update the status of a bug report',
135
+ inputSchema: {
136
+ type: 'object' as const,
137
+ properties: {
138
+ report_id: {
139
+ type: 'string',
140
+ description: 'The UUID of the report to update',
141
+ },
142
+ status: {
143
+ type: 'string',
144
+ enum: ['new', 'triaging', 'confirmed', 'in_progress', 'fixed', 'resolved', 'verified', 'wont_fix', 'duplicate'],
145
+ description: 'The new status for the report',
146
+ },
147
+ resolution: {
148
+ type: 'string',
149
+ description: 'Optional resolution notes when marking as resolved',
150
+ },
151
+ },
152
+ required: ['report_id', 'status'],
153
+ },
154
+ },
155
+ {
156
+ name: 'get_report_context',
157
+ description: 'Get the full debugging context for a report including console logs, network requests, and navigation history',
158
+ inputSchema: {
159
+ type: 'object' as const,
160
+ properties: {
161
+ report_id: {
162
+ type: 'string',
163
+ description: 'The UUID of the report',
164
+ },
165
+ },
166
+ required: ['report_id'],
167
+ },
168
+ },
169
+ {
170
+ name: 'get_project_info',
171
+ description: 'Get project information including QA tracks, test case counts, and common bug patterns',
172
+ inputSchema: {
173
+ type: 'object' as const,
174
+ properties: {},
175
+ },
176
+ },
177
+ {
178
+ name: 'get_qa_tracks',
179
+ description: 'Get all QA tracks for the project with their test templates and requirements',
180
+ inputSchema: {
181
+ type: 'object' as const,
182
+ properties: {},
183
+ },
184
+ },
185
+ {
186
+ name: 'create_test_case',
187
+ description: 'Create a new test case in BugBear. Use this after generating test case suggestions.',
188
+ inputSchema: {
189
+ type: 'object' as const,
190
+ properties: {
191
+ test_key: {
192
+ type: 'string',
193
+ description: 'Unique test case identifier (e.g., TC-001)',
194
+ },
195
+ title: {
196
+ type: 'string',
197
+ description: 'Brief title describing what is being tested',
198
+ },
199
+ description: {
200
+ type: 'string',
201
+ description: 'Detailed description of the test case',
202
+ },
203
+ track: {
204
+ type: 'string',
205
+ enum: ['functional', 'design', 'accessibility', 'performance', 'content', 'ux'],
206
+ description: 'QA track this test belongs to',
207
+ },
208
+ priority: {
209
+ type: 'string',
210
+ enum: ['P0', 'P1', 'P2', 'P3'],
211
+ description: 'Priority level (P0=critical, P3=low)',
212
+ },
213
+ steps: {
214
+ type: 'array',
215
+ description: 'Array of test steps',
216
+ items: {
217
+ type: 'object',
218
+ properties: {
219
+ stepNumber: { type: 'number' },
220
+ action: { type: 'string' },
221
+ expectedResult: { type: 'string' },
222
+ },
223
+ },
224
+ },
225
+ expected_result: {
226
+ type: 'string',
227
+ description: 'Overall expected outcome of the test',
228
+ },
229
+ preconditions: {
230
+ type: 'string',
231
+ description: 'Any setup required before running the test',
232
+ },
233
+ target_route: {
234
+ type: 'string',
235
+ description: 'Route/path to navigate to when starting this test (e.g., /settings/profile). This enables deep linking so testers can jump directly to the screen being tested.',
236
+ },
237
+ },
238
+ required: ['test_key', 'title', 'steps', 'expected_result'],
239
+ },
240
+ },
241
+ {
242
+ name: 'update_test_case',
243
+ description: 'Update an existing test case in BugBear. Use this to add target_route for deep linking or modify other fields.',
244
+ inputSchema: {
245
+ type: 'object' as const,
246
+ properties: {
247
+ test_case_id: {
248
+ type: 'string',
249
+ description: 'The UUID of the test case to update',
250
+ },
251
+ test_key: {
252
+ type: 'string',
253
+ description: 'Look up test case by test_key instead of ID (e.g., TC-001)',
254
+ },
255
+ title: {
256
+ type: 'string',
257
+ description: 'New title for the test case',
258
+ },
259
+ description: {
260
+ type: 'string',
261
+ description: 'New description for the test case',
262
+ },
263
+ priority: {
264
+ type: 'string',
265
+ enum: ['P0', 'P1', 'P2', 'P3'],
266
+ description: 'New priority level',
267
+ },
268
+ steps: {
269
+ type: 'array',
270
+ description: 'New array of test steps (replaces existing steps)',
271
+ items: {
272
+ type: 'object',
273
+ properties: {
274
+ stepNumber: { type: 'number' },
275
+ action: { type: 'string' },
276
+ expectedResult: { type: 'string' },
277
+ },
278
+ },
279
+ },
280
+ expected_result: {
281
+ type: 'string',
282
+ description: 'New expected outcome',
283
+ },
284
+ preconditions: {
285
+ type: 'string',
286
+ description: 'New preconditions',
287
+ },
288
+ target_route: {
289
+ type: 'string',
290
+ description: 'Route/path for deep linking (e.g., /settings/profile)',
291
+ },
292
+ },
293
+ },
294
+ },
295
+ {
296
+ name: 'list_test_cases',
297
+ description: 'List all test cases in the project. Returns test_key, title, target_route, and other metadata. Use this to see existing tests before updating them.',
298
+ inputSchema: {
299
+ type: 'object' as const,
300
+ properties: {
301
+ track: {
302
+ type: 'string',
303
+ description: 'Optional: filter by QA track name',
304
+ },
305
+ priority: {
306
+ type: 'string',
307
+ enum: ['P0', 'P1', 'P2', 'P3'],
308
+ description: 'Optional: filter by priority',
309
+ },
310
+ missing_target_route: {
311
+ type: 'boolean',
312
+ description: 'If true, only return test cases that do NOT have a target_route set (useful for backfilling)',
313
+ },
314
+ limit: {
315
+ type: 'number',
316
+ description: 'Max number of test cases to return (default 100)',
317
+ },
318
+ offset: {
319
+ type: 'number',
320
+ description: 'Offset for pagination (default 0)',
321
+ },
322
+ },
323
+ },
324
+ },
325
+ {
326
+ name: 'get_bug_patterns',
327
+ description: 'Get common bug patterns and hotspots for the project to help prioritize testing',
328
+ inputSchema: {
329
+ type: 'object' as const,
330
+ properties: {
331
+ route: {
332
+ type: 'string',
333
+ description: 'Optional: filter by specific route',
334
+ },
335
+ },
336
+ },
337
+ },
338
+ // === WRITE-BACK TOOLS FOR VIBE CODERS ===
339
+ {
340
+ name: 'create_bug_report',
341
+ description: 'Create a bug report directly from Claude Code. Use this when you notice issues while coding - broken functionality, potential bugs, or problems you discover during development.',
342
+ inputSchema: {
343
+ type: 'object' as const,
344
+ properties: {
345
+ title: {
346
+ type: 'string',
347
+ description: 'Brief title describing the bug',
348
+ },
349
+ description: {
350
+ type: 'string',
351
+ description: 'Detailed description of the bug, including what you observed',
352
+ },
353
+ severity: {
354
+ type: 'string',
355
+ enum: ['critical', 'high', 'medium', 'low'],
356
+ description: 'Bug severity level',
357
+ },
358
+ file_path: {
359
+ type: 'string',
360
+ description: 'File path where the bug was found (e.g., src/components/Auth.tsx)',
361
+ },
362
+ line_number: {
363
+ type: 'number',
364
+ description: 'Line number in the file where the issue occurs',
365
+ },
366
+ code_snippet: {
367
+ type: 'string',
368
+ description: 'Relevant code snippet showing the problematic code',
369
+ },
370
+ suggested_fix: {
371
+ type: 'string',
372
+ description: 'Your suggested fix or approach to resolve this bug',
373
+ },
374
+ related_files: {
375
+ type: 'array',
376
+ items: { type: 'string' },
377
+ description: 'Other files that may be affected or related to this bug',
378
+ },
379
+ },
380
+ required: ['title', 'description', 'severity'],
381
+ },
382
+ },
383
+ {
384
+ name: 'get_bugs_for_file',
385
+ description: 'Get all known bugs related to a specific file. Use this BEFORE editing a file to check for existing issues you should be aware of or might want to fix.',
386
+ inputSchema: {
387
+ type: 'object' as const,
388
+ properties: {
389
+ file_path: {
390
+ type: 'string',
391
+ description: 'File path to check for bugs (e.g., src/components/Auth.tsx)',
392
+ },
393
+ include_resolved: {
394
+ type: 'boolean',
395
+ description: 'Include resolved bugs for context (default: false)',
396
+ },
397
+ },
398
+ required: ['file_path'],
399
+ },
400
+ },
401
+ {
402
+ name: 'mark_fixed_with_commit',
403
+ description: 'Mark a bug as fixed and link it to the commit that resolved it. Use this after you fix a bug to close the loop.',
404
+ inputSchema: {
405
+ type: 'object' as const,
406
+ properties: {
407
+ report_id: {
408
+ type: 'string',
409
+ description: 'The UUID of the bug report to mark as fixed',
410
+ },
411
+ commit_sha: {
412
+ type: 'string',
413
+ description: 'Git commit SHA that contains the fix',
414
+ },
415
+ commit_message: {
416
+ type: 'string',
417
+ description: 'The commit message',
418
+ },
419
+ resolution_notes: {
420
+ type: 'string',
421
+ description: 'Notes explaining how the bug was fixed',
422
+ },
423
+ files_changed: {
424
+ type: 'array',
425
+ items: { type: 'string' },
426
+ description: 'List of files that were modified to fix this bug',
427
+ },
428
+ },
429
+ required: ['report_id', 'commit_sha'],
430
+ },
431
+ },
432
+ {
433
+ name: 'get_bugs_affecting_code',
434
+ description: 'Get bugs that might be affected by changes to specific files. Use this before pushing to understand the impact of your changes.',
435
+ inputSchema: {
436
+ type: 'object' as const,
437
+ properties: {
438
+ file_paths: {
439
+ type: 'array',
440
+ items: { type: 'string' },
441
+ description: 'List of file paths that were changed',
442
+ },
443
+ include_related: {
444
+ type: 'boolean',
445
+ description: 'Include bugs in related/imported files (default: true)',
446
+ },
447
+ },
448
+ required: ['file_paths'],
449
+ },
450
+ },
451
+ {
452
+ name: 'link_bug_to_code',
453
+ description: 'Add code location information to an existing bug report. Use this to help track exactly where bugs occur.',
454
+ inputSchema: {
455
+ type: 'object' as const,
456
+ properties: {
457
+ report_id: {
458
+ type: 'string',
459
+ description: 'The UUID of the bug report',
460
+ },
461
+ file_path: {
462
+ type: 'string',
463
+ description: 'File path where the bug occurs',
464
+ },
465
+ line_number: {
466
+ type: 'number',
467
+ description: 'Line number in the file',
468
+ },
469
+ code_snippet: {
470
+ type: 'string',
471
+ description: 'Relevant code snippet',
472
+ },
473
+ function_name: {
474
+ type: 'string',
475
+ description: 'Function or component name where the bug occurs',
476
+ },
477
+ },
478
+ required: ['report_id', 'file_path'],
479
+ },
480
+ },
481
+ {
482
+ name: 'create_regression_test',
483
+ description: 'Generate a regression test case from a fixed bug to prevent it from recurring. Use this after fixing a bug.',
484
+ inputSchema: {
485
+ type: 'object' as const,
486
+ properties: {
487
+ report_id: {
488
+ type: 'string',
489
+ description: 'The UUID of the fixed bug report to create a test from',
490
+ },
491
+ test_type: {
492
+ type: 'string',
493
+ enum: ['unit', 'integration', 'e2e'],
494
+ description: 'Type of test to generate',
495
+ },
496
+ },
497
+ required: ['report_id'],
498
+ },
499
+ },
500
+ // === FIX QUEUE TOOLS ===
501
+ {
502
+ name: 'get_pending_fixes',
503
+ description: 'Get fix requests queued from the BugBear dashboard. These are bugs that users have requested to be fixed. Check this periodically to see if there are fixes waiting for you.',
504
+ inputSchema: {
505
+ type: 'object' as const,
506
+ properties: {
507
+ limit: {
508
+ type: 'number',
509
+ description: 'Maximum number of fix requests to return (default: 10)',
510
+ },
511
+ include_claimed: {
512
+ type: 'boolean',
513
+ description: 'Include already-claimed fix requests (default: false)',
514
+ },
515
+ },
516
+ },
517
+ },
518
+ {
519
+ name: 'claim_fix_request',
520
+ description: 'Claim a fix request from the queue. This marks it as being worked on so other Claude Code instances know not to pick it up.',
521
+ inputSchema: {
522
+ type: 'object' as const,
523
+ properties: {
524
+ fix_request_id: {
525
+ type: 'string',
526
+ description: 'The UUID of the fix request to claim',
527
+ },
528
+ claimed_by: {
529
+ type: 'string',
530
+ description: 'Identifier for this Claude Code instance (e.g., hostname or session ID)',
531
+ },
532
+ },
533
+ required: ['fix_request_id'],
534
+ },
535
+ },
536
+ {
537
+ name: 'complete_fix_request',
538
+ description: 'Mark a fix request as completed after you have fixed the bug.',
539
+ inputSchema: {
540
+ type: 'object' as const,
541
+ properties: {
542
+ fix_request_id: {
543
+ type: 'string',
544
+ description: 'The UUID of the fix request to complete',
545
+ },
546
+ completion_notes: {
547
+ type: 'string',
548
+ description: 'Notes about how the fix was implemented (e.g., commit SHA, files changed)',
549
+ },
550
+ success: {
551
+ type: 'boolean',
552
+ description: 'Whether the fix was successful (default: true). Set to false if you could not fix the issue.',
553
+ },
554
+ },
555
+ required: ['fix_request_id'],
556
+ },
557
+ },
558
+ {
559
+ name: 'analyze_changes_for_tests',
560
+ description: 'Analyze code changes and intelligently suggest QA tests that should be created. Call this after implementing features, fixing bugs, or making significant changes. Returns prioritized test suggestions with rationale.',
561
+ inputSchema: {
562
+ type: 'object' as const,
563
+ properties: {
564
+ changed_files: {
565
+ type: 'array',
566
+ items: { type: 'string' },
567
+ description: 'List of files that were changed (paths)',
568
+ },
569
+ change_type: {
570
+ type: 'string',
571
+ enum: ['feature', 'bugfix', 'refactor', 'ui_change', 'api_change', 'config'],
572
+ description: 'Type of change made',
573
+ },
574
+ change_summary: {
575
+ type: 'string',
576
+ description: 'Brief description of what was changed (1-2 sentences)',
577
+ },
578
+ affected_routes: {
579
+ type: 'array',
580
+ items: { type: 'string' },
581
+ description: 'Routes/screens affected by this change (e.g., /settings, /profile)',
582
+ },
583
+ },
584
+ required: ['changed_files', 'change_type', 'change_summary'],
585
+ },
586
+ },
587
+ {
588
+ name: 'suggest_test_cases',
589
+ description: 'Get AI-generated test case suggestions based on bug history and routes. Returns formatted suggestions that can be reviewed and created.',
590
+ inputSchema: {
591
+ type: 'object' as const,
592
+ properties: {
593
+ route: {
594
+ type: 'string',
595
+ description: 'Route/feature to generate tests for',
596
+ },
597
+ track: {
598
+ type: 'string',
599
+ enum: ['functional', 'design', 'accessibility', 'performance'],
600
+ description: 'QA track to focus on',
601
+ },
602
+ count: {
603
+ type: 'number',
604
+ description: 'Number of test cases to suggest (default: 5)',
605
+ },
606
+ },
607
+ },
608
+ },
609
+ // === QA INTELLIGENCE TOOLS ===
610
+ {
611
+ name: 'get_test_priorities',
612
+ description: 'Get routes ranked by test priority score. Returns the most urgent routes that need testing based on bug frequency, critical issues, staleness, coverage gaps, and regression risk. Use this to know WHERE to focus testing efforts.',
613
+ inputSchema: {
614
+ type: 'object' as const,
615
+ properties: {
616
+ limit: {
617
+ type: 'number',
618
+ description: 'Maximum number of routes to return (default: 10)',
619
+ },
620
+ min_score: {
621
+ type: 'number',
622
+ description: 'Minimum priority score threshold (0-100, default: 0)',
623
+ },
624
+ include_factors: {
625
+ type: 'boolean',
626
+ description: 'Include detailed breakdown of priority factors (default: true)',
627
+ },
628
+ },
629
+ },
630
+ },
631
+ {
632
+ name: 'get_coverage_gaps',
633
+ description: 'Identify coverage gaps in QA testing. Finds untested routes, routes missing track coverage, and stale test coverage. Use this to understand what areas lack adequate testing.',
634
+ inputSchema: {
635
+ type: 'object' as const,
636
+ properties: {
637
+ gap_type: {
638
+ type: 'string',
639
+ enum: ['untested_routes', 'missing_tracks', 'stale_coverage', 'all'],
640
+ description: 'Type of gap to look for (default: all)',
641
+ },
642
+ stale_days: {
643
+ type: 'number',
644
+ description: 'Days threshold for stale coverage (default: 14)',
645
+ },
646
+ },
647
+ },
648
+ },
649
+ {
650
+ name: 'get_regressions',
651
+ description: 'Detect potential regressions - bugs that reappear after being resolved. Identifies routes with recurring issues and patterns. Use this to find areas prone to regression.',
652
+ inputSchema: {
653
+ type: 'object' as const,
654
+ properties: {
655
+ days: {
656
+ type: 'number',
657
+ description: 'Look back period in days (default: 30)',
658
+ },
659
+ include_history: {
660
+ type: 'boolean',
661
+ description: 'Include full regression history (default: false)',
662
+ },
663
+ },
664
+ },
665
+ },
666
+ {
667
+ name: 'get_coverage_matrix',
668
+ description: 'Get a comprehensive Route × Track coverage matrix showing test counts, pass rates, and execution data. Use this for a complete view of test coverage.',
669
+ inputSchema: {
670
+ type: 'object' as const,
671
+ properties: {
672
+ include_execution_data: {
673
+ type: 'boolean',
674
+ description: 'Include pass/fail rates and last execution times (default: true)',
675
+ },
676
+ include_bug_counts: {
677
+ type: 'boolean',
678
+ description: 'Include open/critical bug counts per route (default: true)',
679
+ },
680
+ },
681
+ },
682
+ },
683
+ {
684
+ name: 'get_stale_coverage',
685
+ description: 'Get routes that have not been tested within a threshold, ordered by risk (open bugs, critical bugs). Use this to find areas that need fresh testing.',
686
+ inputSchema: {
687
+ type: 'object' as const,
688
+ properties: {
689
+ days_threshold: {
690
+ type: 'number',
691
+ description: 'Days without testing to consider stale (default: 14)',
692
+ },
693
+ limit: {
694
+ type: 'number',
695
+ description: 'Maximum routes to return (default: 20)',
696
+ },
697
+ },
698
+ },
699
+ },
700
+ {
701
+ name: 'generate_deploy_checklist',
702
+ description: 'Generate a pre-deployment testing checklist based on changed routes. Returns prioritized tests to run before deploying, including critical tests, recommended tests, and coverage gaps.',
703
+ inputSchema: {
704
+ type: 'object' as const,
705
+ properties: {
706
+ routes: {
707
+ type: 'array',
708
+ items: { type: 'string' },
709
+ description: 'Routes that will be affected by the deployment',
710
+ },
711
+ changed_files: {
712
+ type: 'array',
713
+ items: { type: 'string' },
714
+ description: 'Optional: files changed to infer affected routes',
715
+ },
716
+ deployment_type: {
717
+ type: 'string',
718
+ enum: ['hotfix', 'feature', 'release'],
719
+ description: 'Type of deployment (affects checklist thoroughness, default: feature)',
720
+ },
721
+ },
722
+ required: ['routes'],
723
+ },
724
+ },
725
+ {
726
+ name: 'get_qa_health',
727
+ description: 'Get comprehensive QA health metrics including testing velocity, bug discovery rate, resolution time, coverage, and tester activity. Returns a health score (0-100) with grade.',
728
+ inputSchema: {
729
+ type: 'object' as const,
730
+ properties: {
731
+ period_days: {
732
+ type: 'number',
733
+ description: 'Analysis period in days (default: 30)',
734
+ },
735
+ compare_previous: {
736
+ type: 'boolean',
737
+ description: 'Include comparison with previous period (default: true)',
738
+ },
739
+ },
740
+ },
741
+ },
742
+ ];
743
+
744
+ // Tool handlers
745
+ async function listReports(args: {
746
+ limit?: number;
747
+ status?: string;
748
+ severity?: string;
749
+ type?: string;
750
+ }) {
751
+ let query = supabase
752
+ .from('reports')
753
+ .select('id, report_type, severity, status, description, app_context, created_at, tester:testers(name, email)')
754
+ .eq('project_id', PROJECT_ID)
755
+ .order('created_at', { ascending: false })
756
+ .limit(Math.min(args.limit || 10, 50));
757
+
758
+ if (args.status) query = query.eq('status', args.status);
759
+ if (args.severity) query = query.eq('severity', args.severity);
760
+ if (args.type) query = query.eq('report_type', args.type);
761
+
762
+ const { data, error } = await query;
763
+
764
+ if (error) {
765
+ return { error: error.message };
766
+ }
767
+
768
+ return {
769
+ reports: data?.map(r => ({
770
+ id: r.id,
771
+ type: r.report_type,
772
+ severity: r.severity,
773
+ status: r.status,
774
+ description: r.description,
775
+ route: (r.app_context as any)?.currentRoute,
776
+ reporter: (r.tester as any)?.name || 'Anonymous',
777
+ created_at: r.created_at,
778
+ })),
779
+ total: data?.length || 0,
780
+ };
781
+ }
782
+
783
+ async function getReport(args: { report_id: string }) {
784
+ // Validate UUID format to prevent injection
785
+ if (!args.report_id || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(args.report_id)) {
786
+ return { error: 'Invalid report_id format' };
787
+ }
788
+
789
+ const { data, error } = await supabase
790
+ .from('reports')
791
+ .select('*, tester:testers(*), track:qa_tracks(*)')
792
+ .eq('id', args.report_id)
793
+ .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
794
+ .single();
795
+
796
+ if (error) {
797
+ return { error: error.message };
798
+ }
799
+
800
+ return {
801
+ report: {
802
+ id: data.id,
803
+ type: data.report_type,
804
+ severity: data.severity,
805
+ status: data.status,
806
+ description: data.description,
807
+ app_context: data.app_context,
808
+ device_info: data.device_info,
809
+ navigation_history: data.navigation_history,
810
+ screenshots: data.screenshots,
811
+ created_at: data.created_at,
812
+ reporter: data.tester ? {
813
+ name: data.tester.name,
814
+ email: data.tester.email,
815
+ } : null,
816
+ track: data.track ? {
817
+ name: data.track.name,
818
+ icon: data.track.icon,
819
+ } : null,
820
+ },
821
+ };
822
+ }
823
+
824
+ async function searchReports(args: { query?: string; route?: string }) {
825
+ const sanitizedQuery = sanitizeSearchQuery(args.query);
826
+ const sanitizedRoute = sanitizeSearchQuery(args.route);
827
+
828
+ let query = supabase
829
+ .from('reports')
830
+ .select('id, report_type, severity, status, description, app_context, created_at')
831
+ .eq('project_id', PROJECT_ID)
832
+ .order('created_at', { ascending: false })
833
+ .limit(20);
834
+
835
+ if (sanitizedQuery) {
836
+ query = query.ilike('description', `%${sanitizedQuery}%`);
837
+ }
838
+
839
+ const { data, error } = await query;
840
+
841
+ if (error) {
842
+ return { error: error.message };
843
+ }
844
+
845
+ // Filter by route if provided
846
+ let results = data || [];
847
+ if (sanitizedRoute) {
848
+ results = results.filter(r => {
849
+ const route = (r.app_context as any)?.currentRoute;
850
+ return route && route.includes(sanitizedRoute);
851
+ });
852
+ }
853
+
854
+ return {
855
+ reports: results.map(r => ({
856
+ id: r.id,
857
+ type: r.report_type,
858
+ severity: r.severity,
859
+ status: r.status,
860
+ description: r.description,
861
+ route: (r.app_context as any)?.currentRoute,
862
+ created_at: r.created_at,
863
+ })),
864
+ total: results.length,
865
+ };
866
+ }
867
+
868
+ async function updateReportStatus(args: {
869
+ report_id: string;
870
+ status: string;
871
+ resolution?: string;
872
+ }) {
873
+ if (!isValidUUID(args.report_id)) {
874
+ return { error: 'Invalid report_id format' };
875
+ }
876
+
877
+ const updates: Record<string, unknown> = { status: args.status };
878
+ if (args.resolution) {
879
+ updates.resolution = args.resolution;
880
+ }
881
+
882
+ const { error } = await supabase
883
+ .from('reports')
884
+ .update(updates)
885
+ .eq('id', args.report_id)
886
+ .eq('project_id', PROJECT_ID); // Security: ensure report belongs to this project
887
+
888
+ if (error) {
889
+ return { error: error.message };
890
+ }
891
+
892
+ return { success: true, message: `Report status updated to ${args.status}` };
893
+ }
894
+
895
+ async function getReportContext(args: { report_id: string }) {
896
+ if (!isValidUUID(args.report_id)) {
897
+ return { error: 'Invalid report_id format' };
898
+ }
899
+
900
+ const { data, error } = await supabase
901
+ .from('reports')
902
+ .select('app_context, device_info, navigation_history, enhanced_context')
903
+ .eq('id', args.report_id)
904
+ .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
905
+ .single();
906
+
907
+ if (error) {
908
+ return { error: error.message };
909
+ }
910
+
911
+ return {
912
+ context: {
913
+ app_context: data.app_context,
914
+ device_info: data.device_info,
915
+ navigation_history: data.navigation_history,
916
+ enhanced_context: data.enhanced_context || {},
917
+ },
918
+ };
919
+ }
920
+
921
+ async function getProjectInfo() {
922
+ // Get project details
923
+ const { data: project, error: projectError } = await supabase
924
+ .from('projects')
925
+ .select('id, name, slug, is_qa_enabled')
926
+ .eq('id', PROJECT_ID)
927
+ .single();
928
+
929
+ if (projectError) {
930
+ return { error: projectError.message };
931
+ }
932
+
933
+ // Get track counts
934
+ const { data: tracks } = await supabase
935
+ .from('qa_tracks')
936
+ .select('id, name, icon, test_template')
937
+ .eq('project_id', PROJECT_ID);
938
+
939
+ // Get test case count
940
+ const { count: testCaseCount } = await supabase
941
+ .from('test_cases')
942
+ .select('id', { count: 'exact', head: true })
943
+ .eq('project_id', PROJECT_ID);
944
+
945
+ // Get open bug count
946
+ const { count: openBugCount } = await supabase
947
+ .from('reports')
948
+ .select('id', { count: 'exact', head: true })
949
+ .eq('project_id', PROJECT_ID)
950
+ .eq('report_type', 'bug')
951
+ .in('status', ['new', 'confirmed', 'in_progress']);
952
+
953
+ return {
954
+ project: {
955
+ id: project.id,
956
+ name: project.name,
957
+ slug: project.slug,
958
+ qaEnabled: project.is_qa_enabled,
959
+ },
960
+ stats: {
961
+ tracks: tracks?.length || 0,
962
+ testCases: testCaseCount || 0,
963
+ openBugs: openBugCount || 0,
964
+ },
965
+ tracks: tracks?.map(t => ({
966
+ id: t.id,
967
+ name: t.name,
968
+ icon: t.icon,
969
+ template: t.test_template,
970
+ })) || [],
971
+ };
972
+ }
973
+
974
+ async function getQaTracks() {
975
+ const { data, error } = await supabase
976
+ .from('qa_tracks')
977
+ .select('*')
978
+ .eq('project_id', PROJECT_ID)
979
+ .order('sort_order');
980
+
981
+ if (error) {
982
+ return { error: error.message };
983
+ }
984
+
985
+ return {
986
+ tracks: data?.map(t => ({
987
+ id: t.id,
988
+ name: t.name,
989
+ slug: t.slug,
990
+ icon: t.icon,
991
+ color: t.color,
992
+ testTemplate: t.test_template,
993
+ description: t.description,
994
+ requiresCertification: t.requires_certification,
995
+ evaluationCriteria: t.evaluation_criteria,
996
+ })) || [],
997
+ };
998
+ }
999
+
1000
+ async function createTestCase(args: {
1001
+ test_key: string;
1002
+ title: string;
1003
+ description?: string;
1004
+ track?: string;
1005
+ priority?: string;
1006
+ steps: Array<{ stepNumber: number; action: string; expectedResult?: string }>;
1007
+ expected_result: string;
1008
+ preconditions?: string;
1009
+ target_route?: string;
1010
+ }) {
1011
+ // Find track ID if track name provided
1012
+ let trackId: string | null = null;
1013
+ if (args.track) {
1014
+ const { data: trackData } = await supabase
1015
+ .from('qa_tracks')
1016
+ .select('id')
1017
+ .eq('project_id', PROJECT_ID)
1018
+ .ilike('name', `%${args.track}%`)
1019
+ .single();
1020
+ trackId = trackData?.id || null;
1021
+ }
1022
+
1023
+ const testCase = {
1024
+ project_id: PROJECT_ID,
1025
+ test_key: args.test_key,
1026
+ title: args.title,
1027
+ description: args.description || '',
1028
+ track_id: trackId,
1029
+ priority: args.priority || 'P2',
1030
+ steps: args.steps,
1031
+ expected_result: args.expected_result,
1032
+ preconditions: args.preconditions || '',
1033
+ target_route: args.target_route || null,
1034
+ };
1035
+
1036
+ const { data, error } = await supabase
1037
+ .from('test_cases')
1038
+ .insert(testCase)
1039
+ .select('id, test_key, title')
1040
+ .single();
1041
+
1042
+ if (error) {
1043
+ return { error: error.message };
1044
+ }
1045
+
1046
+ return {
1047
+ success: true,
1048
+ testCase: {
1049
+ id: data.id,
1050
+ testKey: data.test_key,
1051
+ title: data.title,
1052
+ },
1053
+ message: `Test case ${data.test_key} created successfully`,
1054
+ };
1055
+ }
1056
+
1057
+ async function updateTestCase(args: {
1058
+ test_case_id?: string;
1059
+ test_key?: string;
1060
+ title?: string;
1061
+ description?: string;
1062
+ priority?: string;
1063
+ steps?: Array<{ stepNumber: number; action: string; expectedResult?: string }>;
1064
+ expected_result?: string;
1065
+ preconditions?: string;
1066
+ target_route?: string;
1067
+ }) {
1068
+ // Need either test_case_id or test_key
1069
+ if (!args.test_case_id && !args.test_key) {
1070
+ return { error: 'Must provide either test_case_id or test_key to identify the test case' };
1071
+ }
1072
+
1073
+ // Find the test case
1074
+ let testCaseId = args.test_case_id;
1075
+ if (!testCaseId && args.test_key) {
1076
+ const { data: existing } = await supabase
1077
+ .from('test_cases')
1078
+ .select('id')
1079
+ .eq('project_id', PROJECT_ID)
1080
+ .eq('test_key', args.test_key)
1081
+ .single();
1082
+
1083
+ if (!existing) {
1084
+ return { error: `Test case with key ${args.test_key} not found` };
1085
+ }
1086
+ testCaseId = existing.id;
1087
+ }
1088
+
1089
+ // Build update object with only provided fields
1090
+ const updates: Record<string, unknown> = {};
1091
+ if (args.title !== undefined) updates.title = args.title;
1092
+ if (args.description !== undefined) updates.description = args.description;
1093
+ if (args.priority !== undefined) updates.priority = args.priority;
1094
+ if (args.steps !== undefined) updates.steps = args.steps;
1095
+ if (args.expected_result !== undefined) updates.expected_result = args.expected_result;
1096
+ if (args.preconditions !== undefined) updates.preconditions = args.preconditions;
1097
+ if (args.target_route !== undefined) updates.target_route = args.target_route;
1098
+
1099
+ if (Object.keys(updates).length === 0) {
1100
+ return { error: 'No fields to update' };
1101
+ }
1102
+
1103
+ const { data, error } = await supabase
1104
+ .from('test_cases')
1105
+ .update(updates)
1106
+ .eq('id', testCaseId)
1107
+ .eq('project_id', PROJECT_ID)
1108
+ .select('id, test_key, title, target_route')
1109
+ .single();
1110
+
1111
+ if (error) {
1112
+ return { error: error.message };
1113
+ }
1114
+
1115
+ return {
1116
+ success: true,
1117
+ testCase: {
1118
+ id: data.id,
1119
+ testKey: data.test_key,
1120
+ title: data.title,
1121
+ targetRoute: data.target_route,
1122
+ },
1123
+ message: `Test case ${data.test_key} updated successfully`,
1124
+ updatedFields: Object.keys(updates),
1125
+ };
1126
+ }
1127
+
1128
+ async function listTestCases(args: {
1129
+ track?: string;
1130
+ priority?: string;
1131
+ missing_target_route?: boolean;
1132
+ limit?: number;
1133
+ offset?: number;
1134
+ }) {
1135
+ let query = supabase
1136
+ .from('test_cases')
1137
+ .select(`
1138
+ id,
1139
+ test_key,
1140
+ title,
1141
+ description,
1142
+ priority,
1143
+ target_route,
1144
+ preconditions,
1145
+ expected_result,
1146
+ steps,
1147
+ track:qa_tracks(id, name, icon, color)
1148
+ `)
1149
+ .eq('project_id', PROJECT_ID)
1150
+ .order('test_key', { ascending: true });
1151
+
1152
+ // Apply filters
1153
+ if (args.priority) {
1154
+ query = query.eq('priority', args.priority);
1155
+ }
1156
+
1157
+ if (args.missing_target_route) {
1158
+ query = query.is('target_route', null);
1159
+ }
1160
+
1161
+ // Pagination
1162
+ const limit = args.limit || 100;
1163
+ const offset = args.offset || 0;
1164
+ query = query.range(offset, offset + limit - 1);
1165
+
1166
+ const { data, error } = await query;
1167
+
1168
+ if (error) {
1169
+ return { error: error.message };
1170
+ }
1171
+
1172
+ // Filter by track name if provided (post-query since it's a joined field)
1173
+ let testCases = data || [];
1174
+ if (args.track) {
1175
+ testCases = testCases.filter((tc: any) =>
1176
+ tc.track?.name?.toLowerCase().includes(args.track!.toLowerCase())
1177
+ );
1178
+ }
1179
+
1180
+ return {
1181
+ count: testCases.length,
1182
+ testCases: testCases.map((tc: any) => ({
1183
+ id: tc.id,
1184
+ testKey: tc.test_key,
1185
+ title: tc.title,
1186
+ description: tc.description,
1187
+ priority: tc.priority,
1188
+ targetRoute: tc.target_route,
1189
+ hasTargetRoute: !!tc.target_route,
1190
+ track: tc.track?.name || null,
1191
+ stepsCount: tc.steps?.length || 0,
1192
+ })),
1193
+ pagination: {
1194
+ limit,
1195
+ offset,
1196
+ hasMore: testCases.length === limit,
1197
+ },
1198
+ };
1199
+ }
1200
+
1201
+ async function getBugPatterns(args: { route?: string }) {
1202
+ // Get bugs grouped by route
1203
+ let query = supabase
1204
+ .from('reports')
1205
+ .select('app_context, severity, status, created_at')
1206
+ .eq('project_id', PROJECT_ID)
1207
+ .eq('report_type', 'bug')
1208
+ .order('created_at', { ascending: false })
1209
+ .limit(100);
1210
+
1211
+ const { data: bugs, error } = await query;
1212
+
1213
+ if (error) {
1214
+ return { error: error.message };
1215
+ }
1216
+
1217
+ // Analyze patterns
1218
+ const routePatterns: Record<string, {
1219
+ total: number;
1220
+ critical: number;
1221
+ open: number;
1222
+ resolved: number;
1223
+ }> = {};
1224
+
1225
+ for (const bug of bugs || []) {
1226
+ const route = (bug.app_context as any)?.currentRoute || 'unknown';
1227
+
1228
+ // Filter by route if specified
1229
+ if (args.route && !route.includes(args.route)) continue;
1230
+
1231
+ if (!routePatterns[route]) {
1232
+ routePatterns[route] = { total: 0, critical: 0, open: 0, resolved: 0 };
1233
+ }
1234
+
1235
+ routePatterns[route].total++;
1236
+ if (bug.severity === 'critical' || bug.severity === 'high') {
1237
+ routePatterns[route].critical++;
1238
+ }
1239
+ if (['new', 'confirmed', 'in_progress'].includes(bug.status)) {
1240
+ routePatterns[route].open++;
1241
+ } else {
1242
+ routePatterns[route].resolved++;
1243
+ }
1244
+ }
1245
+
1246
+ // Sort by total bugs descending
1247
+ const hotspots = Object.entries(routePatterns)
1248
+ .map(([route, stats]) => ({ route, ...stats }))
1249
+ .sort((a, b) => b.total - a.total)
1250
+ .slice(0, 10);
1251
+
1252
+ return {
1253
+ hotspots,
1254
+ summary: {
1255
+ totalRoutes: Object.keys(routePatterns).length,
1256
+ totalBugs: bugs?.length || 0,
1257
+ criticalRoutes: hotspots.filter(h => h.critical > 0).length,
1258
+ },
1259
+ recommendations: hotspots
1260
+ .filter(h => h.open > 0)
1261
+ .map(h => `Route "${h.route}" has ${h.open} open bugs - prioritize testing here`),
1262
+ };
1263
+ }
1264
+
1265
+ async function suggestTestCases(args: {
1266
+ route?: string;
1267
+ track?: string;
1268
+ count?: number;
1269
+ }) {
1270
+ const count = args.count || 5;
1271
+
1272
+ // Get existing test cases to avoid duplicates
1273
+ const { data: existingTests } = await supabase
1274
+ .from('test_cases')
1275
+ .select('test_key, title')
1276
+ .eq('project_id', PROJECT_ID)
1277
+ .order('test_key', { ascending: false })
1278
+ .limit(1);
1279
+
1280
+ // Calculate next test key number
1281
+ const lastKey = existingTests?.[0]?.test_key || 'TC-000';
1282
+ const lastNum = parseInt(lastKey.replace('TC-', '')) || 0;
1283
+
1284
+ // Get bug patterns for context
1285
+ const patterns = await getBugPatterns({ route: args.route });
1286
+
1287
+ // Generate suggestions based on track
1288
+ const suggestions: Array<{
1289
+ test_key: string;
1290
+ title: string;
1291
+ description: string;
1292
+ track: string;
1293
+ priority: string;
1294
+ steps: Array<{ stepNumber: number; action: string; expectedResult: string }>;
1295
+ expected_result: string;
1296
+ }> = [];
1297
+
1298
+ const track = args.track || 'functional';
1299
+ const route = args.route || '/';
1300
+
1301
+ // Template suggestions based on track type
1302
+ const templates = getTrackTemplates(track);
1303
+
1304
+ for (let i = 0; i < count; i++) {
1305
+ const keyNum = lastNum + i + 1;
1306
+ const template = templates[i % templates.length];
1307
+
1308
+ suggestions.push({
1309
+ test_key: `TC-${String(keyNum).padStart(3, '0')}`,
1310
+ title: template.title.replace('{route}', route),
1311
+ description: template.description.replace('{route}', route),
1312
+ track,
1313
+ priority: i === 0 ? 'P1' : 'P2',
1314
+ steps: template.steps.map((s, idx) => ({
1315
+ stepNumber: idx + 1,
1316
+ action: s.action.replace('{route}', route),
1317
+ expectedResult: s.expectedResult.replace('{route}', route),
1318
+ })),
1319
+ expected_result: template.expected_result.replace('{route}', route),
1320
+ });
1321
+ }
1322
+
1323
+ // Get related bugs for historical context
1324
+ const { data: relatedBugs } = await supabase
1325
+ .from('reports')
1326
+ .select('id, description, severity')
1327
+ .eq('project_id', PROJECT_ID)
1328
+ .eq('report_type', 'bug')
1329
+ .limit(10);
1330
+
1331
+ const routeBugs = (relatedBugs || []).filter(bug => {
1332
+ // In a real impl, would filter by route match
1333
+ return true;
1334
+ }).slice(0, 5);
1335
+
1336
+ return {
1337
+ suggestions,
1338
+ context: {
1339
+ route: args.route || 'all',
1340
+ track,
1341
+ bugHotspots: (patterns as any).hotspots?.slice(0, 3) || [],
1342
+ },
1343
+ historicalContext: {
1344
+ relatedBugs: routeBugs.map(b => ({
1345
+ id: b.id,
1346
+ description: b.description.slice(0, 100),
1347
+ severity: b.severity,
1348
+ })),
1349
+ recommendation: routeBugs.length > 0
1350
+ ? `These test suggestions are informed by ${routeBugs.length} historical bug(s) in this area.`
1351
+ : 'No historical bugs found for this route.',
1352
+ },
1353
+ instructions: `Review these suggestions and use create_test_case to add them to BugBear.
1354
+ You can modify the suggestions before creating them.`,
1355
+ };
1356
+ }
1357
+
1358
+ // === QA INTELLIGENCE TOOL HANDLERS ===
1359
+
1360
+ async function getTestPriorities(args: {
1361
+ limit?: number;
1362
+ min_score?: number;
1363
+ include_factors?: boolean;
1364
+ }) {
1365
+ const limit = args.limit || 10;
1366
+ const minScore = args.min_score || 0;
1367
+ const includeFactors = args.include_factors !== false;
1368
+
1369
+ // First, refresh the route stats
1370
+ await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
1371
+
1372
+ // Get prioritized routes
1373
+ const { data: routes, error } = await supabase
1374
+ .from('route_test_stats')
1375
+ .select('*')
1376
+ .eq('project_id', PROJECT_ID)
1377
+ .gte('priority_score', minScore)
1378
+ .order('priority_score', { ascending: false })
1379
+ .limit(limit);
1380
+
1381
+ if (error) {
1382
+ return { error: error.message };
1383
+ }
1384
+
1385
+ if (!routes || routes.length === 0) {
1386
+ return {
1387
+ priorities: [],
1388
+ summary: { totalRoutes: 0, criticalCount: 0, highCount: 0, mediumCount: 0 },
1389
+ guidance: 'No routes found with test data. Start by creating test cases and running tests.',
1390
+ };
1391
+ }
1392
+
1393
+ // Calculate urgency levels
1394
+ const getUrgency = (score: number): 'critical' | 'high' | 'medium' | 'low' => {
1395
+ if (score >= 70) return 'critical';
1396
+ if (score >= 50) return 'high';
1397
+ if (score >= 30) return 'medium';
1398
+ return 'low';
1399
+ };
1400
+
1401
+ const getRecommendation = (route: any): string => {
1402
+ if (route.critical_bugs > 0) {
1403
+ return `Critical bugs exist - prioritize immediate testing and bug fixes`;
1404
+ }
1405
+ if (route.open_bugs >= 3) {
1406
+ return `Multiple open bugs (${route.open_bugs}) - comprehensive testing needed`;
1407
+ }
1408
+ if (route.last_tested_at === null) {
1409
+ return `Never tested - establish baseline coverage`;
1410
+ }
1411
+ const daysSinceTest = route.last_tested_at
1412
+ ? Math.floor((Date.now() - new Date(route.last_tested_at).getTime()) / (1000 * 60 * 60 * 24))
1413
+ : null;
1414
+ if (daysSinceTest && daysSinceTest > 14) {
1415
+ return `Stale coverage (${daysSinceTest} days) - refresh testing`;
1416
+ }
1417
+ if (route.regression_count > 0) {
1418
+ return `Regression risk (${route.regression_count} past regressions) - add regression tests`;
1419
+ }
1420
+ if (route.test_case_count < 3) {
1421
+ return `Low test coverage (${route.test_case_count} tests) - add more test cases`;
1422
+ }
1423
+ return 'Maintain current testing cadence';
1424
+ };
1425
+
1426
+ const priorities = routes.map((route, idx) => {
1427
+ const daysSinceTest = route.last_tested_at
1428
+ ? Math.floor((Date.now() - new Date(route.last_tested_at).getTime()) / (1000 * 60 * 60 * 24))
1429
+ : null;
1430
+
1431
+ const priority: any = {
1432
+ rank: idx + 1,
1433
+ route: route.route,
1434
+ priorityScore: route.priority_score,
1435
+ urgency: getUrgency(route.priority_score),
1436
+ stats: {
1437
+ openBugs: route.open_bugs,
1438
+ criticalBugs: route.critical_bugs,
1439
+ highBugs: route.high_bugs,
1440
+ testCases: route.test_case_count,
1441
+ daysSinceTest,
1442
+ regressions: route.regression_count,
1443
+ recentBugs: route.bugs_last_7_days,
1444
+ },
1445
+ recommendation: getRecommendation(route),
1446
+ };
1447
+
1448
+ if (includeFactors) {
1449
+ // Calculate factor breakdown
1450
+ const bugFreqScore = Math.min(route.open_bugs * 5, 30);
1451
+ const criticalScore = Math.min(route.critical_bugs * 25 + route.high_bugs * 10, 25);
1452
+ const stalenessScore = daysSinceTest === null ? 20 : Math.min(daysSinceTest, 20);
1453
+ const coverageScore = Math.max(15 - route.test_case_count * 5, 0);
1454
+ const regressionScore = Math.min(route.regression_count * 10, 10);
1455
+
1456
+ priority.factors = {
1457
+ bugFrequency: { score: bugFreqScore, openBugs: route.open_bugs, bugs7d: route.bugs_last_7_days },
1458
+ criticalSeverity: { score: criticalScore, critical: route.critical_bugs, high: route.high_bugs },
1459
+ staleness: { score: stalenessScore, daysSinceTest },
1460
+ coverageGap: { score: coverageScore, testCount: route.test_case_count },
1461
+ regressionRisk: { score: regressionScore, regressionCount: route.regression_count },
1462
+ };
1463
+ }
1464
+
1465
+ return priority;
1466
+ });
1467
+
1468
+ const criticalCount = priorities.filter(p => p.urgency === 'critical').length;
1469
+ const highCount = priorities.filter(p => p.urgency === 'high').length;
1470
+ const mediumCount = priorities.filter(p => p.urgency === 'medium').length;
1471
+
1472
+ let guidance = '';
1473
+ if (criticalCount > 0) {
1474
+ guidance = `URGENT: ${criticalCount} route(s) need immediate attention. Focus on critical bugs and untested areas first.`;
1475
+ } else if (highCount > 0) {
1476
+ guidance = `${highCount} route(s) have high priority. Schedule testing sessions to address coverage gaps.`;
1477
+ } else {
1478
+ guidance = 'QA coverage is in good shape. Maintain regular testing cadence.';
1479
+ }
1480
+
1481
+ return {
1482
+ priorities,
1483
+ summary: {
1484
+ totalRoutes: routes.length,
1485
+ criticalCount,
1486
+ highCount,
1487
+ mediumCount,
1488
+ },
1489
+ guidance,
1490
+ };
1491
+ }
1492
+
1493
+ async function getCoverageGaps(args: {
1494
+ gap_type?: 'untested_routes' | 'missing_tracks' | 'stale_coverage' | 'all';
1495
+ stale_days?: number;
1496
+ }) {
1497
+ const gapType = args.gap_type || 'all';
1498
+ const staleDays = args.stale_days || 14;
1499
+
1500
+ const gaps: {
1501
+ untested: any[];
1502
+ missingTracks: any[];
1503
+ stale: any[];
1504
+ } = { untested: [], missingTracks: [], stale: [] };
1505
+
1506
+ // Get all routes from reports
1507
+ const { data: routesFromReports } = await supabase
1508
+ .from('reports')
1509
+ .select('app_context')
1510
+ .eq('project_id', PROJECT_ID)
1511
+ .not('app_context->currentRoute', 'is', null);
1512
+
1513
+ const allRoutes = new Set<string>();
1514
+ (routesFromReports || []).forEach(r => {
1515
+ const route = (r.app_context as any)?.currentRoute;
1516
+ if (route) allRoutes.add(route);
1517
+ });
1518
+
1519
+ // Get test coverage data
1520
+ const { data: testCases } = await supabase
1521
+ .from('test_cases')
1522
+ .select('target_route, category, track_id')
1523
+ .eq('project_id', PROJECT_ID);
1524
+
1525
+ const coveredRoutes = new Set<string>();
1526
+ const routeTrackCoverage: Record<string, Set<string>> = {};
1527
+
1528
+ (testCases || []).forEach(tc => {
1529
+ const route = tc.target_route || tc.category;
1530
+ if (route) {
1531
+ coveredRoutes.add(route);
1532
+ if (!routeTrackCoverage[route]) routeTrackCoverage[route] = new Set();
1533
+ if (tc.track_id) routeTrackCoverage[route].add(tc.track_id);
1534
+ }
1535
+ });
1536
+
1537
+ // Get all tracks
1538
+ const { data: tracks } = await supabase
1539
+ .from('qa_tracks')
1540
+ .select('id, name')
1541
+ .eq('project_id', PROJECT_ID);
1542
+
1543
+ const trackMap = new Map((tracks || []).map(t => [t.id, t.name]));
1544
+
1545
+ // Get route stats for staleness
1546
+ const { data: routeStats } = await supabase
1547
+ .from('route_test_stats')
1548
+ .select('route, last_tested_at, open_bugs, critical_bugs')
1549
+ .eq('project_id', PROJECT_ID);
1550
+
1551
+ const routeStatsMap = new Map((routeStats || []).map(r => [r.route, r]));
1552
+
1553
+ // Find untested routes
1554
+ if (gapType === 'all' || gapType === 'untested_routes') {
1555
+ allRoutes.forEach(route => {
1556
+ if (!coveredRoutes.has(route)) {
1557
+ const stats = routeStatsMap.get(route);
1558
+ const severity = (stats?.critical_bugs || 0) > 0 ? 'critical' :
1559
+ (stats?.open_bugs || 0) > 2 ? 'high' : 'medium';
1560
+
1561
+ gaps.untested.push({
1562
+ route,
1563
+ severity,
1564
+ type: 'untested',
1565
+ details: {
1566
+ openBugs: stats?.open_bugs || 0,
1567
+ criticalBugs: stats?.critical_bugs || 0,
1568
+ },
1569
+ recommendation: `Create test cases for ${route} - this route has bugs but no test coverage.`,
1570
+ });
1571
+ }
1572
+ });
1573
+ gaps.untested.sort((a, b) => {
1574
+ const severityOrder = { critical: 0, high: 1, medium: 2 };
1575
+ return severityOrder[a.severity as keyof typeof severityOrder] - severityOrder[b.severity as keyof typeof severityOrder];
1576
+ });
1577
+ }
1578
+
1579
+ // Find missing track coverage
1580
+ if (gapType === 'all' || gapType === 'missing_tracks') {
1581
+ coveredRoutes.forEach(route => {
1582
+ const coveredTracks = routeTrackCoverage[route] || new Set();
1583
+ const missingTracks: string[] = [];
1584
+
1585
+ trackMap.forEach((name, id) => {
1586
+ if (!coveredTracks.has(id)) {
1587
+ missingTracks.push(name);
1588
+ }
1589
+ });
1590
+
1591
+ if (missingTracks.length > 0 && missingTracks.length < trackMap.size) {
1592
+ const stats = routeStatsMap.get(route);
1593
+ gaps.missingTracks.push({
1594
+ route,
1595
+ severity: missingTracks.length > trackMap.size / 2 ? 'high' : 'medium',
1596
+ type: 'missing_tracks',
1597
+ details: { missingTracks },
1598
+ recommendation: `Add tests for tracks: ${missingTracks.join(', ')}`,
1599
+ });
1600
+ }
1601
+ });
1602
+ }
1603
+
1604
+ // Find stale coverage
1605
+ if (gapType === 'all' || gapType === 'stale_coverage') {
1606
+ (routeStats || []).forEach(stat => {
1607
+ if (stat.last_tested_at) {
1608
+ const daysSince = Math.floor((Date.now() - new Date(stat.last_tested_at).getTime()) / (1000 * 60 * 60 * 24));
1609
+ if (daysSince >= staleDays) {
1610
+ gaps.stale.push({
1611
+ route: stat.route,
1612
+ severity: daysSince >= staleDays * 2 ? 'high' : 'medium',
1613
+ type: 'stale',
1614
+ details: {
1615
+ daysSinceTest: daysSince,
1616
+ openBugs: stat.open_bugs,
1617
+ criticalBugs: stat.critical_bugs,
1618
+ },
1619
+ recommendation: `Re-run tests for ${stat.route} - last tested ${daysSince} days ago`,
1620
+ });
1621
+ }
1622
+ }
1623
+ });
1624
+ gaps.stale.sort((a, b) => (b.details.daysSinceTest || 0) - (a.details.daysSinceTest || 0));
1625
+ }
1626
+
1627
+ const recommendations: string[] = [];
1628
+ if (gaps.untested.length > 0) {
1629
+ recommendations.push(`${gaps.untested.length} route(s) have bugs but no test coverage - create tests immediately`);
1630
+ }
1631
+ if (gaps.missingTracks.length > 0) {
1632
+ recommendations.push(`${gaps.missingTracks.length} route(s) are missing track-specific tests`);
1633
+ }
1634
+ if (gaps.stale.length > 0) {
1635
+ recommendations.push(`${gaps.stale.length} route(s) have stale coverage (>${staleDays} days) - refresh testing`);
1636
+ }
1637
+
1638
+ return {
1639
+ gaps,
1640
+ summary: {
1641
+ untestedRoutes: gaps.untested.length,
1642
+ routesMissingTracks: gaps.missingTracks.length,
1643
+ staleRoutes: gaps.stale.length,
1644
+ },
1645
+ recommendations,
1646
+ };
1647
+ }
1648
+
1649
+ async function getRegressions(args: {
1650
+ days?: number;
1651
+ include_history?: boolean;
1652
+ }) {
1653
+ const days = args.days || 30;
1654
+ const includeHistory = args.include_history || false;
1655
+
1656
+ // Find potential regressions: routes with resolved bugs that have new bugs
1657
+ const { data: resolvedBugs } = await supabase
1658
+ .from('reports')
1659
+ .select('id, description, severity, app_context, resolved_at')
1660
+ .eq('project_id', PROJECT_ID)
1661
+ .eq('report_type', 'bug')
1662
+ .in('status', ['resolved', 'fixed', 'verified', 'closed'])
1663
+ .gte('resolved_at', new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString());
1664
+
1665
+ const { data: newBugs } = await supabase
1666
+ .from('reports')
1667
+ .select('id, description, severity, app_context, created_at')
1668
+ .eq('project_id', PROJECT_ID)
1669
+ .eq('report_type', 'bug')
1670
+ .in('status', ['new', 'triaging', 'confirmed', 'in_progress', 'reviewed'])
1671
+ .gte('created_at', new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString());
1672
+
1673
+ // Group by route
1674
+ const routeData: Record<string, {
1675
+ resolved: any[];
1676
+ new: any[];
1677
+ }> = {};
1678
+
1679
+ (resolvedBugs || []).forEach(bug => {
1680
+ const route = (bug.app_context as any)?.currentRoute;
1681
+ if (route) {
1682
+ if (!routeData[route]) routeData[route] = { resolved: [], new: [] };
1683
+ routeData[route].resolved.push(bug);
1684
+ }
1685
+ });
1686
+
1687
+ (newBugs || []).forEach(bug => {
1688
+ const route = (bug.app_context as any)?.currentRoute;
1689
+ if (route) {
1690
+ if (!routeData[route]) routeData[route] = { resolved: [], new: [] };
1691
+ routeData[route].new.push(bug);
1692
+ }
1693
+ });
1694
+
1695
+ // Find routes with both resolved and new bugs (potential regressions)
1696
+ const regressions: any[] = [];
1697
+ let criticalCount = 0;
1698
+ const recurringPatterns: string[] = [];
1699
+
1700
+ Object.entries(routeData).forEach(([route, data]) => {
1701
+ if (data.resolved.length > 0 && data.new.length > 0) {
1702
+ // This is a potential regression
1703
+ const latestResolved = data.resolved.sort((a, b) =>
1704
+ new Date(b.resolved_at).getTime() - new Date(a.resolved_at).getTime()
1705
+ )[0];
1706
+
1707
+ const daysSinceResolution = latestResolved.resolved_at
1708
+ ? Math.floor((Date.now() - new Date(latestResolved.resolved_at).getTime()) / (1000 * 60 * 60 * 24))
1709
+ : null;
1710
+
1711
+ const severity = data.new.some(b => b.severity === 'critical') ? 'critical' :
1712
+ data.new.some(b => b.severity === 'high') ? 'high' : 'medium';
1713
+
1714
+ if (severity === 'critical') criticalCount++;
1715
+
1716
+ if (data.resolved.length >= 2 && data.new.length >= 2) {
1717
+ recurringPatterns.push(route);
1718
+ }
1719
+
1720
+ const regression: any = {
1721
+ route,
1722
+ severity,
1723
+ originalBug: {
1724
+ id: latestResolved.id,
1725
+ description: latestResolved.description?.slice(0, 100) || '',
1726
+ resolvedAt: latestResolved.resolved_at,
1727
+ },
1728
+ newBugs: data.new.map(b => ({
1729
+ id: b.id,
1730
+ description: b.description?.slice(0, 100) || '',
1731
+ severity: b.severity,
1732
+ createdAt: b.created_at,
1733
+ })),
1734
+ daysSinceResolution,
1735
+ regressionCount: data.resolved.length,
1736
+ };
1737
+
1738
+ if (includeHistory) {
1739
+ regression.history = {
1740
+ totalResolved: data.resolved.length,
1741
+ totalNew: data.new.length,
1742
+ resolvedBugs: data.resolved.map(b => ({
1743
+ id: b.id,
1744
+ severity: b.severity,
1745
+ resolvedAt: b.resolved_at,
1746
+ })),
1747
+ };
1748
+ }
1749
+
1750
+ regressions.push(regression);
1751
+ }
1752
+ });
1753
+
1754
+ // Sort by severity and count
1755
+ regressions.sort((a, b) => {
1756
+ const severityOrder = { critical: 0, high: 1, medium: 2 };
1757
+ return (severityOrder[a.severity as keyof typeof severityOrder] - severityOrder[b.severity as keyof typeof severityOrder]) ||
1758
+ (b.newBugs.length - a.newBugs.length);
1759
+ });
1760
+
1761
+ const recommendations: string[] = [];
1762
+ if (criticalCount > 0) {
1763
+ recommendations.push(`URGENT: ${criticalCount} regression(s) involve critical bugs - investigate immediately`);
1764
+ }
1765
+ if (recurringPatterns.length > 0) {
1766
+ recommendations.push(`Recurring patterns in ${recurringPatterns.length} route(s): ${recurringPatterns.slice(0, 3).join(', ')} - consider architectural review`);
1767
+ }
1768
+ if (regressions.length > 0) {
1769
+ recommendations.push('Add regression tests to prevent recurrence - use create_regression_test');
1770
+ }
1771
+
1772
+ return {
1773
+ regressions,
1774
+ summary: {
1775
+ totalRegressions: regressions.length,
1776
+ criticalRegressions: criticalCount,
1777
+ recurringPatterns: recurringPatterns.length,
1778
+ },
1779
+ recommendations,
1780
+ };
1781
+ }
1782
+
1783
+ async function getCoverageMatrix(args: {
1784
+ include_execution_data?: boolean;
1785
+ include_bug_counts?: boolean;
1786
+ }) {
1787
+ const includeExecution = args.include_execution_data !== false;
1788
+ const includeBugs = args.include_bug_counts !== false;
1789
+
1790
+ // Get tracks
1791
+ const { data: tracks } = await supabase
1792
+ .from('qa_tracks')
1793
+ .select('id, name, icon, color')
1794
+ .eq('project_id', PROJECT_ID)
1795
+ .order('sort_order');
1796
+
1797
+ // Get test cases with track info
1798
+ const { data: testCases } = await supabase
1799
+ .from('test_cases')
1800
+ .select('id, target_route, category, track_id')
1801
+ .eq('project_id', PROJECT_ID);
1802
+
1803
+ // Get test assignments for execution data
1804
+ let assignments: any[] = [];
1805
+ if (includeExecution) {
1806
+ const { data } = await supabase
1807
+ .from('test_assignments')
1808
+ .select('test_case_id, status, completed_at')
1809
+ .eq('project_id', PROJECT_ID)
1810
+ .in('status', ['passed', 'failed']);
1811
+ assignments = data || [];
1812
+ }
1813
+
1814
+ // Get route stats for bug counts
1815
+ let routeStats: any[] = [];
1816
+ if (includeBugs) {
1817
+ const { data } = await supabase
1818
+ .from('route_test_stats')
1819
+ .select('route, open_bugs, critical_bugs')
1820
+ .eq('project_id', PROJECT_ID);
1821
+ routeStats = data || [];
1822
+ }
1823
+ const routeStatsMap = new Map(routeStats.map(r => [r.route, r]));
1824
+
1825
+ // Build assignment map
1826
+ const assignmentsByTestCase: Record<string, { passed: number; failed: number; lastTested: string | null }> = {};
1827
+ assignments.forEach(a => {
1828
+ if (!assignmentsByTestCase[a.test_case_id]) {
1829
+ assignmentsByTestCase[a.test_case_id] = { passed: 0, failed: 0, lastTested: null };
1830
+ }
1831
+ if (a.status === 'passed') assignmentsByTestCase[a.test_case_id].passed++;
1832
+ if (a.status === 'failed') assignmentsByTestCase[a.test_case_id].failed++;
1833
+ if (a.completed_at) {
1834
+ const current = assignmentsByTestCase[a.test_case_id].lastTested;
1835
+ if (!current || new Date(a.completed_at) > new Date(current)) {
1836
+ assignmentsByTestCase[a.test_case_id].lastTested = a.completed_at;
1837
+ }
1838
+ }
1839
+ });
1840
+
1841
+ // Group test cases by route
1842
+ const routeMap: Record<string, {
1843
+ testCases: any[];
1844
+ trackCoverage: Record<string, any[]>;
1845
+ }> = {};
1846
+
1847
+ (testCases || []).forEach(tc => {
1848
+ const route = tc.target_route || tc.category || 'Uncategorized';
1849
+ if (!routeMap[route]) {
1850
+ routeMap[route] = { testCases: [], trackCoverage: {} };
1851
+ }
1852
+ routeMap[route].testCases.push(tc);
1853
+
1854
+ const trackId = tc.track_id || 'none';
1855
+ if (!routeMap[route].trackCoverage[trackId]) {
1856
+ routeMap[route].trackCoverage[trackId] = [];
1857
+ }
1858
+ routeMap[route].trackCoverage[trackId].push(tc);
1859
+ });
1860
+
1861
+ // Build matrix
1862
+ const matrix: any[] = [];
1863
+ const now = Date.now();
1864
+
1865
+ Object.entries(routeMap).forEach(([route, data]) => {
1866
+ const row: any = {
1867
+ route,
1868
+ totalTests: data.testCases.length,
1869
+ tracks: {},
1870
+ };
1871
+
1872
+ if (includeBugs) {
1873
+ const stats = routeStatsMap.get(route);
1874
+ row.openBugs = stats?.open_bugs || 0;
1875
+ row.criticalBugs = stats?.critical_bugs || 0;
1876
+ }
1877
+
1878
+ // Calculate overall last tested
1879
+ let latestTest: string | null = null;
1880
+ data.testCases.forEach(tc => {
1881
+ const execData = assignmentsByTestCase[tc.id];
1882
+ if (execData?.lastTested) {
1883
+ if (!latestTest || new Date(execData.lastTested) > new Date(latestTest)) {
1884
+ latestTest = execData.lastTested;
1885
+ }
1886
+ }
1887
+ });
1888
+ row.lastTestedAt = latestTest;
1889
+
1890
+ // Build track cells
1891
+ (tracks || []).forEach(track => {
1892
+ const trackTests = data.trackCoverage[track.id] || [];
1893
+ const cell: any = {
1894
+ testCount: trackTests.length,
1895
+ };
1896
+
1897
+ if (includeExecution && trackTests.length > 0) {
1898
+ let passCount = 0;
1899
+ let failCount = 0;
1900
+ let trackLastTested: string | null = null;
1901
+
1902
+ trackTests.forEach(tc => {
1903
+ const execData = assignmentsByTestCase[tc.id];
1904
+ if (execData) {
1905
+ passCount += execData.passed;
1906
+ failCount += execData.failed;
1907
+ if (execData.lastTested) {
1908
+ if (!trackLastTested || new Date(execData.lastTested) > new Date(trackLastTested)) {
1909
+ trackLastTested = execData.lastTested;
1910
+ }
1911
+ }
1912
+ }
1913
+ });
1914
+
1915
+ cell.passCount = passCount;
1916
+ cell.failCount = failCount;
1917
+ cell.passRate = passCount + failCount > 0
1918
+ ? Math.round((passCount / (passCount + failCount)) * 100)
1919
+ : null;
1920
+ cell.lastTestedAt = trackLastTested;
1921
+ cell.staleDays = trackLastTested
1922
+ ? Math.floor((now - new Date(trackLastTested).getTime()) / (1000 * 60 * 60 * 24))
1923
+ : null;
1924
+ }
1925
+
1926
+ row.tracks[track.id] = cell;
1927
+ });
1928
+
1929
+ // Add "none" track for unassigned tests
1930
+ const unassignedTests = data.trackCoverage['none'] || [];
1931
+ if (unassignedTests.length > 0) {
1932
+ const cell: any = { testCount: unassignedTests.length };
1933
+ if (includeExecution) {
1934
+ let passCount = 0;
1935
+ let failCount = 0;
1936
+ unassignedTests.forEach(tc => {
1937
+ const execData = assignmentsByTestCase[tc.id];
1938
+ if (execData) {
1939
+ passCount += execData.passed;
1940
+ failCount += execData.failed;
1941
+ }
1942
+ });
1943
+ cell.passCount = passCount;
1944
+ cell.failCount = failCount;
1945
+ cell.passRate = passCount + failCount > 0
1946
+ ? Math.round((passCount / (passCount + failCount)) * 100)
1947
+ : null;
1948
+ }
1949
+ row.tracks['none'] = cell;
1950
+ }
1951
+
1952
+ matrix.push(row);
1953
+ });
1954
+
1955
+ // Sort by route name
1956
+ matrix.sort((a, b) => a.route.localeCompare(b.route));
1957
+
1958
+ return {
1959
+ matrix,
1960
+ tracks: (tracks || []).map(t => ({
1961
+ id: t.id,
1962
+ name: t.name,
1963
+ icon: t.icon,
1964
+ color: t.color,
1965
+ })),
1966
+ summary: {
1967
+ totalRoutes: matrix.length,
1968
+ totalTests: matrix.reduce((sum, r) => sum + r.totalTests, 0),
1969
+ routesWithCriticalBugs: includeBugs ? matrix.filter(r => r.criticalBugs > 0).length : undefined,
1970
+ },
1971
+ };
1972
+ }
1973
+
1974
+ async function getStaleCoverage(args: {
1975
+ days_threshold?: number;
1976
+ limit?: number;
1977
+ }) {
1978
+ const daysThreshold = args.days_threshold || 14;
1979
+ const limit = args.limit || 20;
1980
+
1981
+ // Refresh stats first
1982
+ await supabase.rpc('refresh_route_test_stats', { p_project_id: PROJECT_ID });
1983
+
1984
+ // Get routes ordered by staleness and risk
1985
+ const { data: routes, error } = await supabase
1986
+ .from('route_test_stats')
1987
+ .select('route, last_tested_at, open_bugs, critical_bugs, test_case_count, priority_score')
1988
+ .eq('project_id', PROJECT_ID)
1989
+ .order('last_tested_at', { ascending: true, nullsFirst: true })
1990
+ .limit(limit * 2); // Get extra to filter
1991
+
1992
+ if (error) {
1993
+ return { error: error.message };
1994
+ }
1995
+
1996
+ const now = Date.now();
1997
+ const staleRoutes: any[] = [];
1998
+
1999
+ (routes || []).forEach(route => {
2000
+ let daysSinceTest: number | null = null;
2001
+ if (route.last_tested_at) {
2002
+ daysSinceTest = Math.floor((now - new Date(route.last_tested_at).getTime()) / (1000 * 60 * 60 * 24));
2003
+ if (daysSinceTest < daysThreshold) return; // Not stale
2004
+ }
2005
+
2006
+ const riskLevel = route.critical_bugs > 0 ? 'critical' :
2007
+ route.open_bugs > 2 ? 'high' :
2008
+ daysSinceTest === null ? 'high' : 'medium';
2009
+
2010
+ staleRoutes.push({
2011
+ route: route.route,
2012
+ daysSinceTest,
2013
+ neverTested: route.last_tested_at === null,
2014
+ lastTestedAt: route.last_tested_at,
2015
+ openBugs: route.open_bugs,
2016
+ criticalBugs: route.critical_bugs,
2017
+ testCaseCount: route.test_case_count,
2018
+ riskLevel,
2019
+ priorityScore: route.priority_score,
2020
+ recommendation: route.last_tested_at === null
2021
+ ? 'Never tested - establish baseline coverage immediately'
2022
+ : `Last tested ${daysSinceTest} days ago - refresh testing`,
2023
+ });
2024
+ });
2025
+
2026
+ // Sort by risk then staleness
2027
+ staleRoutes.sort((a, b) => {
2028
+ const riskOrder = { critical: 0, high: 1, medium: 2 };
2029
+ const riskDiff = riskOrder[a.riskLevel as keyof typeof riskOrder] - riskOrder[b.riskLevel as keyof typeof riskOrder];
2030
+ if (riskDiff !== 0) return riskDiff;
2031
+ if (a.neverTested && !b.neverTested) return -1;
2032
+ if (!a.neverTested && b.neverTested) return 1;
2033
+ return (b.daysSinceTest || 999) - (a.daysSinceTest || 999);
2034
+ });
2035
+
2036
+ return {
2037
+ staleRoutes: staleRoutes.slice(0, limit),
2038
+ summary: {
2039
+ totalStale: staleRoutes.length,
2040
+ neverTested: staleRoutes.filter(r => r.neverTested).length,
2041
+ withCriticalBugs: staleRoutes.filter(r => r.criticalBugs > 0).length,
2042
+ threshold: daysThreshold,
2043
+ },
2044
+ guidance: staleRoutes.length > 0
2045
+ ? `${staleRoutes.length} route(s) have stale or missing test coverage. Prioritize routes with critical bugs first.`
2046
+ : 'All routes have been tested within the threshold period.',
2047
+ };
2048
+ }
2049
+
2050
+ async function generateDeployChecklist(args: {
2051
+ routes: string[];
2052
+ changed_files?: string[];
2053
+ deployment_type?: 'hotfix' | 'feature' | 'release';
2054
+ }) {
2055
+ const routes = args.routes;
2056
+ const deploymentType = args.deployment_type || 'feature';
2057
+
2058
+ // Infer additional routes from changed files
2059
+ const allRoutes = new Set(routes);
2060
+ if (args.changed_files) {
2061
+ args.changed_files.forEach(file => {
2062
+ // Extract route from common patterns
2063
+ const matches = [
2064
+ /\/app\/(.+?)\/page\./,
2065
+ /\/pages\/(.+?)\./,
2066
+ /\/routes\/(.+?)\./,
2067
+ /\/screens\/(.+?)\./,
2068
+ ];
2069
+ for (const pattern of matches) {
2070
+ const match = file.match(pattern);
2071
+ if (match) {
2072
+ allRoutes.add('/' + match[1].replace(/\[.*?\]/g, ':id'));
2073
+ }
2074
+ }
2075
+ });
2076
+ }
2077
+
2078
+ // Get test cases for these routes
2079
+ const { data: testCases } = await supabase
2080
+ .from('test_cases')
2081
+ .select('id, test_key, title, target_route, category, priority, track:qa_tracks(name)')
2082
+ .eq('project_id', PROJECT_ID)
2083
+ .or(routes.map(r => `target_route.eq.${r}`).join(',') + ',' + routes.map(r => `category.eq.${r}`).join(','));
2084
+
2085
+ // Get route stats for risk assessment
2086
+ const { data: routeStats } = await supabase
2087
+ .from('route_test_stats')
2088
+ .select('*')
2089
+ .eq('project_id', PROJECT_ID)
2090
+ .in('route', Array.from(allRoutes));
2091
+
2092
+ const routeStatsMap = new Map((routeStats || []).map(r => [r.route, r]));
2093
+
2094
+ // Categorize tests
2095
+ const checklist: {
2096
+ critical: any[];
2097
+ recommended: any[];
2098
+ optional: any[];
2099
+ gaps: any[];
2100
+ } = { critical: [], recommended: [], optional: [], gaps: [] };
2101
+
2102
+ // Track covered routes
2103
+ const coveredRoutes = new Set<string>();
2104
+
2105
+ (testCases || []).forEach(tc => {
2106
+ const route = tc.target_route || tc.category || '';
2107
+ coveredRoutes.add(route);
2108
+ const stats = routeStatsMap.get(route);
2109
+
2110
+ const item = {
2111
+ testCaseId: tc.id,
2112
+ testKey: tc.test_key,
2113
+ title: tc.title,
2114
+ route,
2115
+ track: (tc.track as any)?.name,
2116
+ priority: tc.priority,
2117
+ hasCriticalBugs: (stats?.critical_bugs || 0) > 0,
2118
+ lastTested: stats?.last_tested_at,
2119
+ reason: '',
2120
+ };
2121
+
2122
+ // Categorize based on priority and context
2123
+ if (tc.priority === 'P0' || (stats?.critical_bugs || 0) > 0) {
2124
+ item.reason = tc.priority === 'P0'
2125
+ ? 'P0 priority test case'
2126
+ : `Route has ${stats?.critical_bugs} critical bug(s)`;
2127
+ checklist.critical.push(item);
2128
+ } else if (tc.priority === 'P1' || deploymentType === 'hotfix') {
2129
+ item.reason = deploymentType === 'hotfix'
2130
+ ? 'Hotfix deployment - verify fix'
2131
+ : 'P1 priority test case';
2132
+ checklist.recommended.push(item);
2133
+ } else if (deploymentType === 'release') {
2134
+ item.reason = 'Release deployment - full verification';
2135
+ checklist.recommended.push(item);
2136
+ } else {
2137
+ item.reason = 'Standard test coverage';
2138
+ checklist.optional.push(item);
2139
+ }
2140
+ });
2141
+
2142
+ // Find coverage gaps
2143
+ allRoutes.forEach(route => {
2144
+ if (!coveredRoutes.has(route)) {
2145
+ const stats = routeStatsMap.get(route);
2146
+ checklist.gaps.push({
2147
+ route,
2148
+ title: `No test coverage for ${route}`,
2149
+ reason: 'Route is being deployed but has no test cases',
2150
+ hasCriticalBugs: (stats?.critical_bugs || 0) > 0,
2151
+ openBugs: stats?.open_bugs || 0,
2152
+ recommendation: `Create test cases for ${route} before deploying`,
2153
+ });
2154
+ }
2155
+ });
2156
+
2157
+ // Calculate thoroughness
2158
+ const totalTests = checklist.critical.length + checklist.recommended.length + checklist.optional.length;
2159
+ const thoroughness = allRoutes.size > 0
2160
+ ? Math.round((coveredRoutes.size / allRoutes.size) * 100)
2161
+ : 100;
2162
+
2163
+ let guidance = '';
2164
+ if (checklist.critical.length > 0) {
2165
+ guidance = `MUST RUN: ${checklist.critical.length} critical test(s) before deploying. `;
2166
+ }
2167
+ if (checklist.gaps.length > 0) {
2168
+ guidance += `WARNING: ${checklist.gaps.length} route(s) have no test coverage. `;
2169
+ }
2170
+ if (deploymentType === 'hotfix') {
2171
+ guidance += 'Hotfix mode: Focus on critical and recommended tests.';
2172
+ } else if (deploymentType === 'release') {
2173
+ guidance += 'Release mode: Run all tests for comprehensive verification.';
2174
+ }
2175
+
2176
+ return {
2177
+ checklist,
2178
+ summary: {
2179
+ criticalTests: checklist.critical.length,
2180
+ recommendedTests: checklist.recommended.length,
2181
+ optionalTests: checklist.optional.length,
2182
+ coverageGaps: checklist.gaps.length,
2183
+ thoroughness,
2184
+ deploymentType,
2185
+ },
2186
+ guidance: guidance || 'Ready to deploy with standard test coverage.',
2187
+ };
2188
+ }
2189
+
2190
+ async function getQAHealth(args: {
2191
+ period_days?: number;
2192
+ compare_previous?: boolean;
2193
+ }) {
2194
+ const periodDays = args.period_days || 30;
2195
+ const comparePrevious = args.compare_previous !== false;
2196
+
2197
+ const now = new Date();
2198
+ const periodStart = new Date(now.getTime() - periodDays * 24 * 60 * 60 * 1000);
2199
+ const previousStart = new Date(periodStart.getTime() - periodDays * 24 * 60 * 60 * 1000);
2200
+
2201
+ // Get current period data
2202
+ const { data: currentTests } = await supabase
2203
+ .from('test_assignments')
2204
+ .select('id, status, completed_at')
2205
+ .eq('project_id', PROJECT_ID)
2206
+ .gte('completed_at', periodStart.toISOString())
2207
+ .in('status', ['passed', 'failed']);
2208
+
2209
+ const { data: currentBugs } = await supabase
2210
+ .from('reports')
2211
+ .select('id, severity, status, created_at')
2212
+ .eq('project_id', PROJECT_ID)
2213
+ .eq('report_type', 'bug')
2214
+ .gte('created_at', periodStart.toISOString());
2215
+
2216
+ const { data: resolvedBugs } = await supabase
2217
+ .from('reports')
2218
+ .select('id, created_at, resolved_at')
2219
+ .eq('project_id', PROJECT_ID)
2220
+ .eq('report_type', 'bug')
2221
+ .in('status', ['resolved', 'fixed', 'verified', 'closed'])
2222
+ .gte('resolved_at', periodStart.toISOString());
2223
+
2224
+ const { data: testers } = await supabase
2225
+ .from('testers')
2226
+ .select('id, status')
2227
+ .eq('project_id', PROJECT_ID);
2228
+
2229
+ const { data: routeStats } = await supabase
2230
+ .from('route_test_stats')
2231
+ .select('route, test_case_count')
2232
+ .eq('project_id', PROJECT_ID);
2233
+
2234
+ // Get previous period data for comparison
2235
+ let previousTests: any[] = [];
2236
+ let previousBugs: any[] = [];
2237
+ let previousResolved: any[] = [];
2238
+
2239
+ if (comparePrevious) {
2240
+ const { data: pt } = await supabase
2241
+ .from('test_assignments')
2242
+ .select('id, status')
2243
+ .eq('project_id', PROJECT_ID)
2244
+ .gte('completed_at', previousStart.toISOString())
2245
+ .lt('completed_at', periodStart.toISOString())
2246
+ .in('status', ['passed', 'failed']);
2247
+ previousTests = pt || [];
2248
+
2249
+ const { data: pb } = await supabase
2250
+ .from('reports')
2251
+ .select('id, severity')
2252
+ .eq('project_id', PROJECT_ID)
2253
+ .eq('report_type', 'bug')
2254
+ .gte('created_at', previousStart.toISOString())
2255
+ .lt('created_at', periodStart.toISOString());
2256
+ previousBugs = pb || [];
2257
+
2258
+ const { data: pr } = await supabase
2259
+ .from('reports')
2260
+ .select('id')
2261
+ .eq('project_id', PROJECT_ID)
2262
+ .in('status', ['resolved', 'fixed', 'verified', 'closed'])
2263
+ .gte('resolved_at', previousStart.toISOString())
2264
+ .lt('resolved_at', periodStart.toISOString());
2265
+ previousResolved = pr || [];
2266
+ }
2267
+
2268
+ // Calculate metrics
2269
+ const testsCompleted = (currentTests || []).length;
2270
+ const testsPerWeek = Math.round(testsCompleted / (periodDays / 7));
2271
+ const prevTestsPerWeek = comparePrevious ? Math.round(previousTests.length / (periodDays / 7)) : 0;
2272
+
2273
+ const bugsFound = (currentBugs || []).length;
2274
+ const criticalBugs = (currentBugs || []).filter(b => b.severity === 'critical').length;
2275
+ const bugsPerTest = testsCompleted > 0 ? Math.round((bugsFound / testsCompleted) * 100) / 100 : 0;
2276
+ const prevBugsFound = previousBugs.length;
2277
+
2278
+ const bugsResolvedCount = (resolvedBugs || []).length;
2279
+ const prevResolvedCount = previousResolved.length;
2280
+
2281
+ // Calculate average resolution time (approximate)
2282
+ let avgResolutionDays = 0;
2283
+ if (resolvedBugs && resolvedBugs.length > 0) {
2284
+ const totalDays = resolvedBugs.reduce((sum, bug) => {
2285
+ if (bug.created_at && bug.resolved_at) {
2286
+ const days = (new Date(bug.resolved_at).getTime() - new Date(bug.created_at).getTime()) / (1000 * 60 * 60 * 24);
2287
+ return sum + days;
2288
+ }
2289
+ return sum;
2290
+ }, 0);
2291
+ avgResolutionDays = Math.round(totalDays / resolvedBugs.length);
2292
+ }
2293
+
2294
+ // Coverage metrics
2295
+ const totalRoutes = (routeStats || []).length;
2296
+ const routesWithTests = (routeStats || []).filter(r => r.test_case_count > 0).length;
2297
+ const routeCoverage = totalRoutes > 0 ? Math.round((routesWithTests / totalRoutes) * 100) : 0;
2298
+
2299
+ // Tester health
2300
+ const totalTesters = (testers || []).length;
2301
+ const activeTesters = (testers || []).filter(t => t.status === 'active').length;
2302
+ const utilizationPercent = totalTesters > 0 ? Math.round((activeTesters / totalTesters) * 100) : 0;
2303
+
2304
+ // Calculate trends
2305
+ const getTrend = (current: number, previous: number): 'up' | 'down' | 'stable' => {
2306
+ if (!comparePrevious || previous === 0) return 'stable';
2307
+ const change = ((current - previous) / previous) * 100;
2308
+ if (change > 10) return 'up';
2309
+ if (change < -10) return 'down';
2310
+ return 'stable';
2311
+ };
2312
+
2313
+ const getChangePercent = (current: number, previous: number): number | undefined => {
2314
+ if (!comparePrevious || previous === 0) return undefined;
2315
+ return Math.round(((current - previous) / previous) * 100);
2316
+ };
2317
+
2318
+ const metrics = {
2319
+ velocity: {
2320
+ testsPerWeek,
2321
+ testsCompleted,
2322
+ trend: getTrend(testsPerWeek, prevTestsPerWeek),
2323
+ changePercent: getChangePercent(testsPerWeek, prevTestsPerWeek),
2324
+ },
2325
+ bugDiscovery: {
2326
+ bugsFound,
2327
+ bugsPerTest,
2328
+ criticalBugs,
2329
+ trend: getTrend(bugsFound, prevBugsFound),
2330
+ changePercent: getChangePercent(bugsFound, prevBugsFound),
2331
+ },
2332
+ resolution: {
2333
+ bugsResolved: bugsResolvedCount,
2334
+ avgResolutionDays,
2335
+ trend: getTrend(bugsResolvedCount, prevResolvedCount),
2336
+ changePercent: getChangePercent(bugsResolvedCount, prevResolvedCount),
2337
+ },
2338
+ coverage: {
2339
+ routeCoverage,
2340
+ routesWithTests,
2341
+ totalRoutes,
2342
+ },
2343
+ testerHealth: {
2344
+ activeTesters,
2345
+ totalTesters,
2346
+ utilizationPercent,
2347
+ },
2348
+ };
2349
+
2350
+ // Calculate health score (0-100)
2351
+ const coverageScore = routeCoverage; // 0-100
2352
+ const velocityScore = Math.min(testsPerWeek * 10, 100); // 10+ tests/week = 100
2353
+ const resolutionScore = avgResolutionDays <= 3 ? 100 : avgResolutionDays <= 7 ? 75 : avgResolutionDays <= 14 ? 50 : 25;
2354
+ const stabilityScore = criticalBugs === 0 ? 100 : criticalBugs === 1 ? 75 : criticalBugs <= 3 ? 50 : 25;
2355
+
2356
+ const overallScore = Math.round(
2357
+ (coverageScore * 0.3) +
2358
+ (velocityScore * 0.25) +
2359
+ (resolutionScore * 0.25) +
2360
+ (stabilityScore * 0.2)
2361
+ );
2362
+
2363
+ const getGrade = (score: number): 'A' | 'B' | 'C' | 'D' | 'F' => {
2364
+ if (score >= 90) return 'A';
2365
+ if (score >= 80) return 'B';
2366
+ if (score >= 70) return 'C';
2367
+ if (score >= 60) return 'D';
2368
+ return 'F';
2369
+ };
2370
+
2371
+ const healthScore = {
2372
+ score: overallScore,
2373
+ grade: getGrade(overallScore),
2374
+ breakdown: {
2375
+ coverage: coverageScore,
2376
+ velocity: velocityScore,
2377
+ resolution: resolutionScore,
2378
+ stability: stabilityScore,
2379
+ },
2380
+ };
2381
+
2382
+ // Generate recommendations
2383
+ const recommendations: string[] = [];
2384
+ if (coverageScore < 70) {
2385
+ recommendations.push(`Increase test coverage (currently ${routeCoverage}%) - add tests for ${totalRoutes - routesWithTests} uncovered routes`);
2386
+ }
2387
+ if (velocityScore < 50) {
2388
+ recommendations.push(`Increase testing velocity (${testsPerWeek}/week) - aim for 10+ tests per week`);
2389
+ }
2390
+ if (avgResolutionDays > 7) {
2391
+ recommendations.push(`Improve bug resolution time (currently ${avgResolutionDays} days) - target <7 days`);
2392
+ }
2393
+ if (criticalBugs > 0) {
2394
+ recommendations.push(`Address ${criticalBugs} critical bug(s) immediately`);
2395
+ }
2396
+ if (utilizationPercent < 50) {
2397
+ recommendations.push(`Improve tester utilization (${utilizationPercent}%) - ${totalTesters - activeTesters} testers are inactive`);
2398
+ }
2399
+
2400
+ return {
2401
+ metrics,
2402
+ healthScore,
2403
+ recommendations,
2404
+ period: {
2405
+ days: periodDays,
2406
+ start: periodStart.toISOString(),
2407
+ end: now.toISOString(),
2408
+ },
2409
+ };
2410
+ }
2411
+
2412
+ async function analyzeChangesForTests(args: {
2413
+ changed_files: string[];
2414
+ change_type: 'feature' | 'bugfix' | 'refactor' | 'ui_change' | 'api_change' | 'config';
2415
+ change_summary: string;
2416
+ affected_routes?: string[];
2417
+ }) {
2418
+ // Get existing tests to check coverage
2419
+ const { data: existingTests } = await supabase
2420
+ .from('test_cases')
2421
+ .select('test_key, title, target_route, description')
2422
+ .eq('project_id', PROJECT_ID);
2423
+
2424
+ // Get next test key
2425
+ const { data: lastTest } = await supabase
2426
+ .from('test_cases')
2427
+ .select('test_key')
2428
+ .eq('project_id', PROJECT_ID)
2429
+ .order('test_key', { ascending: false })
2430
+ .limit(1);
2431
+
2432
+ const lastKey = lastTest?.[0]?.test_key || 'TC-000';
2433
+ const lastNum = parseInt(lastKey.replace('TC-', '')) || 0;
2434
+
2435
+ // Get bug history for these routes to understand risk areas
2436
+ const routes = args.affected_routes || [];
2437
+ let relatedBugs: any[] = [];
2438
+
2439
+ if (routes.length > 0) {
2440
+ const { data: bugs } = await supabase
2441
+ .from('reports')
2442
+ .select('id, description, severity, app_context')
2443
+ .eq('project_id', PROJECT_ID)
2444
+ .eq('report_type', 'bug')
2445
+ .limit(50);
2446
+
2447
+ relatedBugs = (bugs || []).filter(bug => {
2448
+ const bugRoute = (bug.app_context as any)?.currentRoute;
2449
+ return bugRoute && routes.some(r => bugRoute.includes(r) || r.includes(bugRoute));
2450
+ });
2451
+ }
2452
+
2453
+ // Analyze file types to determine what kind of tests are needed
2454
+ const fileAnalysis = analyzeFileTypes(args.changed_files);
2455
+
2456
+ // Check existing test coverage for affected routes
2457
+ const existingCoverage = (existingTests || []).filter(test =>
2458
+ routes.some(r => test.target_route?.includes(r) || test.title.toLowerCase().includes(r.toLowerCase()))
2459
+ );
2460
+
2461
+ // Generate intelligent suggestions based on change type and file analysis
2462
+ const suggestions: Array<{
2463
+ test_key: string;
2464
+ title: string;
2465
+ description: string;
2466
+ track: string;
2467
+ priority: string;
2468
+ target_route: string | null;
2469
+ rationale: string;
2470
+ steps: Array<{ stepNumber: number; action: string; expectedResult: string }>;
2471
+ expected_result: string;
2472
+ }> = [];
2473
+
2474
+ let testNum = lastNum;
2475
+
2476
+ // Change-type specific test suggestions
2477
+ if (args.change_type === 'feature') {
2478
+ // New features need comprehensive testing
2479
+ for (const route of routes.slice(0, 2)) {
2480
+ testNum++;
2481
+ suggestions.push({
2482
+ test_key: `TC-${String(testNum).padStart(3, '0')}`,
2483
+ title: `Verify new feature: ${args.change_summary.slice(0, 40)}`,
2484
+ description: `Test the new functionality added: ${args.change_summary}`,
2485
+ track: 'functional',
2486
+ priority: 'P1',
2487
+ target_route: route,
2488
+ rationale: 'New features require verification that they work as intended',
2489
+ steps: [
2490
+ { stepNumber: 1, action: `Navigate to ${route}`, expectedResult: 'Page loads successfully' },
2491
+ { stepNumber: 2, action: 'Locate the new feature/element', expectedResult: 'Feature is visible and accessible' },
2492
+ { stepNumber: 3, action: 'Interact with the new feature', expectedResult: 'Feature responds as expected' },
2493
+ { stepNumber: 4, action: 'Verify the expected outcome', expectedResult: 'Correct result is produced' },
2494
+ ],
2495
+ expected_result: 'New feature functions correctly without errors',
2496
+ });
2497
+ }
2498
+
2499
+ // Add edge case test for features
2500
+ if (routes.length > 0) {
2501
+ testNum++;
2502
+ suggestions.push({
2503
+ test_key: `TC-${String(testNum).padStart(3, '0')}`,
2504
+ title: `Edge cases: ${args.change_summary.slice(0, 35)}`,
2505
+ description: `Test edge cases and error handling for the new feature`,
2506
+ track: 'functional',
2507
+ priority: 'P2',
2508
+ target_route: routes[0],
2509
+ rationale: 'Edge cases often reveal bugs that happy-path testing misses',
2510
+ steps: [
2511
+ { stepNumber: 1, action: `Navigate to ${routes[0]}`, expectedResult: 'Page loads' },
2512
+ { stepNumber: 2, action: 'Test with empty/null input', expectedResult: 'Graceful handling, no crash' },
2513
+ { stepNumber: 3, action: 'Test with invalid input', expectedResult: 'Appropriate error message' },
2514
+ { stepNumber: 4, action: 'Test boundary conditions', expectedResult: 'Correct behavior at limits' },
2515
+ ],
2516
+ expected_result: 'Feature handles edge cases gracefully without errors',
2517
+ });
2518
+ }
2519
+ }
2520
+
2521
+ if (args.change_type === 'bugfix') {
2522
+ // Bug fixes need regression tests
2523
+ for (const route of routes.slice(0, 1)) {
2524
+ testNum++;
2525
+ suggestions.push({
2526
+ test_key: `TC-${String(testNum).padStart(3, '0')}`,
2527
+ title: `Regression: ${args.change_summary.slice(0, 40)}`,
2528
+ description: `Verify the bug fix works and hasn't regressed: ${args.change_summary}`,
2529
+ track: 'functional',
2530
+ priority: 'P1',
2531
+ target_route: route,
2532
+ rationale: 'Bug fixes should have regression tests to prevent recurrence',
2533
+ steps: [
2534
+ { stepNumber: 1, action: `Navigate to ${route}`, expectedResult: 'Page loads' },
2535
+ { stepNumber: 2, action: 'Reproduce the original bug scenario', expectedResult: 'Bug no longer occurs' },
2536
+ { stepNumber: 3, action: 'Test related functionality', expectedResult: 'No side effects from fix' },
2537
+ ],
2538
+ expected_result: 'Bug is fixed and related functionality still works',
2539
+ });
2540
+ }
2541
+ }
2542
+
2543
+ if (args.change_type === 'ui_change') {
2544
+ // UI changes need visual and interaction testing
2545
+ for (const route of routes.slice(0, 1)) {
2546
+ testNum++;
2547
+ suggestions.push({
2548
+ test_key: `TC-${String(testNum).padStart(3, '0')}`,
2549
+ title: `UI verification: ${args.change_summary.slice(0, 35)}`,
2550
+ description: `Verify UI changes display correctly across devices`,
2551
+ track: 'design',
2552
+ priority: 'P2',
2553
+ target_route: route,
2554
+ rationale: 'UI changes should be verified visually and for responsiveness',
2555
+ steps: [
2556
+ { stepNumber: 1, action: `Navigate to ${route}`, expectedResult: 'Page loads' },
2557
+ { stepNumber: 2, action: 'Verify visual appearance matches design', expectedResult: 'UI looks correct' },
2558
+ { stepNumber: 3, action: 'Test on mobile viewport (375px)', expectedResult: 'Responsive layout works' },
2559
+ { stepNumber: 4, action: 'Test interactive elements', expectedResult: 'Buttons, links work correctly' },
2560
+ ],
2561
+ expected_result: 'UI changes look correct and are responsive',
2562
+ });
2563
+ }
2564
+ }
2565
+
2566
+ if (args.change_type === 'api_change') {
2567
+ // API changes need integration testing
2568
+ for (const route of routes.slice(0, 1)) {
2569
+ testNum++;
2570
+ suggestions.push({
2571
+ test_key: `TC-${String(testNum).padStart(3, '0')}`,
2572
+ title: `API integration: ${args.change_summary.slice(0, 35)}`,
2573
+ description: `Verify API changes work correctly end-to-end`,
2574
+ track: 'functional',
2575
+ priority: 'P1',
2576
+ target_route: route,
2577
+ rationale: 'API changes can break frontend functionality',
2578
+ steps: [
2579
+ { stepNumber: 1, action: `Navigate to ${route}`, expectedResult: 'Page loads' },
2580
+ { stepNumber: 2, action: 'Trigger the API call', expectedResult: 'Request is sent' },
2581
+ { stepNumber: 3, action: 'Verify response handling', expectedResult: 'Data displays correctly' },
2582
+ { stepNumber: 4, action: 'Test error scenarios', expectedResult: 'Errors handled gracefully' },
2583
+ ],
2584
+ expected_result: 'API integration works correctly',
2585
+ });
2586
+ }
2587
+ }
2588
+
2589
+ // Add accessibility test if UI components were changed
2590
+ if (fileAnalysis.hasUIComponents && (args.change_type === 'feature' || args.change_type === 'ui_change')) {
2591
+ testNum++;
2592
+ suggestions.push({
2593
+ test_key: `TC-${String(testNum).padStart(3, '0')}`,
2594
+ title: `Accessibility: ${args.change_summary.slice(0, 35)}`,
2595
+ description: `Verify changes meet accessibility standards`,
2596
+ track: 'accessibility',
2597
+ priority: 'P2',
2598
+ target_route: routes[0] || null,
2599
+ rationale: 'UI changes should maintain accessibility compliance',
2600
+ steps: [
2601
+ { stepNumber: 1, action: 'Navigate using keyboard only (Tab)', expectedResult: 'All interactive elements reachable' },
2602
+ { stepNumber: 2, action: 'Check focus indicators', expectedResult: 'Focus is visible on all elements' },
2603
+ { stepNumber: 3, action: 'Verify with screen reader', expectedResult: 'Content is announced correctly' },
2604
+ ],
2605
+ expected_result: 'Changes are accessible to all users',
2606
+ });
2607
+ }
2608
+
2609
+ // Determine if tests are recommended
2610
+ const shouldCreateTests = suggestions.length > 0 &&
2611
+ (args.change_type !== 'config' && args.change_type !== 'refactor');
2612
+
2613
+ // Check if routes already have good coverage
2614
+ const coverageRatio = existingCoverage.length / Math.max(routes.length, 1);
2615
+
2616
+ return {
2617
+ analysis: {
2618
+ change_type: args.change_type,
2619
+ files_changed: args.changed_files.length,
2620
+ file_types: fileAnalysis,
2621
+ affected_routes: routes,
2622
+ existing_coverage: existingCoverage.length,
2623
+ related_bugs: relatedBugs.length,
2624
+ coverage_ratio: coverageRatio,
2625
+ },
2626
+ recommendation: {
2627
+ should_create_tests: shouldCreateTests,
2628
+ urgency: args.change_type === 'feature' || args.change_type === 'bugfix' ? 'high' :
2629
+ args.change_type === 'api_change' ? 'medium' : 'low',
2630
+ reason: shouldCreateTests
2631
+ ? `${args.change_type === 'feature' ? 'New features' : args.change_type === 'bugfix' ? 'Bug fixes' : 'Changes'} should have QA coverage to catch issues early.`
2632
+ : args.change_type === 'config' ? 'Config changes typically don\'t need manual QA tests.'
2633
+ : 'Refactoring with good existing coverage may not need new tests.',
2634
+ },
2635
+ suggestions: suggestions.map(s => ({
2636
+ ...s,
2637
+ create_command: `create_test_case with test_key="${s.test_key}", title="${s.title}", target_route="${s.target_route}"`,
2638
+ })),
2639
+ existing_tests: existingCoverage.slice(0, 5).map(t => ({
2640
+ test_key: t.test_key,
2641
+ title: t.title,
2642
+ target_route: t.target_route,
2643
+ })),
2644
+ next_steps: shouldCreateTests
2645
+ ? [
2646
+ 'Review the suggested tests above',
2647
+ 'Modify titles/steps as needed for your specific implementation',
2648
+ 'Use create_test_case to add the ones that make sense',
2649
+ 'Skip any that duplicate existing coverage',
2650
+ ]
2651
+ : ['No new tests recommended for this change type'],
2652
+ };
2653
+ }
2654
+
2655
+ function analyzeFileTypes(files: string[]): {
2656
+ hasUIComponents: boolean;
2657
+ hasAPIRoutes: boolean;
2658
+ hasTests: boolean;
2659
+ hasStyles: boolean;
2660
+ hasConfig: boolean;
2661
+ primaryType: string;
2662
+ } {
2663
+ const analysis = {
2664
+ hasUIComponents: false,
2665
+ hasAPIRoutes: false,
2666
+ hasTests: false,
2667
+ hasStyles: false,
2668
+ hasConfig: false,
2669
+ primaryType: 'unknown',
2670
+ };
2671
+
2672
+ for (const file of files) {
2673
+ const lower = file.toLowerCase();
2674
+
2675
+ if (lower.includes('component') || lower.endsWith('.tsx') || lower.endsWith('.jsx') ||
2676
+ lower.includes('/pages/') || lower.includes('/app/') || lower.includes('/screens/')) {
2677
+ analysis.hasUIComponents = true;
2678
+ }
2679
+
2680
+ if (lower.includes('/api/') || lower.includes('route.ts') || lower.includes('handler') ||
2681
+ lower.includes('service') || lower.includes('controller')) {
2682
+ analysis.hasAPIRoutes = true;
2683
+ }
2684
+
2685
+ if (lower.includes('.test.') || lower.includes('.spec.') || lower.includes('__tests__')) {
2686
+ analysis.hasTests = true;
2687
+ }
2688
+
2689
+ if (lower.endsWith('.css') || lower.endsWith('.scss') || lower.includes('style') ||
2690
+ lower.includes('tailwind')) {
2691
+ analysis.hasStyles = true;
2692
+ }
2693
+
2694
+ if (lower.includes('config') || lower.endsWith('.json') || lower.endsWith('.env') ||
2695
+ lower.includes('settings')) {
2696
+ analysis.hasConfig = true;
2697
+ }
2698
+ }
2699
+
2700
+ // Determine primary type
2701
+ if (analysis.hasUIComponents) analysis.primaryType = 'ui';
2702
+ else if (analysis.hasAPIRoutes) analysis.primaryType = 'api';
2703
+ else if (analysis.hasConfig) analysis.primaryType = 'config';
2704
+ else if (analysis.hasStyles) analysis.primaryType = 'styles';
2705
+
2706
+ return analysis;
2707
+ }
2708
+
2709
+ function getTrackTemplates(track: string) {
2710
+ const templates: Record<string, Array<{
2711
+ title: string;
2712
+ description: string;
2713
+ steps: Array<{ action: string; expectedResult: string }>;
2714
+ expected_result: string;
2715
+ }>> = {
2716
+ functional: [
2717
+ {
2718
+ title: 'Happy path navigation to {route}',
2719
+ description: 'Verify basic navigation and page load for {route}',
2720
+ steps: [
2721
+ { action: 'Navigate to {route}', expectedResult: 'Page loads without errors' },
2722
+ { action: 'Verify all main elements are visible', expectedResult: 'Headers, content, and navigation present' },
2723
+ { action: 'Check for console errors', expectedResult: 'No JavaScript errors in console' },
2724
+ ],
2725
+ expected_result: 'Page functions correctly with all elements visible',
2726
+ },
2727
+ {
2728
+ title: 'Error handling at {route}',
2729
+ description: 'Test error states and edge cases at {route}',
2730
+ steps: [
2731
+ { action: 'Navigate to {route}', expectedResult: 'Page loads' },
2732
+ { action: 'Trigger an error condition (e.g., invalid input)', expectedResult: 'Error message displayed' },
2733
+ { action: 'Verify error is user-friendly', expectedResult: 'Clear explanation of what went wrong' },
2734
+ ],
2735
+ expected_result: 'Errors are handled gracefully with clear messaging',
2736
+ },
2737
+ {
2738
+ title: 'Form submission at {route}',
2739
+ description: 'Test form validation and submission at {route}',
2740
+ steps: [
2741
+ { action: 'Navigate to {route}', expectedResult: 'Form is visible' },
2742
+ { action: 'Submit form with empty fields', expectedResult: 'Validation errors shown' },
2743
+ { action: 'Fill in valid data and submit', expectedResult: 'Success message or redirect' },
2744
+ ],
2745
+ expected_result: 'Form validates input and submits successfully',
2746
+ },
2747
+ ],
2748
+ design: [
2749
+ {
2750
+ title: 'Visual consistency at {route}',
2751
+ description: 'Check design system compliance at {route}',
2752
+ steps: [
2753
+ { action: 'Navigate to {route}', expectedResult: 'Page loads' },
2754
+ { action: 'Check typography matches design system', expectedResult: 'Fonts, sizes, weights are correct' },
2755
+ { action: 'Verify color usage', expectedResult: 'Colors match brand guidelines' },
2756
+ { action: 'Check spacing and alignment', expectedResult: 'Consistent margins and padding' },
2757
+ ],
2758
+ expected_result: 'Page matches design specifications',
2759
+ },
2760
+ {
2761
+ title: 'Responsive behavior at {route}',
2762
+ description: 'Test responsive design at {route}',
2763
+ steps: [
2764
+ { action: 'View {route} on desktop (1920px)', expectedResult: 'Full layout visible' },
2765
+ { action: 'Resize to tablet (768px)', expectedResult: 'Layout adapts appropriately' },
2766
+ { action: 'Resize to mobile (375px)', expectedResult: 'Mobile-friendly layout' },
2767
+ ],
2768
+ expected_result: 'Page is fully responsive across breakpoints',
2769
+ },
2770
+ ],
2771
+ accessibility: [
2772
+ {
2773
+ title: 'Keyboard navigation at {route}',
2774
+ description: 'Test keyboard accessibility at {route}',
2775
+ steps: [
2776
+ { action: 'Navigate to {route}', expectedResult: 'Page loads' },
2777
+ { action: 'Press Tab to navigate through elements', expectedResult: 'Focus moves in logical order' },
2778
+ { action: 'Verify focus indicators are visible', expectedResult: 'Clear focus ring on interactive elements' },
2779
+ { action: 'Test Enter/Space on buttons', expectedResult: 'Buttons activate correctly' },
2780
+ ],
2781
+ expected_result: 'Page is fully navigable by keyboard',
2782
+ },
2783
+ {
2784
+ title: 'Screen reader compatibility at {route}',
2785
+ description: 'Test screen reader accessibility at {route}',
2786
+ steps: [
2787
+ { action: 'Enable screen reader', expectedResult: 'Screen reader activates' },
2788
+ { action: 'Navigate to {route}', expectedResult: 'Page title is announced' },
2789
+ { action: 'Tab through content', expectedResult: 'All content is announced meaningfully' },
2790
+ { action: 'Check form labels', expectedResult: 'Inputs have associated labels' },
2791
+ ],
2792
+ expected_result: 'Page is fully accessible via screen reader',
2793
+ },
2794
+ ],
2795
+ performance: [
2796
+ {
2797
+ title: 'Page load performance at {route}',
2798
+ description: 'Measure load times at {route}',
2799
+ steps: [
2800
+ { action: 'Clear browser cache', expectedResult: 'Cache cleared' },
2801
+ { action: 'Navigate to {route}', expectedResult: 'Page loads' },
2802
+ { action: 'Check Network tab for load time', expectedResult: 'Initial load under 3 seconds' },
2803
+ { action: 'Note Largest Contentful Paint', expectedResult: 'LCP under 2.5 seconds' },
2804
+ ],
2805
+ expected_result: 'Page loads within performance budget',
2806
+ },
2807
+ ],
2808
+ };
2809
+
2810
+ return templates[track] || templates.functional;
2811
+ }
2812
+
2813
+ // === WRITE-BACK TOOL HANDLERS ===
2814
+
2815
+ async function createBugReport(args: {
2816
+ title: string;
2817
+ description: string;
2818
+ severity: string;
2819
+ file_path?: string;
2820
+ line_number?: number;
2821
+ code_snippet?: string;
2822
+ suggested_fix?: string;
2823
+ related_files?: string[];
2824
+ }) {
2825
+ // Build code context for the report
2826
+ const codeContext: Record<string, unknown> = {};
2827
+ if (args.file_path) {
2828
+ codeContext.file_path = args.file_path;
2829
+ codeContext.line_number = args.line_number;
2830
+ codeContext.code_snippet = args.code_snippet;
2831
+ codeContext.function_name = extractFunctionName(args.code_snippet);
2832
+ }
2833
+ if (args.related_files) {
2834
+ codeContext.related_files = args.related_files;
2835
+ }
2836
+ if (args.suggested_fix) {
2837
+ codeContext.suggested_fix = args.suggested_fix;
2838
+ }
2839
+
2840
+ const report = {
2841
+ project_id: PROJECT_ID,
2842
+ report_type: 'bug',
2843
+ title: args.title,
2844
+ description: args.description,
2845
+ severity: args.severity,
2846
+ status: 'new',
2847
+ app_context: {
2848
+ currentRoute: args.file_path || 'code',
2849
+ source: 'claude_code',
2850
+ timestamp: new Date().toISOString(),
2851
+ },
2852
+ device_info: {
2853
+ platform: 'claude_code',
2854
+ environment: 'development',
2855
+ },
2856
+ code_context: codeContext,
2857
+ };
2858
+
2859
+ const { data, error } = await supabase
2860
+ .from('reports')
2861
+ .insert(report)
2862
+ .select('id')
2863
+ .single();
2864
+
2865
+ if (error) {
2866
+ return { error: error.message };
2867
+ }
2868
+
2869
+ return {
2870
+ success: true,
2871
+ report_id: data.id,
2872
+ message: `Bug report created: ${args.title}`,
2873
+ details: {
2874
+ id: data.id,
2875
+ severity: args.severity,
2876
+ file: args.file_path,
2877
+ line: args.line_number,
2878
+ },
2879
+ };
2880
+ }
2881
+
2882
+ function extractFunctionName(codeSnippet?: string): string | undefined {
2883
+ if (!codeSnippet) return undefined;
2884
+
2885
+ // Try to extract function/component name from code
2886
+ const patterns = [
2887
+ /function\s+(\w+)/,
2888
+ /const\s+(\w+)\s*=\s*(?:async\s*)?\(/,
2889
+ /export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)/,
2890
+ /class\s+(\w+)/,
2891
+ /(\w+)\s*:\s*(?:React\.)?FC/,
2892
+ ];
2893
+
2894
+ for (const pattern of patterns) {
2895
+ const match = codeSnippet.match(pattern);
2896
+ if (match) return match[1];
2897
+ }
2898
+
2899
+ return undefined;
2900
+ }
2901
+
2902
+ async function getBugsForFile(args: {
2903
+ file_path: string;
2904
+ include_resolved?: boolean;
2905
+ }) {
2906
+ // Normalize the file path for matching
2907
+ const normalizedPath = args.file_path.replace(/^\.\//, '').replace(/^\//, '');
2908
+
2909
+ let query = supabase
2910
+ .from('reports')
2911
+ .select('id, title, description, severity, status, created_at, code_context')
2912
+ .eq('project_id', PROJECT_ID)
2913
+ .eq('report_type', 'bug');
2914
+
2915
+ if (!args.include_resolved) {
2916
+ query = query.in('status', ['new', 'confirmed', 'in_progress', 'reviewed']);
2917
+ }
2918
+
2919
+ const { data, error } = await query.order('created_at', { ascending: false });
2920
+
2921
+ if (error) {
2922
+ return { error: error.message };
2923
+ }
2924
+
2925
+ // Filter bugs that match the file path
2926
+ const matchingBugs = (data || []).filter(bug => {
2927
+ const codeContext = bug.code_context as Record<string, unknown> | null;
2928
+ if (!codeContext) return false;
2929
+
2930
+ const bugFilePath = codeContext.file_path as string | undefined;
2931
+ if (!bugFilePath) return false;
2932
+
2933
+ const normalizedBugPath = bugFilePath.replace(/^\.\//, '').replace(/^\//, '');
2934
+ return normalizedBugPath.includes(normalizedPath) || normalizedPath.includes(normalizedBugPath);
2935
+ });
2936
+
2937
+ // Also check related_files
2938
+ const relatedBugs = (data || []).filter(bug => {
2939
+ if (matchingBugs.includes(bug)) return false;
2940
+
2941
+ const codeContext = bug.code_context as Record<string, unknown> | null;
2942
+ const relatedFiles = codeContext?.related_files as string[] | undefined;
2943
+ if (!relatedFiles) return false;
2944
+
2945
+ return relatedFiles.some(f =>
2946
+ f.includes(normalizedPath) || normalizedPath.includes(f)
2947
+ );
2948
+ });
2949
+
2950
+ return {
2951
+ file: args.file_path,
2952
+ direct_bugs: matchingBugs.map(b => ({
2953
+ id: b.id,
2954
+ title: b.title,
2955
+ severity: b.severity,
2956
+ status: b.status,
2957
+ line: (b.code_context as any)?.line_number,
2958
+ description: b.description.slice(0, 200),
2959
+ })),
2960
+ related_bugs: relatedBugs.map(b => ({
2961
+ id: b.id,
2962
+ title: b.title,
2963
+ severity: b.severity,
2964
+ status: b.status,
2965
+ source_file: (b.code_context as any)?.file_path,
2966
+ })),
2967
+ summary: {
2968
+ total: matchingBugs.length + relatedBugs.length,
2969
+ critical: matchingBugs.filter(b => b.severity === 'critical').length,
2970
+ open: matchingBugs.filter(b => ['new', 'confirmed', 'in_progress'].includes(b.status)).length,
2971
+ },
2972
+ recommendation: matchingBugs.length > 0
2973
+ ? `Found ${matchingBugs.length} bug(s) in this file. Consider fixing them while you're here.`
2974
+ : 'No known bugs in this file.',
2975
+ };
2976
+ }
2977
+
2978
+ async function markFixedWithCommit(args: {
2979
+ report_id: string;
2980
+ commit_sha: string;
2981
+ commit_message?: string;
2982
+ resolution_notes?: string;
2983
+ files_changed?: string[];
2984
+ }) {
2985
+ if (!isValidUUID(args.report_id)) {
2986
+ return { error: 'Invalid report_id format' };
2987
+ }
2988
+
2989
+ // Get current report to preserve existing data
2990
+ const { data: existing, error: fetchError } = await supabase
2991
+ .from('reports')
2992
+ .select('code_context')
2993
+ .eq('id', args.report_id)
2994
+ .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
2995
+ .single();
2996
+
2997
+ if (fetchError) {
2998
+ return { error: fetchError.message };
2999
+ }
3000
+
3001
+ const existingContext = (existing?.code_context as Record<string, unknown>) || {};
3002
+
3003
+ const updates = {
3004
+ status: 'resolved',
3005
+ resolved_at: new Date().toISOString(),
3006
+ resolution: args.resolution_notes || `Fixed in commit ${args.commit_sha.slice(0, 7)}`,
3007
+ code_context: {
3008
+ ...existingContext,
3009
+ fix: {
3010
+ commit_sha: args.commit_sha,
3011
+ commit_message: args.commit_message,
3012
+ files_changed: args.files_changed,
3013
+ fixed_at: new Date().toISOString(),
3014
+ fixed_by: 'claude_code',
3015
+ },
3016
+ },
3017
+ };
3018
+
3019
+ const { error } = await supabase
3020
+ .from('reports')
3021
+ .update(updates)
3022
+ .eq('id', args.report_id)
3023
+ .eq('project_id', PROJECT_ID); // Security: ensure report belongs to this project
3024
+
3025
+ if (error) {
3026
+ return { error: error.message };
3027
+ }
3028
+
3029
+ return {
3030
+ success: true,
3031
+ message: `Bug marked as fixed in commit ${args.commit_sha.slice(0, 7)}`,
3032
+ report_id: args.report_id,
3033
+ commit: args.commit_sha,
3034
+ next_steps: [
3035
+ 'Consider running create_regression_test to prevent this bug from recurring',
3036
+ 'Push your changes to trigger CI/CD',
3037
+ ],
3038
+ };
3039
+ }
3040
+
3041
+ async function getBugsAffectingCode(args: {
3042
+ file_paths: string[];
3043
+ include_related?: boolean;
3044
+ }) {
3045
+ const includeRelated = args.include_related !== false;
3046
+
3047
+ const { data, error } = await supabase
3048
+ .from('reports')
3049
+ .select('id, title, description, severity, status, code_context, app_context')
3050
+ .eq('project_id', PROJECT_ID)
3051
+ .eq('report_type', 'bug')
3052
+ .in('status', ['new', 'confirmed', 'in_progress', 'reviewed'])
3053
+ .order('severity', { ascending: true });
3054
+
3055
+ if (error) {
3056
+ return { error: error.message };
3057
+ }
3058
+
3059
+ const normalizedPaths = args.file_paths.map(p =>
3060
+ p.replace(/^\.\//, '').replace(/^\//, '')
3061
+ );
3062
+
3063
+ const affectedBugs: Array<{
3064
+ id: string;
3065
+ title: string;
3066
+ severity: string;
3067
+ status: string;
3068
+ matched_file: string;
3069
+ match_type: 'direct' | 'related' | 'route';
3070
+ }> = [];
3071
+
3072
+ for (const bug of data || []) {
3073
+ const codeContext = bug.code_context as Record<string, unknown> | null;
3074
+ const appContext = bug.app_context as Record<string, unknown> | null;
3075
+
3076
+ // Check direct file match
3077
+ const bugFile = codeContext?.file_path as string | undefined;
3078
+ if (bugFile) {
3079
+ const normalizedBugFile = bugFile.replace(/^\.\//, '').replace(/^\//, '');
3080
+ for (const path of normalizedPaths) {
3081
+ if (normalizedBugFile.includes(path) || path.includes(normalizedBugFile)) {
3082
+ affectedBugs.push({
3083
+ id: bug.id,
3084
+ title: bug.title,
3085
+ severity: bug.severity,
3086
+ status: bug.status,
3087
+ matched_file: path,
3088
+ match_type: 'direct',
3089
+ });
3090
+ break;
3091
+ }
3092
+ }
3093
+ }
3094
+
3095
+ // Check related files
3096
+ if (includeRelated && codeContext?.related_files) {
3097
+ const relatedFiles = codeContext.related_files as string[];
3098
+ for (const relatedFile of relatedFiles) {
3099
+ for (const path of normalizedPaths) {
3100
+ if (relatedFile.includes(path) || path.includes(relatedFile)) {
3101
+ if (!affectedBugs.find(b => b.id === bug.id)) {
3102
+ affectedBugs.push({
3103
+ id: bug.id,
3104
+ title: bug.title,
3105
+ severity: bug.severity,
3106
+ status: bug.status,
3107
+ matched_file: path,
3108
+ match_type: 'related',
3109
+ });
3110
+ }
3111
+ break;
3112
+ }
3113
+ }
3114
+ }
3115
+ }
3116
+
3117
+ // Check route-based matches (for component files)
3118
+ const route = appContext?.currentRoute as string | undefined;
3119
+ if (route && route !== 'code') {
3120
+ for (const path of normalizedPaths) {
3121
+ // Match route to common file patterns
3122
+ if (path.includes('page') || path.includes('route') || path.includes('component')) {
3123
+ const pathParts = path.split('/');
3124
+ const fileName = pathParts[pathParts.length - 1].replace(/\.(tsx?|jsx?)$/, '');
3125
+ if (route.toLowerCase().includes(fileName.toLowerCase())) {
3126
+ if (!affectedBugs.find(b => b.id === bug.id)) {
3127
+ affectedBugs.push({
3128
+ id: bug.id,
3129
+ title: bug.title,
3130
+ severity: bug.severity,
3131
+ status: bug.status,
3132
+ matched_file: path,
3133
+ match_type: 'route',
3134
+ });
3135
+ }
3136
+ }
3137
+ }
3138
+ }
3139
+ }
3140
+ }
3141
+
3142
+ // Group by severity
3143
+ const critical = affectedBugs.filter(b => b.severity === 'critical');
3144
+ const high = affectedBugs.filter(b => b.severity === 'high');
3145
+ const other = affectedBugs.filter(b => !['critical', 'high'].includes(b.severity));
3146
+
3147
+ return {
3148
+ files_checked: args.file_paths,
3149
+ affected_bugs: affectedBugs,
3150
+ summary: {
3151
+ total: affectedBugs.length,
3152
+ critical: critical.length,
3153
+ high: high.length,
3154
+ direct_matches: affectedBugs.filter(b => b.match_type === 'direct').length,
3155
+ },
3156
+ warnings: critical.length > 0
3157
+ ? [`⚠️ ${critical.length} CRITICAL bug(s) may be affected by your changes!`]
3158
+ : [],
3159
+ recommendation: affectedBugs.length > 0
3160
+ ? `Review ${affectedBugs.length} potentially affected bug(s) before pushing.`
3161
+ : 'No known bugs affected by these changes.',
3162
+ };
3163
+ }
3164
+
3165
+ async function linkBugToCode(args: {
3166
+ report_id: string;
3167
+ file_path: string;
3168
+ line_number?: number;
3169
+ code_snippet?: string;
3170
+ function_name?: string;
3171
+ }) {
3172
+ if (!isValidUUID(args.report_id)) {
3173
+ return { error: 'Invalid report_id format' };
3174
+ }
3175
+
3176
+ // Get current report
3177
+ const { data: existing, error: fetchError } = await supabase
3178
+ .from('reports')
3179
+ .select('code_context')
3180
+ .eq('id', args.report_id)
3181
+ .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
3182
+ .single();
3183
+
3184
+ if (fetchError) {
3185
+ return { error: fetchError.message };
3186
+ }
3187
+
3188
+ const existingContext = (existing?.code_context as Record<string, unknown>) || {};
3189
+
3190
+ const updates = {
3191
+ code_context: {
3192
+ ...existingContext,
3193
+ file_path: args.file_path,
3194
+ line_number: args.line_number,
3195
+ code_snippet: args.code_snippet,
3196
+ function_name: args.function_name || extractFunctionName(args.code_snippet),
3197
+ linked_at: new Date().toISOString(),
3198
+ linked_by: 'claude_code',
3199
+ },
3200
+ };
3201
+
3202
+ const { error } = await supabase
3203
+ .from('reports')
3204
+ .update(updates)
3205
+ .eq('id', args.report_id)
3206
+ .eq('project_id', PROJECT_ID); // Security: ensure report belongs to this project
3207
+
3208
+ if (error) {
3209
+ return { error: error.message };
3210
+ }
3211
+
3212
+ return {
3213
+ success: true,
3214
+ message: `Bug linked to ${args.file_path}${args.line_number ? `:${args.line_number}` : ''}`,
3215
+ report_id: args.report_id,
3216
+ };
3217
+ }
3218
+
3219
+ async function createRegressionTest(args: {
3220
+ report_id: string;
3221
+ test_type?: string;
3222
+ }) {
3223
+ if (!isValidUUID(args.report_id)) {
3224
+ return { error: 'Invalid report_id format' };
3225
+ }
3226
+
3227
+ // Get the bug report details
3228
+ const { data: report, error: fetchError } = await supabase
3229
+ .from('reports')
3230
+ .select('*')
3231
+ .eq('id', args.report_id)
3232
+ .eq('project_id', PROJECT_ID) // Security: ensure report belongs to this project
3233
+ .single();
3234
+
3235
+ if (fetchError) {
3236
+ return { error: fetchError.message };
3237
+ }
3238
+
3239
+ if (report.status !== 'resolved') {
3240
+ return {
3241
+ error: 'Bug must be resolved before creating a regression test',
3242
+ current_status: report.status,
3243
+ };
3244
+ }
3245
+
3246
+ const codeContext = report.code_context as Record<string, unknown> | null;
3247
+ const testType = args.test_type || 'integration';
3248
+
3249
+ // Get next test key
3250
+ const { data: existingTests } = await supabase
3251
+ .from('test_cases')
3252
+ .select('test_key')
3253
+ .eq('project_id', PROJECT_ID)
3254
+ .order('test_key', { ascending: false })
3255
+ .limit(1);
3256
+
3257
+ const lastKey = existingTests?.[0]?.test_key || 'TC-000';
3258
+ const lastNum = parseInt(lastKey.replace('TC-', '')) || 0;
3259
+ const newKey = `TC-${String(lastNum + 1).padStart(3, '0')}`;
3260
+
3261
+ // Get the route from the bug's app_context for deep linking
3262
+ const appContext = report.app_context as Record<string, unknown> | null;
3263
+ const targetRoute = appContext?.currentRoute as string | null;
3264
+
3265
+ // Generate test case from bug
3266
+ const testCase = {
3267
+ project_id: PROJECT_ID,
3268
+ test_key: newKey,
3269
+ title: `Regression: ${report.title}`,
3270
+ description: `Regression test to prevent recurrence of bug #${args.report_id.slice(0, 8)}\n\nOriginal bug: ${report.description}`,
3271
+ priority: report.severity === 'critical' ? 'P0' : report.severity === 'high' ? 'P1' : 'P2',
3272
+ steps: [
3273
+ {
3274
+ stepNumber: 1,
3275
+ action: codeContext?.file_path
3276
+ ? `Navigate to the code/feature in ${codeContext.file_path}`
3277
+ : 'Navigate to the affected feature',
3278
+ expectedResult: 'Feature loads correctly',
3279
+ },
3280
+ {
3281
+ stepNumber: 2,
3282
+ action: 'Reproduce the original bug scenario',
3283
+ expectedResult: 'The bug should NOT occur',
3284
+ },
3285
+ {
3286
+ stepNumber: 3,
3287
+ action: 'Verify the fix is working',
3288
+ expectedResult: report.resolution || 'Feature works as expected',
3289
+ },
3290
+ ],
3291
+ expected_result: `The bug "${report.title}" should not recur`,
3292
+ preconditions: codeContext?.fix
3293
+ ? `Requires commit ${(codeContext.fix as any).commit_sha?.slice(0, 7) || 'unknown'} or later`
3294
+ : '',
3295
+ target_route: targetRoute,
3296
+ metadata: {
3297
+ source: 'regression_from_bug',
3298
+ original_bug_id: args.report_id,
3299
+ test_type: testType,
3300
+ created_by: 'claude_code',
3301
+ },
3302
+ };
3303
+
3304
+ const { data, error } = await supabase
3305
+ .from('test_cases')
3306
+ .insert(testCase)
3307
+ .select('id, test_key, title')
3308
+ .single();
3309
+
3310
+ if (error) {
3311
+ return { error: error.message };
3312
+ }
3313
+
3314
+ return {
3315
+ success: true,
3316
+ test_case: {
3317
+ id: data.id,
3318
+ test_key: data.test_key,
3319
+ title: data.title,
3320
+ type: testType,
3321
+ },
3322
+ message: `Regression test ${data.test_key} created from bug report`,
3323
+ original_bug: {
3324
+ id: args.report_id,
3325
+ title: report.title,
3326
+ },
3327
+ };
3328
+ }
3329
+
3330
+ // === FIX QUEUE TOOL HANDLERS ===
3331
+
3332
+ async function getPendingFixes(args: {
3333
+ limit?: number;
3334
+ include_claimed?: boolean;
3335
+ }) {
3336
+ const limit = args.limit || 10;
3337
+
3338
+ let query = supabase
3339
+ .from('fix_requests')
3340
+ .select(`
3341
+ id,
3342
+ title,
3343
+ description,
3344
+ prompt,
3345
+ file_path,
3346
+ status,
3347
+ claimed_at,
3348
+ claimed_by,
3349
+ created_at,
3350
+ report:reports(id, title, severity, description)
3351
+ `)
3352
+ .eq('project_id', PROJECT_ID)
3353
+ .order('created_at', { ascending: true })
3354
+ .limit(limit);
3355
+
3356
+ if (!args.include_claimed) {
3357
+ query = query.eq('status', 'pending');
3358
+ } else {
3359
+ query = query.in('status', ['pending', 'claimed']);
3360
+ }
3361
+
3362
+ const { data, error } = await query;
3363
+
3364
+ if (error) {
3365
+ return { error: error.message };
3366
+ }
3367
+
3368
+ if (!data || data.length === 0) {
3369
+ return {
3370
+ fix_requests: [],
3371
+ count: 0,
3372
+ message: 'No pending fix requests in the queue. Great job keeping up!',
3373
+ };
3374
+ }
3375
+
3376
+ return {
3377
+ fix_requests: data.map((fr: any) => ({
3378
+ id: fr.id,
3379
+ title: fr.title,
3380
+ description: fr.description,
3381
+ prompt: fr.prompt,
3382
+ file_path: fr.file_path,
3383
+ status: fr.status,
3384
+ claimed_by: fr.claimed_by,
3385
+ claimed_at: fr.claimed_at,
3386
+ created_at: fr.created_at,
3387
+ related_report: fr.report ? {
3388
+ id: fr.report.id,
3389
+ title: fr.report.title,
3390
+ severity: fr.report.severity,
3391
+ } : null,
3392
+ })),
3393
+ count: data.length,
3394
+ message: `Found ${data.length} fix request(s) waiting. Use claim_fix_request to start working on one.`,
3395
+ };
3396
+ }
3397
+
3398
+ async function claimFixRequest(args: {
3399
+ fix_request_id: string;
3400
+ claimed_by?: string;
3401
+ }) {
3402
+ if (!isValidUUID(args.fix_request_id)) {
3403
+ return { error: 'Invalid fix_request_id format' };
3404
+ }
3405
+
3406
+ // First check if it's still available
3407
+ const { data: existing, error: checkError } = await supabase
3408
+ .from('fix_requests')
3409
+ .select('id, status, claimed_by, prompt, title')
3410
+ .eq('id', args.fix_request_id)
3411
+ .eq('project_id', PROJECT_ID) // Security: ensure fix request belongs to this project
3412
+ .single();
3413
+
3414
+ if (checkError) {
3415
+ return { error: checkError.message };
3416
+ }
3417
+
3418
+ if (existing.status === 'claimed') {
3419
+ return {
3420
+ error: `This fix request is already claimed by ${existing.claimed_by || 'another instance'}`,
3421
+ status: existing.status,
3422
+ };
3423
+ }
3424
+
3425
+ if (existing.status === 'completed') {
3426
+ return {
3427
+ error: 'This fix request has already been completed',
3428
+ status: existing.status,
3429
+ };
3430
+ }
3431
+
3432
+ // Claim it
3433
+ const claimedBy = args.claimed_by || `claude-code-${Date.now()}`;
3434
+ const { error: updateError } = await supabase
3435
+ .from('fix_requests')
3436
+ .update({
3437
+ status: 'claimed',
3438
+ claimed_at: new Date().toISOString(),
3439
+ claimed_by: claimedBy,
3440
+ })
3441
+ .eq('id', args.fix_request_id)
3442
+ .eq('project_id', PROJECT_ID) // Security: ensure fix request belongs to this project
3443
+ .eq('status', 'pending'); // Only claim if still pending (race condition protection)
3444
+
3445
+ if (updateError) {
3446
+ return { error: updateError.message };
3447
+ }
3448
+
3449
+ return {
3450
+ success: true,
3451
+ message: `Fix request claimed successfully. Here's your task:`,
3452
+ fix_request: {
3453
+ id: args.fix_request_id,
3454
+ title: existing.title,
3455
+ prompt: existing.prompt,
3456
+ },
3457
+ next_steps: [
3458
+ '1. Read and understand the prompt below',
3459
+ '2. Implement the fix',
3460
+ '3. Test your changes',
3461
+ '4. Use complete_fix_request when done',
3462
+ ],
3463
+ };
3464
+ }
3465
+
3466
+ async function completeFixRequest(args: {
3467
+ fix_request_id: string;
3468
+ completion_notes?: string;
3469
+ success?: boolean;
3470
+ }) {
3471
+ if (!isValidUUID(args.fix_request_id)) {
3472
+ return { error: 'Invalid fix_request_id format' };
3473
+ }
3474
+
3475
+ const isSuccess = args.success !== false;
3476
+
3477
+ const updates: Record<string, unknown> = {
3478
+ status: isSuccess ? 'completed' : 'cancelled',
3479
+ completed_at: new Date().toISOString(),
3480
+ completion_notes: args.completion_notes || (isSuccess ? 'Fix completed' : 'Could not complete fix'),
3481
+ };
3482
+
3483
+ const { error } = await supabase
3484
+ .from('fix_requests')
3485
+ .update(updates)
3486
+ .eq('id', args.fix_request_id)
3487
+ .eq('project_id', PROJECT_ID); // Security: ensure fix request belongs to this project
3488
+
3489
+ if (error) {
3490
+ return { error: error.message };
3491
+ }
3492
+
3493
+ return {
3494
+ success: true,
3495
+ message: isSuccess
3496
+ ? 'Fix request marked as completed!'
3497
+ : 'Fix request marked as cancelled.',
3498
+ fix_request_id: args.fix_request_id,
3499
+ status: updates.status,
3500
+ };
3501
+ }
3502
+
3503
+ // === MCP PROMPTS FOR GUIDED WORKFLOWS ===
3504
+ const prompts = [
3505
+ {
3506
+ name: 'fix_bugs',
3507
+ description: 'Review and fix open bugs in the project. Shows critical bugs first and guides you through fixing them one by one.',
3508
+ arguments: [
3509
+ {
3510
+ name: 'severity',
3511
+ description: 'Filter by severity: critical, high, medium, low, or all (default: all)',
3512
+ required: false,
3513
+ },
3514
+ {
3515
+ name: 'file',
3516
+ description: 'Filter to bugs in a specific file path',
3517
+ required: false,
3518
+ },
3519
+ ],
3520
+ },
3521
+ {
3522
+ name: 'qa_check',
3523
+ description: 'Run a QA check on your recent changes. Analyzes changed files for potential issues and related bugs.',
3524
+ arguments: [
3525
+ {
3526
+ name: 'files',
3527
+ description: 'Comma-separated list of changed file paths (or leave empty to auto-detect from git)',
3528
+ required: false,
3529
+ },
3530
+ ],
3531
+ },
3532
+ {
3533
+ name: 'regression_tests',
3534
+ description: 'Generate regression tests for recently fixed bugs to prevent them from recurring.',
3535
+ arguments: [
3536
+ {
3537
+ name: 'limit',
3538
+ description: 'Number of recent fixes to generate tests for (default: 5)',
3539
+ required: false,
3540
+ },
3541
+ ],
3542
+ },
3543
+ {
3544
+ name: 'bug_hunt',
3545
+ description: 'Proactive bug hunting mode. Analyzes the codebase for potential issues based on common patterns and past bugs.',
3546
+ arguments: [
3547
+ {
3548
+ name: 'focus',
3549
+ description: 'Area to focus on: error_handling, null_checks, async_issues, type_safety, security',
3550
+ required: false,
3551
+ },
3552
+ ],
3553
+ },
3554
+ ];
3555
+
3556
+ async function generatePromptContent(name: string, args: Record<string, string>): Promise<string> {
3557
+ switch (name) {
3558
+ case 'fix_bugs': {
3559
+ const severity = args.severity || 'all';
3560
+ const fileFilter = args.file;
3561
+
3562
+ // First, check the fix queue for requested fixes from the dashboard
3563
+ const { data: fixRequests } = await supabase
3564
+ .from('fix_requests')
3565
+ .select(`
3566
+ id,
3567
+ title,
3568
+ description,
3569
+ prompt,
3570
+ file_path,
3571
+ status,
3572
+ created_at,
3573
+ report:reports(id, title, severity)
3574
+ `)
3575
+ .eq('project_id', PROJECT_ID)
3576
+ .eq('status', 'pending')
3577
+ .order('created_at', { ascending: true })
3578
+ .limit(5);
3579
+
3580
+ // Fetch open bugs
3581
+ let query = supabase
3582
+ .from('reports')
3583
+ .select('id, title, description, severity, status, code_context, created_at')
3584
+ .eq('project_id', PROJECT_ID)
3585
+ .eq('report_type', 'bug')
3586
+ .in('status', ['new', 'confirmed', 'in_progress']);
3587
+
3588
+ if (severity !== 'all') {
3589
+ query = query.eq('severity', severity);
3590
+ }
3591
+
3592
+ const { data: bugs } = await query.order('severity', { ascending: true }).limit(20);
3593
+
3594
+ // Filter by file if specified
3595
+ let filteredBugs = bugs || [];
3596
+ if (fileFilter) {
3597
+ filteredBugs = filteredBugs.filter(b => {
3598
+ const ctx = b.code_context as Record<string, unknown> | null;
3599
+ const filePath = ctx?.file_path as string | undefined;
3600
+ return filePath && filePath.includes(fileFilter);
3601
+ });
3602
+ }
3603
+
3604
+ // Build fix queue section if there are pending requests
3605
+ let fixQueueSection = '';
3606
+ if (fixRequests && fixRequests.length > 0) {
3607
+ const queueList = fixRequests.map((fr: any, i: number) => {
3608
+ return `
3609
+ ### ${i + 1}. ${fr.title}
3610
+ - **Fix Request ID:** \`${fr.id}\`
3611
+ - **File:** ${fr.file_path || 'Not specified'}
3612
+ - **Related Bug:** ${fr.report ? `[${fr.report.severity?.toUpperCase()}] ${fr.report.title}` : 'None'}
3613
+ - **Created:** ${new Date(fr.created_at).toLocaleDateString()}
3614
+
3615
+ <details>
3616
+ <summary>View Fix Prompt</summary>
3617
+
3618
+ \`\`\`
3619
+ ${fr.prompt}
3620
+ \`\`\`
3621
+ </details>
3622
+ `;
3623
+ }).join('\n');
3624
+
3625
+ fixQueueSection = `
3626
+ # 🔧 Fix Queue (Priority)
3627
+
3628
+ **${fixRequests.length} fix request(s)** have been queued from the BugBear dashboard:
3629
+
3630
+ ${queueList}
3631
+
3632
+ ---
3633
+
3634
+ **To start on a fix request:**
3635
+ 1. Use \`claim_fix_request\` with the fix request ID
3636
+ 2. Follow the prompt instructions
3637
+ 3. Use \`complete_fix_request\` when done
3638
+
3639
+ ---
3640
+
3641
+ `;
3642
+ }
3643
+
3644
+ if (filteredBugs.length === 0 && (!fixRequests || fixRequests.length === 0)) {
3645
+ return `# No Open Bugs Found
3646
+
3647
+ Great news! There are no open bugs or fix requests matching your criteria.
3648
+
3649
+ ${severity !== 'all' ? `Filtered by severity: ${severity}` : ''}
3650
+ ${fileFilter ? `Filtered by file: ${fileFilter}` : ''}
3651
+
3652
+ Use \`list_reports\` to see all reports including resolved ones.`;
3653
+ }
3654
+
3655
+ const bugList = filteredBugs.map((b, i) => {
3656
+ const ctx = b.code_context as Record<string, unknown> | null;
3657
+ return `
3658
+ ## ${i + 1}. [${b.severity?.toUpperCase()}] ${b.title}
3659
+ - **ID:** \`${b.id}\`
3660
+ - **Status:** ${b.status}
3661
+ - **File:** ${ctx?.file_path || 'Unknown'}${ctx?.line_number ? `:${ctx.line_number}` : ''}
3662
+ - **Description:** ${b.description.slice(0, 200)}${b.description.length > 200 ? '...' : ''}
3663
+ ${ctx?.suggested_fix ? `- **Suggested Fix:** ${ctx.suggested_fix}` : ''}
3664
+ `;
3665
+ }).join('\n');
3666
+
3667
+ return `${fixQueueSection}# Bug Fixing Session
3668
+
3669
+ Found **${filteredBugs.length}** open bug(s) to fix.
3670
+
3671
+ ${bugList}
3672
+
3673
+ ---
3674
+
3675
+ ## Workflow
3676
+
3677
+ 1. **Read the bug details** - Use \`get_report\` with the bug ID for full context
3678
+ 2. **Check the code** - Use \`get_bugs_for_file\` to see related issues
3679
+ 3. **Fix the bug** - Make your code changes
3680
+ 4. **Mark as fixed** - Use \`mark_fixed_with_commit\` after committing
3681
+ 5. **Create regression test** - Use \`create_regression_test\` to prevent recurrence
3682
+
3683
+ ${fixRequests && fixRequests.length > 0 ? '**Start with the Fix Queue items above** - these have been specifically requested by users!' : "Let's start with the most critical bug. Which one would you like to tackle first?"}`;
3684
+ }
3685
+
3686
+ case 'qa_check': {
3687
+ const filesArg = args.files;
3688
+ let filePaths: string[] = [];
3689
+
3690
+ if (filesArg) {
3691
+ filePaths = filesArg.split(',').map(f => f.trim());
3692
+ }
3693
+
3694
+ // If no files specified, we'll provide instructions
3695
+ if (filePaths.length === 0) {
3696
+ return `# QA Check Mode
3697
+
3698
+ I'll analyze your changes for potential issues and related bugs.
3699
+
3700
+ **To get started, tell me which files you've changed:**
3701
+
3702
+ Option 1: Run \`git diff --name-only\` and provide the file list
3703
+ Option 2: Tell me which files you're working on
3704
+
3705
+ Once I know the files, I'll:
3706
+ 1. Check for known bugs in those files
3707
+ 2. Look for related issues that might be affected
3708
+ 3. Suggest test cases to verify your changes
3709
+ 4. Identify potential problem patterns
3710
+
3711
+ What files have you changed?`;
3712
+ }
3713
+
3714
+ // Analyze the files
3715
+ const analysis = await getBugsAffectingCode({ file_paths: filePaths });
3716
+
3717
+ return `# QA Check Results
3718
+
3719
+ Analyzed **${filePaths.length}** file(s):
3720
+ ${filePaths.map(f => `- ${f}`).join('\n')}
3721
+
3722
+ ## Bug Analysis
3723
+
3724
+ ${(analysis as any).affected_bugs?.length > 0
3725
+ ? `Found **${(analysis as any).affected_bugs.length}** potentially affected bug(s):
3726
+
3727
+ ${(analysis as any).affected_bugs.map((b: any) => `- [${b.severity.toUpperCase()}] ${b.title} (${b.match_type} match in ${b.matched_file})`).join('\n')}
3728
+
3729
+ ${(analysis as any).warnings?.join('\n') || ''}
3730
+ `
3731
+ : '✅ No known bugs affected by these changes.'}
3732
+
3733
+ ## Recommended Actions
3734
+
3735
+ 1. ${(analysis as any).affected_bugs?.length > 0 ? 'Review the affected bugs above before pushing' : 'Proceed with your changes'}
3736
+ 2. Use \`suggest_test_cases\` to generate tests for your changed files
3737
+ 3. Consider running the app and manually testing the affected areas
3738
+
3739
+ Would you like me to generate test cases for these files?`;
3740
+ }
3741
+
3742
+ case 'regression_tests': {
3743
+ const limit = parseInt(args.limit) || 5;
3744
+
3745
+ // Get recently resolved bugs
3746
+ const { data: resolvedBugs } = await supabase
3747
+ .from('reports')
3748
+ .select('id, title, description, severity, resolved_at, code_context')
3749
+ .eq('project_id', PROJECT_ID)
3750
+ .eq('report_type', 'bug')
3751
+ .eq('status', 'resolved')
3752
+ .order('resolved_at', { ascending: false })
3753
+ .limit(limit);
3754
+
3755
+ if (!resolvedBugs || resolvedBugs.length === 0) {
3756
+ return `# Regression Test Generation
3757
+
3758
+ No recently resolved bugs found.
3759
+
3760
+ To create regression tests:
3761
+ 1. First fix some bugs and mark them as resolved using \`mark_fixed_with_commit\`
3762
+ 2. Then come back here to generate regression tests
3763
+
3764
+ Alternatively, use \`suggest_test_cases\` to generate general test cases.`;
3765
+ }
3766
+
3767
+ // Check which ones already have regression tests
3768
+ const bugList = resolvedBugs.map((b, i) => {
3769
+ const ctx = b.code_context as Record<string, unknown> | null;
3770
+ const hasCommit = ctx?.fix && (ctx.fix as any).commit_sha;
3771
+
3772
+ return `
3773
+ ## ${i + 1}. ${b.title}
3774
+ - **Bug ID:** \`${b.id}\`
3775
+ - **Severity:** ${b.severity}
3776
+ - **Resolved:** ${b.resolved_at ? new Date(b.resolved_at).toLocaleDateString() : 'Unknown'}
3777
+ - **Commit:** ${hasCommit ? (ctx!.fix as any).commit_sha.slice(0, 7) : 'Not linked'}
3778
+ `;
3779
+ }).join('\n');
3780
+
3781
+ return `# Regression Test Generation
3782
+
3783
+ Found **${resolvedBugs.length}** recently resolved bug(s) that need regression tests:
3784
+
3785
+ ${bugList}
3786
+
3787
+ ---
3788
+
3789
+ ## Generate Tests
3790
+
3791
+ For each bug, I can create a regression test to prevent it from recurring.
3792
+
3793
+ Use \`create_regression_test\` with the bug ID to generate a test case.
3794
+
3795
+ Example:
3796
+ \`\`\`
3797
+ create_regression_test(report_id: "${resolvedBugs[0]?.id}", test_type: "integration")
3798
+ \`\`\`
3799
+
3800
+ Would you like me to generate regression tests for all of these bugs?`;
3801
+ }
3802
+
3803
+ case 'bug_hunt': {
3804
+ const focus = args.focus || 'general';
3805
+
3806
+ const focusDescriptions: Record<string, string> = {
3807
+ error_handling: 'missing try/catch blocks, unhandled promise rejections, missing error boundaries',
3808
+ null_checks: 'potential null/undefined access, optional chaining opportunities, nullish coalescing',
3809
+ async_issues: 'race conditions, missing await, unhandled async errors, memory leaks',
3810
+ type_safety: 'type assertions, any types, missing type guards, unsafe casts',
3811
+ security: 'XSS vulnerabilities, injection risks, exposed secrets, insecure data handling',
3812
+ general: 'common patterns that have caused bugs in this project',
3813
+ };
3814
+
3815
+ // Get bug patterns for context
3816
+ const patterns = await getBugPatterns({});
3817
+
3818
+ return `# Bug Hunt Mode: ${focus.charAt(0).toUpperCase() + focus.slice(1)}
3819
+
3820
+ I'm looking for: **${focusDescriptions[focus] || focusDescriptions.general}**
3821
+
3822
+ ## Historical Bug Hotspots
3823
+
3824
+ ${(patterns as any).hotspots?.slice(0, 5).map((h: any) =>
3825
+ `- **${h.route}**: ${h.total} bugs (${h.open} open, ${h.critical} critical)`
3826
+ ).join('\n') || 'No bug patterns found yet.'}
3827
+
3828
+ ## Bug Hunt Checklist
3829
+
3830
+ Based on past bugs in this project, here's what to look for:
3831
+
3832
+ ${focus === 'error_handling' || focus === 'general' ? `
3833
+ ### Error Handling
3834
+ - [ ] All async functions have try/catch or .catch()
3835
+ - [ ] API calls handle network errors
3836
+ - [ ] User-facing error messages are helpful
3837
+ - [ ] Errors are logged appropriately
3838
+ ` : ''}
3839
+
3840
+ ${focus === 'null_checks' || focus === 'general' ? `
3841
+ ### Null Safety
3842
+ - [ ] Optional chaining (?.) used for potentially undefined values
3843
+ - [ ] Nullish coalescing (??) for default values
3844
+ - [ ] Array/object access is guarded
3845
+ - [ ] Props have appropriate defaults
3846
+ ` : ''}
3847
+
3848
+ ${focus === 'async_issues' || focus === 'general' ? `
3849
+ ### Async Issues
3850
+ - [ ] All Promises are awaited or have .then()
3851
+ - [ ] Cleanup functions in useEffect
3852
+ - [ ] Race conditions prevented with AbortController
3853
+ - [ ] Loading states are handled
3854
+ ` : ''}
3855
+
3856
+ ---
3857
+
3858
+ **To report bugs you find:** Use \`create_bug_report\` with the file, line number, and code snippet.
3859
+
3860
+ Which files or areas would you like me to analyze?`;
3861
+ }
3862
+
3863
+ default:
3864
+ return 'Unknown prompt';
3865
+ }
3866
+ }
3867
+
3868
+ // Main server setup
3869
+ async function main() {
3870
+ initSupabase();
3871
+
3872
+ const server = new Server(
3873
+ {
3874
+ name: 'bugbear-mcp',
3875
+ version: '0.1.0',
3876
+ },
3877
+ {
3878
+ capabilities: {
3879
+ tools: {},
3880
+ resources: {},
3881
+ prompts: {},
3882
+ },
3883
+ }
3884
+ );
3885
+
3886
+ // Handle tool listing
3887
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
3888
+ tools,
3889
+ }));
3890
+
3891
+ // Handle tool execution
3892
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3893
+ const { name, arguments: args } = request.params;
3894
+
3895
+ try {
3896
+ let result: unknown;
3897
+
3898
+ switch (name) {
3899
+ case 'list_reports':
3900
+ result = await listReports(args as any);
3901
+ break;
3902
+ case 'get_report':
3903
+ result = await getReport(args as any);
3904
+ break;
3905
+ case 'search_reports':
3906
+ result = await searchReports(args as any);
3907
+ break;
3908
+ case 'update_report_status':
3909
+ result = await updateReportStatus(args as any);
3910
+ break;
3911
+ case 'get_report_context':
3912
+ result = await getReportContext(args as any);
3913
+ break;
3914
+ case 'get_project_info':
3915
+ result = await getProjectInfo();
3916
+ break;
3917
+ case 'get_qa_tracks':
3918
+ result = await getQaTracks();
3919
+ break;
3920
+ case 'create_test_case':
3921
+ result = await createTestCase(args as any);
3922
+ break;
3923
+ case 'update_test_case':
3924
+ result = await updateTestCase(args as any);
3925
+ break;
3926
+ case 'list_test_cases':
3927
+ result = await listTestCases(args as any);
3928
+ break;
3929
+ case 'get_bug_patterns':
3930
+ result = await getBugPatterns(args as any);
3931
+ break;
3932
+ case 'analyze_changes_for_tests':
3933
+ result = await analyzeChangesForTests(args as any);
3934
+ break;
3935
+ case 'suggest_test_cases':
3936
+ result = await suggestTestCases(args as any);
3937
+ break;
3938
+ // === QA INTELLIGENCE TOOLS ===
3939
+ case 'get_test_priorities':
3940
+ result = await getTestPriorities(args as any);
3941
+ break;
3942
+ case 'get_coverage_gaps':
3943
+ result = await getCoverageGaps(args as any);
3944
+ break;
3945
+ case 'get_regressions':
3946
+ result = await getRegressions(args as any);
3947
+ break;
3948
+ case 'get_coverage_matrix':
3949
+ result = await getCoverageMatrix(args as any);
3950
+ break;
3951
+ case 'get_stale_coverage':
3952
+ result = await getStaleCoverage(args as any);
3953
+ break;
3954
+ case 'generate_deploy_checklist':
3955
+ result = await generateDeployChecklist(args as any);
3956
+ break;
3957
+ case 'get_qa_health':
3958
+ result = await getQAHealth(args as any);
3959
+ break;
3960
+ // === WRITE-BACK TOOLS ===
3961
+ case 'create_bug_report':
3962
+ result = await createBugReport(args as any);
3963
+ break;
3964
+ case 'get_bugs_for_file':
3965
+ result = await getBugsForFile(args as any);
3966
+ break;
3967
+ case 'mark_fixed_with_commit':
3968
+ result = await markFixedWithCommit(args as any);
3969
+ break;
3970
+ case 'get_bugs_affecting_code':
3971
+ result = await getBugsAffectingCode(args as any);
3972
+ break;
3973
+ case 'link_bug_to_code':
3974
+ result = await linkBugToCode(args as any);
3975
+ break;
3976
+ case 'create_regression_test':
3977
+ result = await createRegressionTest(args as any);
3978
+ break;
3979
+ // === FIX QUEUE TOOLS ===
3980
+ case 'get_pending_fixes':
3981
+ result = await getPendingFixes(args as any);
3982
+ break;
3983
+ case 'claim_fix_request':
3984
+ result = await claimFixRequest(args as any);
3985
+ break;
3986
+ case 'complete_fix_request':
3987
+ result = await completeFixRequest(args as any);
3988
+ break;
3989
+ default:
3990
+ return {
3991
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
3992
+ isError: true,
3993
+ };
3994
+ }
3995
+
3996
+ return {
3997
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3998
+ };
3999
+ } catch (err) {
4000
+ return {
4001
+ content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : 'Unknown error'}` }],
4002
+ isError: true,
4003
+ };
4004
+ }
4005
+ });
4006
+
4007
+ // Handle resource listing (reports as resources)
4008
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
4009
+ const { data } = await supabase
4010
+ .from('reports')
4011
+ .select('id, description, report_type, severity')
4012
+ .eq('project_id', PROJECT_ID)
4013
+ .eq('status', 'new')
4014
+ .order('created_at', { ascending: false })
4015
+ .limit(10);
4016
+
4017
+ return {
4018
+ resources: (data || []).map(r => ({
4019
+ uri: `bugbear://reports/${r.id}`,
4020
+ name: `[${r.severity?.toUpperCase() || 'N/A'}] ${r.description.slice(0, 50)}...`,
4021
+ description: `${r.report_type} report`,
4022
+ mimeType: 'application/json',
4023
+ })),
4024
+ };
4025
+ });
4026
+
4027
+ // Handle prompt listing
4028
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
4029
+ prompts,
4030
+ }));
4031
+
4032
+ // Handle prompt execution
4033
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
4034
+ const { name, arguments: args } = request.params;
4035
+
4036
+ const content = await generatePromptContent(name, args || {});
4037
+
4038
+ return {
4039
+ description: prompts.find(p => p.name === name)?.description || '',
4040
+ messages: [
4041
+ {
4042
+ role: 'user',
4043
+ content: {
4044
+ type: 'text',
4045
+ text: content,
4046
+ },
4047
+ },
4048
+ ],
4049
+ };
4050
+ });
4051
+
4052
+ // Handle resource reading
4053
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
4054
+ const uri = request.params.uri;
4055
+ const match = uri.match(/^bugbear:\/\/reports\/(.+)$/);
4056
+
4057
+ if (!match) {
4058
+ throw new Error(`Invalid resource URI: ${uri}`);
4059
+ }
4060
+
4061
+ const reportId = match[1];
4062
+ const result = await getReport({ report_id: reportId });
4063
+
4064
+ return {
4065
+ contents: [
4066
+ {
4067
+ uri,
4068
+ mimeType: 'application/json',
4069
+ text: JSON.stringify(result, null, 2),
4070
+ },
4071
+ ],
4072
+ };
4073
+ });
4074
+
4075
+ // Start the server
4076
+ const transport = new StdioServerTransport();
4077
+ await server.connect(transport);
4078
+ console.error('BugBear MCP server started');
4079
+ }
4080
+
4081
+ main().catch((err) => {
4082
+ console.error('Failed to start server:', err);
4083
+ process.exit(1);
4084
+ });