@bbearai/mcp-server 0.3.0 → 0.3.4

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 CHANGED
@@ -144,7 +144,7 @@ const tools = [
144
144
  enum: ['new', 'triaging', 'confirmed', 'in_progress', 'fixed', 'resolved', 'verified', 'wont_fix', 'duplicate'],
145
145
  description: 'The new status for the report',
146
146
  },
147
- resolution: {
147
+ resolution_notes: {
148
148
  type: 'string',
149
149
  description: 'Optional resolution notes when marking as resolved',
150
150
  },
@@ -739,6 +739,159 @@ const tools = [
739
739
  },
740
740
  },
741
741
  },
742
+ // === SPRINT 5: QA INTELLIGENCE EXPANSION ===
743
+ {
744
+ name: 'get_qa_sessions',
745
+ description: 'Get QA session history showing exploratory testing sessions. Sessions capture findings that may or may not become formal bugs. Use this to understand tester exploration patterns and unstructured discoveries.',
746
+ inputSchema: {
747
+ type: 'object' as const,
748
+ properties: {
749
+ status: {
750
+ type: 'string',
751
+ enum: ['active', 'completed', 'all'],
752
+ description: 'Filter by session status (default: all)',
753
+ },
754
+ tester_id: {
755
+ type: 'string',
756
+ description: 'Filter by specific tester UUID',
757
+ },
758
+ limit: {
759
+ type: 'number',
760
+ description: 'Maximum sessions to return (default: 20)',
761
+ },
762
+ include_findings: {
763
+ type: 'boolean',
764
+ description: 'Include session findings in response (default: true)',
765
+ },
766
+ },
767
+ },
768
+ },
769
+ {
770
+ name: 'get_qa_alerts',
771
+ description: 'Get active QA alerts including hot spots (routes with many bugs), coverage gaps (untested areas), and track gaps. Alerts are proactively generated to highlight areas needing attention.',
772
+ inputSchema: {
773
+ type: 'object' as const,
774
+ properties: {
775
+ severity: {
776
+ type: 'string',
777
+ enum: ['critical', 'warning', 'info', 'all'],
778
+ description: 'Filter by alert severity (default: all)',
779
+ },
780
+ type: {
781
+ type: 'string',
782
+ enum: ['hot_spot', 'coverage_gap', 'track_gap', 'regression', 'all'],
783
+ description: 'Filter by alert type (default: all)',
784
+ },
785
+ status: {
786
+ type: 'string',
787
+ enum: ['active', 'acknowledged', 'all'],
788
+ description: 'Filter by status (default: active)',
789
+ },
790
+ refresh: {
791
+ type: 'boolean',
792
+ description: 'Refresh alerts by running detection (default: false)',
793
+ },
794
+ },
795
+ },
796
+ },
797
+ {
798
+ name: 'get_deployment_analysis',
799
+ description: 'Get deployment history with risk analysis. Shows what files/routes were affected by each deploy and the calculated risk score. Use this to understand deployment impact on QA.',
800
+ inputSchema: {
801
+ type: 'object' as const,
802
+ properties: {
803
+ deployment_id: {
804
+ type: 'string',
805
+ description: 'Get specific deployment by ID',
806
+ },
807
+ environment: {
808
+ type: 'string',
809
+ enum: ['production', 'preview', 'staging', 'all'],
810
+ description: 'Filter by environment (default: all)',
811
+ },
812
+ limit: {
813
+ type: 'number',
814
+ description: 'Maximum deployments to return (default: 10)',
815
+ },
816
+ include_testing_priority: {
817
+ type: 'boolean',
818
+ description: 'Include testing priority recommendations (default: true)',
819
+ },
820
+ },
821
+ },
822
+ },
823
+ {
824
+ name: 'get_tester_recommendations',
825
+ description: 'Get recommended testers for a test case based on their expertise, track strengths, and past performance. Use this for smart test assignment.',
826
+ inputSchema: {
827
+ type: 'object' as const,
828
+ properties: {
829
+ test_case_id: {
830
+ type: 'string',
831
+ description: 'Test case UUID to get recommendations for (required)',
832
+ },
833
+ limit: {
834
+ type: 'number',
835
+ description: 'Maximum recommendations to return (default: 5)',
836
+ },
837
+ },
838
+ required: ['test_case_id'],
839
+ },
840
+ },
841
+ {
842
+ name: 'analyze_commit_for_testing',
843
+ description: 'Analyze a commit to suggest what should be tested. Maps changed files to routes/features and checks bug history to provide prioritized testing recommendations.',
844
+ inputSchema: {
845
+ type: 'object' as const,
846
+ properties: {
847
+ commit_sha: {
848
+ type: 'string',
849
+ description: 'Git commit SHA to analyze',
850
+ },
851
+ files_changed: {
852
+ type: 'array',
853
+ items: { type: 'string' },
854
+ description: 'List of files changed in the commit',
855
+ },
856
+ commit_message: {
857
+ type: 'string',
858
+ description: 'Commit message for context',
859
+ },
860
+ record_deployment: {
861
+ type: 'boolean',
862
+ description: 'Record this as a deployment for tracking (default: false)',
863
+ },
864
+ },
865
+ required: ['files_changed'],
866
+ },
867
+ },
868
+ {
869
+ name: 'get_testing_patterns',
870
+ description: 'Get common testing patterns from the curated pattern library. Patterns are based on public knowledge (OWASP, WCAG, framework docs) NOT customer data. Use this to suggest tests for specific feature types.',
871
+ inputSchema: {
872
+ type: 'object' as const,
873
+ properties: {
874
+ feature_type: {
875
+ type: 'string',
876
+ enum: ['form', 'auth', 'payment', 'file_upload', 'search', 'navigation', 'settings', 'dashboard', 'checkout', 'media', 'async', 'error', 'rendering'],
877
+ description: 'Type of feature to get patterns for',
878
+ },
879
+ framework: {
880
+ type: 'string',
881
+ description: 'Framework to filter patterns (react, nextjs, vue, etc.)',
882
+ },
883
+ tracks: {
884
+ type: 'array',
885
+ items: { type: 'string' },
886
+ description: 'QA tracks to focus on (functional, security, accessibility, performance)',
887
+ },
888
+ search: {
889
+ type: 'string',
890
+ description: 'Search patterns by keyword instead of category',
891
+ },
892
+ },
893
+ },
894
+ },
742
895
  ];
743
896
 
744
897
  // Tool handlers
@@ -750,7 +903,7 @@ async function listReports(args: {
750
903
  }) {
751
904
  let query = supabase
752
905
  .from('reports')
753
- .select('id, report_type, severity, status, description, app_context, created_at, tester:testers(name, email)')
906
+ .select('id, report_type, severity, status, description, app_context, created_at, reporter_name, reporter_email, tester:testers(name, email)')
754
907
  .eq('project_id', PROJECT_ID)
755
908
  .order('created_at', { ascending: false })
756
909
  .limit(Math.min(args.limit || 10, 50));
@@ -773,7 +926,7 @@ async function listReports(args: {
773
926
  status: r.status,
774
927
  description: r.description,
775
928
  route: (r.app_context as any)?.currentRoute,
776
- reporter: (r.tester as any)?.name || 'Anonymous',
929
+ reporter: (r.tester as any)?.name || (r as any).reporter_name || 'Anonymous',
777
930
  created_at: r.created_at,
778
931
  })),
779
932
  total: data?.length || 0,
@@ -812,7 +965,10 @@ async function getReport(args: { report_id: string }) {
812
965
  reporter: data.tester ? {
813
966
  name: data.tester.name,
814
967
  email: data.tester.email,
815
- } : null,
968
+ } : (data.reporter_name ? {
969
+ name: data.reporter_name,
970
+ email: data.reporter_email,
971
+ } : null),
816
972
  track: data.track ? {
817
973
  name: data.track.name,
818
974
  icon: data.track.icon,
@@ -868,15 +1024,15 @@ async function searchReports(args: { query?: string; route?: string }) {
868
1024
  async function updateReportStatus(args: {
869
1025
  report_id: string;
870
1026
  status: string;
871
- resolution?: string;
1027
+ resolution_notes?: string;
872
1028
  }) {
873
1029
  if (!isValidUUID(args.report_id)) {
874
1030
  return { error: 'Invalid report_id format' };
875
1031
  }
876
1032
 
877
1033
  const updates: Record<string, unknown> = { status: args.status };
878
- if (args.resolution) {
879
- updates.resolution = args.resolution;
1034
+ if (args.resolution_notes) {
1035
+ updates.resolution_notes = args.resolution_notes;
880
1036
  }
881
1037
 
882
1038
  const { error } = await supabase
@@ -2409,6 +2565,429 @@ async function getQAHealth(args: {
2409
2565
  };
2410
2566
  }
2411
2567
 
2568
+ // === SPRINT 5: NEW HANDLER FUNCTIONS ===
2569
+
2570
+ async function getQASessions(args: {
2571
+ status?: 'active' | 'completed' | 'all';
2572
+ tester_id?: string;
2573
+ limit?: number;
2574
+ include_findings?: boolean;
2575
+ }) {
2576
+ const status = args.status || 'all';
2577
+ const limit = Math.min(args.limit || 20, 50);
2578
+ const includeFindings = args.include_findings !== false;
2579
+
2580
+ let query = supabase
2581
+ .from('qa_sessions')
2582
+ .select(`
2583
+ id, focus_area, track, platform, started_at, ended_at,
2584
+ notes, routes_covered, status, duration_minutes,
2585
+ findings_count, bugs_filed, created_at,
2586
+ tester:testers(id, name, email)
2587
+ `)
2588
+ .eq('project_id', PROJECT_ID)
2589
+ .order('started_at', { ascending: false })
2590
+ .limit(limit);
2591
+
2592
+ if (status !== 'all') {
2593
+ query = query.eq('status', status);
2594
+ }
2595
+
2596
+ if (args.tester_id && isValidUUID(args.tester_id)) {
2597
+ query = query.eq('tester_id', args.tester_id);
2598
+ }
2599
+
2600
+ const { data: sessions, error } = await query;
2601
+
2602
+ if (error) {
2603
+ return { error: error.message };
2604
+ }
2605
+
2606
+ // Optionally load findings for each session
2607
+ let sessionsWithFindings = sessions || [];
2608
+ if (includeFindings && sessions && sessions.length > 0) {
2609
+ const sessionIds = sessions.map(s => s.id);
2610
+ const { data: findings } = await supabase
2611
+ .from('qa_findings')
2612
+ .select('id, session_id, type, severity, title, description, route, converted_to_bug_id, dismissed')
2613
+ .in('session_id', sessionIds);
2614
+
2615
+ const findingsList = findings || [];
2616
+ const findingsBySession = findingsList.reduce((acc, f) => {
2617
+ acc[f.session_id] = acc[f.session_id] || [];
2618
+ acc[f.session_id].push(f);
2619
+ return acc;
2620
+ }, {} as Record<string, typeof findingsList>);
2621
+
2622
+ sessionsWithFindings = sessions.map(s => ({
2623
+ ...s,
2624
+ findings: findingsBySession[s.id] || [],
2625
+ }));
2626
+ }
2627
+
2628
+ const summary = {
2629
+ total: sessionsWithFindings.length,
2630
+ active: sessionsWithFindings.filter(s => s.status === 'active').length,
2631
+ completed: sessionsWithFindings.filter(s => s.status === 'completed').length,
2632
+ totalFindings: sessionsWithFindings.reduce((sum, s) => sum + (s.findings_count || 0), 0),
2633
+ totalBugsFiled: sessionsWithFindings.reduce((sum, s) => sum + (s.bugs_filed || 0), 0),
2634
+ };
2635
+
2636
+ return {
2637
+ sessions: sessionsWithFindings,
2638
+ summary,
2639
+ };
2640
+ }
2641
+
2642
+ async function getQAAlerts(args: {
2643
+ severity?: 'critical' | 'warning' | 'info' | 'all';
2644
+ type?: 'hot_spot' | 'coverage_gap' | 'track_gap' | 'regression' | 'all';
2645
+ status?: 'active' | 'acknowledged' | 'all';
2646
+ refresh?: boolean;
2647
+ }) {
2648
+ const severity = args.severity || 'all';
2649
+ const type = args.type || 'all';
2650
+ const status = args.status || 'active';
2651
+
2652
+ // Optionally refresh alerts
2653
+ if (args.refresh) {
2654
+ await supabase.rpc('detect_all_alerts', { p_project_id: PROJECT_ID });
2655
+ }
2656
+
2657
+ let query = supabase
2658
+ .from('qa_alerts')
2659
+ .select('*')
2660
+ .eq('project_id', PROJECT_ID)
2661
+ .order('severity', { ascending: true }) // critical first
2662
+ .order('created_at', { ascending: false });
2663
+
2664
+ if (severity !== 'all') {
2665
+ query = query.eq('severity', severity);
2666
+ }
2667
+ if (type !== 'all') {
2668
+ query = query.eq('type', type);
2669
+ }
2670
+ if (status !== 'all') {
2671
+ query = query.eq('status', status);
2672
+ }
2673
+
2674
+ const { data: alerts, error } = await query;
2675
+
2676
+ if (error) {
2677
+ return { error: error.message };
2678
+ }
2679
+
2680
+ const summary = {
2681
+ total: alerts?.length || 0,
2682
+ critical: alerts?.filter(a => a.severity === 'critical').length || 0,
2683
+ warning: alerts?.filter(a => a.severity === 'warning').length || 0,
2684
+ info: alerts?.filter(a => a.severity === 'info').length || 0,
2685
+ byType: {
2686
+ hot_spot: alerts?.filter(a => a.type === 'hot_spot').length || 0,
2687
+ coverage_gap: alerts?.filter(a => a.type === 'coverage_gap').length || 0,
2688
+ track_gap: alerts?.filter(a => a.type === 'track_gap').length || 0,
2689
+ },
2690
+ };
2691
+
2692
+ return {
2693
+ alerts: alerts?.map(a => ({
2694
+ id: a.id,
2695
+ type: a.type,
2696
+ severity: a.severity,
2697
+ title: a.title,
2698
+ description: a.description,
2699
+ route: a.trigger_route,
2700
+ track: a.trigger_track,
2701
+ recommendation: a.recommendation,
2702
+ action_type: a.action_type,
2703
+ status: a.status,
2704
+ created_at: a.created_at,
2705
+ })),
2706
+ summary,
2707
+ };
2708
+ }
2709
+
2710
+ async function getDeploymentAnalysis(args: {
2711
+ deployment_id?: string;
2712
+ environment?: 'production' | 'preview' | 'staging' | 'all';
2713
+ limit?: number;
2714
+ include_testing_priority?: boolean;
2715
+ }) {
2716
+ const limit = Math.min(args.limit || 10, 50);
2717
+ const includeTestingPriority = args.include_testing_priority !== false;
2718
+
2719
+ if (args.deployment_id && isValidUUID(args.deployment_id)) {
2720
+ // Get specific deployment
2721
+ const { data: deployment, error } = await supabase
2722
+ .from('deployments')
2723
+ .select('*')
2724
+ .eq('id', args.deployment_id)
2725
+ .eq('project_id', PROJECT_ID)
2726
+ .single();
2727
+
2728
+ if (error) {
2729
+ return { error: error.message };
2730
+ }
2731
+
2732
+ return { deployment };
2733
+ }
2734
+
2735
+ // Get deployment history
2736
+ let query = supabase
2737
+ .from('deployments')
2738
+ .select('*')
2739
+ .eq('project_id', PROJECT_ID)
2740
+ .order('deployed_at', { ascending: false })
2741
+ .limit(limit);
2742
+
2743
+ if (args.environment && args.environment !== 'all') {
2744
+ query = query.eq('environment', args.environment);
2745
+ }
2746
+
2747
+ const { data: deployments, error } = await query;
2748
+
2749
+ if (error) {
2750
+ return { error: error.message };
2751
+ }
2752
+
2753
+ const summary = {
2754
+ total: deployments?.length || 0,
2755
+ avgRiskScore: deployments?.length
2756
+ ? Math.round(deployments.reduce((sum, d) => sum + (d.risk_score || 0), 0) / deployments.length)
2757
+ : 0,
2758
+ highRisk: deployments?.filter(d => (d.risk_score || 0) >= 70).length || 0,
2759
+ verified: deployments?.filter(d => d.verified_at).length || 0,
2760
+ };
2761
+
2762
+ return {
2763
+ deployments: deployments?.map(d => ({
2764
+ id: d.id,
2765
+ environment: d.environment,
2766
+ commit_sha: d.commit_sha,
2767
+ commit_message: d.commit_message,
2768
+ branch: d.branch,
2769
+ deployed_at: d.deployed_at,
2770
+ risk_score: d.risk_score,
2771
+ routes_affected: d.routes_affected,
2772
+ testing_priority: includeTestingPriority ? d.testing_priority : undefined,
2773
+ verified: !!d.verified_at,
2774
+ })),
2775
+ summary,
2776
+ };
2777
+ }
2778
+
2779
+ async function getTesterRecommendations(args: {
2780
+ test_case_id: string;
2781
+ limit?: number;
2782
+ }) {
2783
+ if (!isValidUUID(args.test_case_id)) {
2784
+ return { error: 'Invalid test_case_id format' };
2785
+ }
2786
+
2787
+ const limit = Math.min(args.limit || 5, 10);
2788
+
2789
+ const { data: recommendations, error } = await supabase.rpc('get_tester_recommendations', {
2790
+ p_test_case_id: args.test_case_id,
2791
+ p_limit: limit,
2792
+ });
2793
+
2794
+ if (error) {
2795
+ return { error: error.message };
2796
+ }
2797
+
2798
+ // Get test case info for context
2799
+ const { data: testCase } = await supabase
2800
+ .from('test_cases')
2801
+ .select('test_key, title, track:qa_tracks(name)')
2802
+ .eq('id', args.test_case_id)
2803
+ .single();
2804
+
2805
+ return {
2806
+ test_case: testCase ? {
2807
+ id: args.test_case_id,
2808
+ test_key: testCase.test_key,
2809
+ title: testCase.title,
2810
+ track: (testCase.track as any)?.name,
2811
+ } : null,
2812
+ recommendations: recommendations?.map((r: any) => ({
2813
+ tester_id: r.tester_id,
2814
+ name: r.tester_name,
2815
+ email: r.tester_email,
2816
+ match_score: r.match_score,
2817
+ reasons: r.match_reasons,
2818
+ })) || [],
2819
+ };
2820
+ }
2821
+
2822
+ async function analyzeCommitForTesting(args: {
2823
+ commit_sha?: string;
2824
+ files_changed: string[];
2825
+ commit_message?: string;
2826
+ record_deployment?: boolean;
2827
+ }) {
2828
+ const filesChanged = args.files_changed || [];
2829
+
2830
+ // Map files to routes using file_route_mapping
2831
+ const { data: mappings } = await supabase
2832
+ .from('file_route_mapping')
2833
+ .select('file_pattern, route, feature, confidence')
2834
+ .eq('project_id', PROJECT_ID);
2835
+
2836
+ const affectedRoutes: Array<{ route: string; feature?: string; confidence: number; matched_files: string[] }> = [];
2837
+
2838
+ for (const mapping of mappings || []) {
2839
+ const matchedFiles = filesChanged.filter(file => {
2840
+ const pattern = mapping.file_pattern
2841
+ .replace(/\*\*/g, '.*')
2842
+ .replace(/\*/g, '[^/]*');
2843
+ return new RegExp(pattern).test(file);
2844
+ });
2845
+
2846
+ if (matchedFiles.length > 0) {
2847
+ affectedRoutes.push({
2848
+ route: mapping.route,
2849
+ feature: mapping.feature || undefined,
2850
+ confidence: mapping.confidence,
2851
+ matched_files: matchedFiles,
2852
+ });
2853
+ }
2854
+ }
2855
+
2856
+ // Get bug history for affected routes
2857
+ const routes = affectedRoutes.map(r => r.route);
2858
+ let bugHistory: any[] = [];
2859
+
2860
+ if (routes.length > 0) {
2861
+ const { data: bugs } = await supabase
2862
+ .from('reports')
2863
+ .select('id, severity, description, route, created_at')
2864
+ .eq('project_id', PROJECT_ID)
2865
+ .eq('report_type', 'bug')
2866
+ .in('route', routes)
2867
+ .gte('created_at', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString());
2868
+
2869
+ bugHistory = bugs || [];
2870
+ }
2871
+
2872
+ // Calculate risk score
2873
+ let riskScore = 0;
2874
+ riskScore += Math.min(filesChanged.length * 2, 20); // File count
2875
+ riskScore += Math.min(affectedRoutes.length * 5, 30); // Route count
2876
+ riskScore += Math.min(bugHistory.length * 10, 50); // Bug history
2877
+
2878
+ // Generate testing recommendations
2879
+ const recommendations = affectedRoutes.map(r => {
2880
+ const routeBugs = bugHistory.filter(b => b.route === r.route);
2881
+ const priority = routeBugs.length >= 3 ? 'critical' :
2882
+ routeBugs.length >= 1 ? 'high' : 'medium';
2883
+
2884
+ return {
2885
+ route: r.route,
2886
+ feature: r.feature,
2887
+ priority,
2888
+ reason: routeBugs.length > 0
2889
+ ? `${routeBugs.length} bug(s) in last 30 days`
2890
+ : 'Code changed in this area',
2891
+ recent_bugs: routeBugs.length,
2892
+ };
2893
+ }).sort((a, b) => {
2894
+ const order = { critical: 0, high: 1, medium: 2 };
2895
+ return order[a.priority as keyof typeof order] - order[b.priority as keyof typeof order];
2896
+ });
2897
+
2898
+ // Optionally record as deployment
2899
+ if (args.record_deployment) {
2900
+ await supabase.rpc('record_deployment', {
2901
+ p_project_id: PROJECT_ID,
2902
+ p_environment: 'production',
2903
+ p_commit_sha: args.commit_sha || null,
2904
+ p_commit_message: args.commit_message || null,
2905
+ p_files_changed: filesChanged,
2906
+ p_webhook_source: 'mcp',
2907
+ });
2908
+ }
2909
+
2910
+ return {
2911
+ commit_sha: args.commit_sha,
2912
+ files_analyzed: filesChanged.length,
2913
+ risk_score: Math.min(riskScore, 100),
2914
+ affected_routes: affectedRoutes,
2915
+ bug_history_summary: {
2916
+ total: bugHistory.length,
2917
+ critical: bugHistory.filter(b => b.severity === 'critical').length,
2918
+ high: bugHistory.filter(b => b.severity === 'high').length,
2919
+ },
2920
+ testing_recommendations: recommendations,
2921
+ deployment_recorded: args.record_deployment || false,
2922
+ };
2923
+ }
2924
+
2925
+ async function getTestingPatterns(args: {
2926
+ feature_type?: string;
2927
+ framework?: string;
2928
+ tracks?: string[];
2929
+ search?: string;
2930
+ }) {
2931
+ let patterns: any[] = [];
2932
+
2933
+ if (args.search) {
2934
+ // Search patterns by keyword
2935
+ const { data, error } = await supabase.rpc('search_patterns', {
2936
+ p_query: args.search,
2937
+ p_limit: 20,
2938
+ });
2939
+ if (!error) patterns = data || [];
2940
+ } else if (args.feature_type) {
2941
+ // Get patterns for feature type
2942
+ const { data, error } = await supabase.rpc('get_patterns_for_feature', {
2943
+ p_category: args.feature_type,
2944
+ p_framework: args.framework || null,
2945
+ p_tracks: args.tracks || null,
2946
+ });
2947
+ if (!error) patterns = data || [];
2948
+ } else {
2949
+ // Get all patterns (limited)
2950
+ const { data, error } = await supabase
2951
+ .from('qa_patterns')
2952
+ .select('*')
2953
+ .eq('is_active', true)
2954
+ .order('severity')
2955
+ .limit(30);
2956
+ if (!error) patterns = data || [];
2957
+ }
2958
+
2959
+ const summary = {
2960
+ total: patterns.length,
2961
+ by_severity: {
2962
+ critical: patterns.filter(p => p.severity === 'critical').length,
2963
+ high: patterns.filter(p => p.severity === 'high').length,
2964
+ medium: patterns.filter(p => p.severity === 'medium').length,
2965
+ low: patterns.filter(p => p.severity === 'low').length,
2966
+ },
2967
+ by_track: patterns.reduce((acc, p) => {
2968
+ acc[p.track] = (acc[p.track] || 0) + 1;
2969
+ return acc;
2970
+ }, {} as Record<string, number>),
2971
+ };
2972
+
2973
+ return {
2974
+ patterns: patterns.map(p => ({
2975
+ title: p.title,
2976
+ category: p.category,
2977
+ track: p.track,
2978
+ severity: p.severity,
2979
+ description: p.description,
2980
+ why_it_happens: p.why_it_happens,
2981
+ suggested_tests: p.suggested_tests,
2982
+ common_fix: p.common_fix,
2983
+ source: p.source,
2984
+ frameworks: p.frameworks,
2985
+ })),
2986
+ summary,
2987
+ note: 'Patterns are from public knowledge (OWASP, WCAG, framework docs), not customer data.',
2988
+ };
2989
+ }
2990
+
2412
2991
  async function analyzeChangesForTests(args: {
2413
2992
  changed_files: string[];
2414
2993
  change_type: 'feature' | 'bugfix' | 'refactor' | 'ui_change' | 'api_change' | 'config';
@@ -2837,7 +3416,28 @@ async function createBugReport(args: {
2837
3416
  codeContext.suggested_fix = args.suggested_fix;
2838
3417
  }
2839
3418
 
2840
- const report = {
3419
+ // Find a reporter_id: try to get the project owner or first tester
3420
+ let reporterId: string | null = null;
3421
+ const { data: project } = await supabase
3422
+ .from('projects')
3423
+ .select('owner_id')
3424
+ .eq('id', PROJECT_ID)
3425
+ .single();
3426
+ if (project?.owner_id) {
3427
+ reporterId = project.owner_id;
3428
+ } else {
3429
+ // Fallback: use the first tester for this project
3430
+ const { data: testers } = await supabase
3431
+ .from('testers')
3432
+ .select('id')
3433
+ .eq('project_id', PROJECT_ID)
3434
+ .limit(1);
3435
+ if (testers && testers.length > 0) {
3436
+ reporterId = testers[0].id;
3437
+ }
3438
+ }
3439
+
3440
+ const report: Record<string, unknown> = {
2841
3441
  project_id: PROJECT_ID,
2842
3442
  report_type: 'bug',
2843
3443
  title: args.title,
@@ -2856,6 +3456,10 @@ async function createBugReport(args: {
2856
3456
  code_context: codeContext,
2857
3457
  };
2858
3458
 
3459
+ if (reporterId) {
3460
+ report.reporter_id = reporterId;
3461
+ }
3462
+
2859
3463
  const { data, error } = await supabase
2860
3464
  .from('reports')
2861
3465
  .insert(report)
@@ -3003,7 +3607,7 @@ async function markFixedWithCommit(args: {
3003
3607
  const updates = {
3004
3608
  status: 'resolved',
3005
3609
  resolved_at: new Date().toISOString(),
3006
- resolution: args.resolution_notes || `Fixed in commit ${args.commit_sha.slice(0, 7)}`,
3610
+ resolution_notes: args.resolution_notes || `Fixed in commit ${args.commit_sha.slice(0, 7)}`,
3007
3611
  code_context: {
3008
3612
  ...existingContext,
3009
3613
  fix: {
@@ -3285,7 +3889,7 @@ async function createRegressionTest(args: {
3285
3889
  {
3286
3890
  stepNumber: 3,
3287
3891
  action: 'Verify the fix is working',
3288
- expectedResult: report.resolution || 'Feature works as expected',
3892
+ expectedResult: report.resolution_notes || 'Feature works as expected',
3289
3893
  },
3290
3894
  ],
3291
3895
  expected_result: `The bug "${report.title}" should not recur`,
@@ -3986,6 +4590,25 @@ async function main() {
3986
4590
  case 'complete_fix_request':
3987
4591
  result = await completeFixRequest(args as any);
3988
4592
  break;
4593
+ // === SPRINT 5: NEW MCP TOOLS ===
4594
+ case 'get_qa_sessions':
4595
+ result = await getQASessions(args as any);
4596
+ break;
4597
+ case 'get_qa_alerts':
4598
+ result = await getQAAlerts(args as any);
4599
+ break;
4600
+ case 'get_deployment_analysis':
4601
+ result = await getDeploymentAnalysis(args as any);
4602
+ break;
4603
+ case 'get_tester_recommendations':
4604
+ result = await getTesterRecommendations(args as any);
4605
+ break;
4606
+ case 'analyze_commit_for_testing':
4607
+ result = await analyzeCommitForTesting(args as any);
4608
+ break;
4609
+ case 'get_testing_patterns':
4610
+ result = await getTestingPatterns(args as any);
4611
+ break;
3989
4612
  default:
3990
4613
  return {
3991
4614
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],