@bbearai/admin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,2437 @@
1
+ // src/index.ts
2
+ import { createClient } from "@supabase/supabase-js";
3
+
4
+ // src/utils.ts
5
+ var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6
+ function isValidUUID(str) {
7
+ return typeof str === "string" && UUID_REGEX.test(str);
8
+ }
9
+ function sanitizeSearchQuery(query) {
10
+ if (!query) return void 0;
11
+ return query.replace(/[%_\\]/g, "").slice(0, 500);
12
+ }
13
+ function extractFunctionName(codeSnippet) {
14
+ if (!codeSnippet) return void 0;
15
+ const patterns = [
16
+ /function\s+(\w+)/,
17
+ /const\s+(\w+)\s*=\s*(?:async\s*)?\(/,
18
+ /export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)/,
19
+ /class\s+(\w+)/,
20
+ /(\w+)\s*:\s*(?:React\.)?FC/
21
+ ];
22
+ for (const pattern of patterns) {
23
+ const match = codeSnippet.match(pattern);
24
+ if (match) return match[1];
25
+ }
26
+ return void 0;
27
+ }
28
+ function analyzeFileTypes(files) {
29
+ const analysis = {
30
+ hasUIComponents: false,
31
+ hasAPIRoutes: false,
32
+ hasTests: false,
33
+ hasStyles: false,
34
+ hasConfig: false,
35
+ primaryType: "unknown"
36
+ };
37
+ for (const file of files) {
38
+ const lower = file.toLowerCase();
39
+ if (lower.includes("component") || lower.endsWith(".tsx") || lower.endsWith(".jsx") || lower.includes("/pages/") || lower.includes("/app/") || lower.includes("/screens/")) {
40
+ analysis.hasUIComponents = true;
41
+ }
42
+ if (lower.includes("/api/") || lower.includes("route.ts") || lower.includes("handler") || lower.includes("service") || lower.includes("controller")) {
43
+ analysis.hasAPIRoutes = true;
44
+ }
45
+ if (lower.includes(".test.") || lower.includes(".spec.") || lower.includes("__tests__")) {
46
+ analysis.hasTests = true;
47
+ }
48
+ if (lower.endsWith(".css") || lower.endsWith(".scss") || lower.includes("style") || lower.includes("tailwind")) {
49
+ analysis.hasStyles = true;
50
+ }
51
+ if (lower.includes("config") || lower.endsWith(".json") || lower.endsWith(".env") || lower.includes("settings")) {
52
+ analysis.hasConfig = true;
53
+ }
54
+ }
55
+ if (analysis.hasUIComponents) analysis.primaryType = "ui";
56
+ else if (analysis.hasAPIRoutes) analysis.primaryType = "api";
57
+ else if (analysis.hasConfig) analysis.primaryType = "config";
58
+ else if (analysis.hasStyles) analysis.primaryType = "styles";
59
+ return analysis;
60
+ }
61
+ function getTrackTemplates(track) {
62
+ const templates = {
63
+ functional: [
64
+ {
65
+ title: "Happy path navigation to {route}",
66
+ description: "Verify basic navigation and page load for {route}",
67
+ steps: [
68
+ { action: "Navigate to {route}", expectedResult: "Page loads without errors" },
69
+ { action: "Verify all main elements are visible", expectedResult: "Headers, content, and navigation present" },
70
+ { action: "Check for console errors", expectedResult: "No JavaScript errors in console" }
71
+ ],
72
+ expected_result: "Page functions correctly with all elements visible"
73
+ },
74
+ {
75
+ title: "Error handling at {route}",
76
+ description: "Test error states and edge cases at {route}",
77
+ steps: [
78
+ { action: "Navigate to {route}", expectedResult: "Page loads" },
79
+ { action: "Trigger an error condition (e.g., invalid input)", expectedResult: "Error message displayed" },
80
+ { action: "Verify error is user-friendly", expectedResult: "Clear explanation of what went wrong" }
81
+ ],
82
+ expected_result: "Errors are handled gracefully with clear messaging"
83
+ },
84
+ {
85
+ title: "Form submission at {route}",
86
+ description: "Test form validation and submission at {route}",
87
+ steps: [
88
+ { action: "Navigate to {route}", expectedResult: "Form is visible" },
89
+ { action: "Submit form with empty fields", expectedResult: "Validation errors shown" },
90
+ { action: "Fill in valid data and submit", expectedResult: "Success message or redirect" }
91
+ ],
92
+ expected_result: "Form validates input and submits successfully"
93
+ }
94
+ ],
95
+ design: [
96
+ {
97
+ title: "Visual consistency at {route}",
98
+ description: "Check design system compliance at {route}",
99
+ steps: [
100
+ { action: "Navigate to {route}", expectedResult: "Page loads" },
101
+ { action: "Check typography matches design system", expectedResult: "Fonts, sizes, weights are correct" },
102
+ { action: "Verify color usage", expectedResult: "Colors match brand guidelines" },
103
+ { action: "Check spacing and alignment", expectedResult: "Consistent margins and padding" }
104
+ ],
105
+ expected_result: "Page matches design specifications"
106
+ },
107
+ {
108
+ title: "Responsive behavior at {route}",
109
+ description: "Test responsive design at {route}",
110
+ steps: [
111
+ { action: "View {route} on desktop (1920px)", expectedResult: "Full layout visible" },
112
+ { action: "Resize to tablet (768px)", expectedResult: "Layout adapts appropriately" },
113
+ { action: "Resize to mobile (375px)", expectedResult: "Mobile-friendly layout" }
114
+ ],
115
+ expected_result: "Page is fully responsive across breakpoints"
116
+ }
117
+ ],
118
+ accessibility: [
119
+ {
120
+ title: "Keyboard navigation at {route}",
121
+ description: "Test keyboard accessibility at {route}",
122
+ steps: [
123
+ { action: "Navigate to {route}", expectedResult: "Page loads" },
124
+ { action: "Press Tab to navigate through elements", expectedResult: "Focus moves in logical order" },
125
+ { action: "Verify focus indicators are visible", expectedResult: "Clear focus ring on interactive elements" },
126
+ { action: "Test Enter/Space on buttons", expectedResult: "Buttons activate correctly" }
127
+ ],
128
+ expected_result: "Page is fully navigable by keyboard"
129
+ },
130
+ {
131
+ title: "Screen reader compatibility at {route}",
132
+ description: "Test screen reader accessibility at {route}",
133
+ steps: [
134
+ { action: "Enable screen reader", expectedResult: "Screen reader activates" },
135
+ { action: "Navigate to {route}", expectedResult: "Page title is announced" },
136
+ { action: "Tab through content", expectedResult: "All content is announced meaningfully" },
137
+ { action: "Check form labels", expectedResult: "Inputs have associated labels" }
138
+ ],
139
+ expected_result: "Page is fully accessible via screen reader"
140
+ }
141
+ ],
142
+ performance: [
143
+ {
144
+ title: "Page load performance at {route}",
145
+ description: "Measure load times at {route}",
146
+ steps: [
147
+ { action: "Clear browser cache", expectedResult: "Cache cleared" },
148
+ { action: "Navigate to {route}", expectedResult: "Page loads" },
149
+ { action: "Check Network tab for load time", expectedResult: "Initial load under 3 seconds" },
150
+ { action: "Note Largest Contentful Paint", expectedResult: "LCP under 2.5 seconds" }
151
+ ],
152
+ expected_result: "Page loads within performance budget"
153
+ }
154
+ ]
155
+ };
156
+ return templates[track] || templates.functional;
157
+ }
158
+
159
+ // src/reports.ts
160
+ async function listReports(supabase, projectId, args) {
161
+ let query = supabase.from("reports").select("id, report_type, severity, status, description, app_context, created_at, reporter_name, tester:testers(name)").eq("project_id", projectId).order("created_at", { ascending: false }).limit(Math.min(args.limit || 10, 50));
162
+ if (args.status) query = query.eq("status", args.status);
163
+ if (args.severity) query = query.eq("severity", args.severity);
164
+ if (args.type) query = query.eq("report_type", args.type);
165
+ const { data, error } = await query;
166
+ if (error) return { error: error.message };
167
+ return {
168
+ reports: data?.map((r) => ({
169
+ id: r.id,
170
+ type: r.report_type,
171
+ severity: r.severity,
172
+ status: r.status,
173
+ description: r.description,
174
+ route: r.app_context?.currentRoute,
175
+ reporter: r.tester?.name || r.reporter_name || "Anonymous",
176
+ created_at: r.created_at
177
+ })),
178
+ total: data?.length || 0
179
+ };
180
+ }
181
+ async function getReport(supabase, projectId, args) {
182
+ if (!isValidUUID(args.report_id)) {
183
+ return { error: "Invalid report_id format" };
184
+ }
185
+ const { data, error } = await supabase.from("reports").select("*, tester:testers(*), track:qa_tracks(*)").eq("id", args.report_id).eq("project_id", projectId).single();
186
+ if (error) return { error: error.message };
187
+ return {
188
+ report: {
189
+ id: data.id,
190
+ type: data.report_type,
191
+ severity: data.severity,
192
+ status: data.status,
193
+ description: data.description,
194
+ app_context: data.app_context,
195
+ device_info: data.device_info,
196
+ navigation_history: data.navigation_history,
197
+ screenshots: data.screenshots,
198
+ created_at: data.created_at,
199
+ reporter: data.tester ? { name: data.tester.name } : data.reporter_name ? { name: data.reporter_name } : null,
200
+ track: data.track ? { name: data.track.name, icon: data.track.icon } : null
201
+ }
202
+ };
203
+ }
204
+ async function searchReports(supabase, projectId, args) {
205
+ const sanitizedQuery = sanitizeSearchQuery(args.query);
206
+ const sanitizedRoute = sanitizeSearchQuery(args.route);
207
+ let query = supabase.from("reports").select("id, report_type, severity, status, description, app_context, created_at").eq("project_id", projectId).order("created_at", { ascending: false }).limit(20);
208
+ if (sanitizedQuery) {
209
+ query = query.ilike("description", `%${sanitizedQuery}%`);
210
+ }
211
+ const { data, error } = await query;
212
+ if (error) return { error: error.message };
213
+ let results = data || [];
214
+ if (sanitizedRoute) {
215
+ results = results.filter((r) => {
216
+ const route = r.app_context?.currentRoute;
217
+ return route && route.includes(sanitizedRoute);
218
+ });
219
+ }
220
+ return {
221
+ reports: results.map((r) => ({
222
+ id: r.id,
223
+ type: r.report_type,
224
+ severity: r.severity,
225
+ status: r.status,
226
+ description: r.description,
227
+ route: r.app_context?.currentRoute,
228
+ created_at: r.created_at
229
+ })),
230
+ total: results.length
231
+ };
232
+ }
233
+ async function updateReportStatus(supabase, projectId, args) {
234
+ if (!isValidUUID(args.report_id)) {
235
+ return { error: "Invalid report_id format" };
236
+ }
237
+ const updates = { status: args.status };
238
+ if (args.resolution_notes) updates.resolution_notes = args.resolution_notes;
239
+ const { error } = await supabase.from("reports").update(updates).eq("id", args.report_id).eq("project_id", projectId);
240
+ if (error) return { error: error.message };
241
+ return { success: true, message: `Report status updated to ${args.status}` };
242
+ }
243
+ async function getReportContext(supabase, projectId, args) {
244
+ if (!isValidUUID(args.report_id)) {
245
+ return { error: "Invalid report_id format" };
246
+ }
247
+ const { data, error } = await supabase.from("reports").select("app_context, device_info, navigation_history, enhanced_context").eq("id", args.report_id).eq("project_id", projectId).single();
248
+ if (error) return { error: error.message };
249
+ return {
250
+ context: {
251
+ app_context: data.app_context,
252
+ device_info: data.device_info,
253
+ navigation_history: data.navigation_history,
254
+ enhanced_context: data.enhanced_context || {}
255
+ }
256
+ };
257
+ }
258
+ async function bulkUpdateReports(supabase, projectId, args) {
259
+ if (!args.report_ids || args.report_ids.length === 0) {
260
+ return { error: "At least one report_id is required" };
261
+ }
262
+ if (args.report_ids.length > 50) {
263
+ return { error: "Maximum 50 reports per bulk update" };
264
+ }
265
+ for (const id of args.report_ids) {
266
+ if (!isValidUUID(id)) return { error: `Invalid report_id format: ${id}` };
267
+ }
268
+ const updates = { status: args.status };
269
+ if (args.resolution_notes) updates.resolution_notes = args.resolution_notes;
270
+ if (["fixed", "resolved", "verified", "wont_fix", "duplicate", "closed"].includes(args.status)) {
271
+ updates.resolved_at = (/* @__PURE__ */ new Date()).toISOString();
272
+ }
273
+ const { data, error } = await supabase.from("reports").update(updates).eq("project_id", projectId).in("id", args.report_ids).select("id, status, description");
274
+ if (error) return { error: error.message };
275
+ const updated = data || [];
276
+ const updatedIds = new Set(updated.map((r) => r.id));
277
+ const notFound = args.report_ids.filter((id) => !updatedIds.has(id));
278
+ return {
279
+ success: true,
280
+ updatedCount: updated.length,
281
+ requestedCount: args.report_ids.length,
282
+ notFound: notFound.length > 0 ? notFound : void 0,
283
+ status: args.status,
284
+ reports: updated.map((r) => ({
285
+ id: r.id,
286
+ status: r.status,
287
+ description: r.description?.slice(0, 80)
288
+ })),
289
+ message: `Updated ${updated.length} report(s) to "${args.status}".${notFound.length > 0 ? ` ${notFound.length} report(s) not found.` : ""}`
290
+ };
291
+ }
292
+
293
+ // src/project.ts
294
+ async function getProjectInfo(supabase, projectId) {
295
+ const { data: project, error: projectError } = await supabase.from("projects").select("id, name, slug, is_qa_enabled").eq("id", projectId).single();
296
+ if (projectError) return { error: projectError.message };
297
+ const { data: tracks } = await supabase.from("qa_tracks").select("id, name, icon, test_template").eq("project_id", projectId);
298
+ const { count: testCaseCount } = await supabase.from("test_cases").select("id", { count: "exact", head: true }).eq("project_id", projectId);
299
+ const { count: openBugCount } = await supabase.from("reports").select("id", { count: "exact", head: true }).eq("project_id", projectId).eq("report_type", "bug").in("status", ["new", "confirmed", "in_progress"]);
300
+ return {
301
+ project: {
302
+ id: project.id,
303
+ name: project.name,
304
+ slug: project.slug,
305
+ qaEnabled: project.is_qa_enabled
306
+ },
307
+ stats: {
308
+ tracks: tracks?.length || 0,
309
+ testCases: testCaseCount || 0,
310
+ openBugs: openBugCount || 0
311
+ },
312
+ tracks: tracks?.map((t) => ({
313
+ id: t.id,
314
+ name: t.name,
315
+ icon: t.icon,
316
+ template: t.test_template
317
+ })) || []
318
+ };
319
+ }
320
+ async function getQaTracks(supabase, projectId) {
321
+ const { data, error } = await supabase.from("qa_tracks").select("*").eq("project_id", projectId).order("sort_order");
322
+ if (error) return { error: error.message };
323
+ return {
324
+ tracks: data?.map((t) => ({
325
+ id: t.id,
326
+ name: t.name,
327
+ slug: t.slug,
328
+ icon: t.icon,
329
+ color: t.color,
330
+ testTemplate: t.test_template,
331
+ description: t.description,
332
+ requiresCertification: t.requires_certification,
333
+ evaluationCriteria: t.evaluation_criteria
334
+ })) || []
335
+ };
336
+ }
337
+
338
+ // src/tests.ts
339
+ async function createTestCase(supabase, projectId, args) {
340
+ let trackId = null;
341
+ if (args.track) {
342
+ const sanitizedTrack = sanitizeSearchQuery(args.track);
343
+ if (sanitizedTrack) {
344
+ const { data: trackData } = await supabase.from("qa_tracks").select("id").eq("project_id", projectId).ilike("name", `%${sanitizedTrack}%`).single();
345
+ trackId = trackData?.id || null;
346
+ }
347
+ }
348
+ const testCase = {
349
+ project_id: projectId,
350
+ test_key: args.test_key,
351
+ title: args.title,
352
+ description: args.description || "",
353
+ track_id: trackId,
354
+ priority: args.priority || "P2",
355
+ steps: args.steps,
356
+ expected_result: args.expected_result,
357
+ preconditions: args.preconditions || "",
358
+ target_route: args.target_route || null
359
+ };
360
+ const { data, error } = await supabase.from("test_cases").insert(testCase).select("id, test_key, title").single();
361
+ if (error) return { error: error.message };
362
+ return {
363
+ success: true,
364
+ testCase: { id: data.id, testKey: data.test_key, title: data.title },
365
+ message: `Test case ${data.test_key} created successfully`
366
+ };
367
+ }
368
+ async function updateTestCase(supabase, projectId, args) {
369
+ if (!args.test_case_id && !args.test_key) {
370
+ return { error: "Must provide either test_case_id or test_key to identify the test case" };
371
+ }
372
+ let testCaseId = args.test_case_id;
373
+ if (!testCaseId && args.test_key) {
374
+ const { data: existing } = await supabase.from("test_cases").select("id").eq("project_id", projectId).eq("test_key", args.test_key).single();
375
+ if (!existing) return { error: `Test case with key ${args.test_key} not found` };
376
+ testCaseId = existing.id;
377
+ }
378
+ const updates = {};
379
+ if (args.title !== void 0) updates.title = args.title;
380
+ if (args.description !== void 0) updates.description = args.description;
381
+ if (args.priority !== void 0) updates.priority = args.priority;
382
+ if (args.steps !== void 0) updates.steps = args.steps;
383
+ if (args.expected_result !== void 0) updates.expected_result = args.expected_result;
384
+ if (args.preconditions !== void 0) updates.preconditions = args.preconditions;
385
+ if (args.target_route !== void 0) updates.target_route = args.target_route;
386
+ if (Object.keys(updates).length === 0) return { error: "No fields to update" };
387
+ const { data, error } = await supabase.from("test_cases").update(updates).eq("id", testCaseId).eq("project_id", projectId).select("id, test_key, title, target_route").single();
388
+ if (error) return { error: error.message };
389
+ return {
390
+ success: true,
391
+ testCase: { id: data.id, testKey: data.test_key, title: data.title, targetRoute: data.target_route },
392
+ message: `Test case ${data.test_key} updated successfully`,
393
+ updatedFields: Object.keys(updates)
394
+ };
395
+ }
396
+ async function deleteTestCases(supabase, projectId, args) {
397
+ const modes = [args.test_case_id, args.test_key, args.test_case_ids, args.test_keys].filter((v) => v !== void 0 && v !== null);
398
+ if (modes.length === 0) return { error: "Must provide one of: test_case_id, test_key, test_case_ids, or test_keys" };
399
+ if (modes.length > 1) return { error: "Provide only one of: test_case_id, test_key, test_case_ids, or test_keys" };
400
+ let idsToDelete = [];
401
+ if (args.test_case_id) {
402
+ if (!isValidUUID(args.test_case_id)) return { error: "Invalid test_case_id format (must be UUID)" };
403
+ idsToDelete = [args.test_case_id];
404
+ }
405
+ if (args.test_key) {
406
+ const { data: existing } = await supabase.from("test_cases").select("id").eq("project_id", projectId).eq("test_key", args.test_key).single();
407
+ if (!existing) return { error: `Test case with key "${args.test_key}" not found` };
408
+ idsToDelete = [existing.id];
409
+ }
410
+ if (args.test_case_ids) {
411
+ if (!Array.isArray(args.test_case_ids) || args.test_case_ids.length === 0) return { error: "test_case_ids must be a non-empty array" };
412
+ if (args.test_case_ids.length > 50) return { error: "Cannot delete more than 50 test cases at once" };
413
+ const invalidIds = args.test_case_ids.filter((id) => !isValidUUID(id));
414
+ if (invalidIds.length > 0) return { error: `Invalid UUID(s): ${invalidIds.join(", ")}` };
415
+ idsToDelete = args.test_case_ids;
416
+ }
417
+ if (args.test_keys) {
418
+ if (!Array.isArray(args.test_keys) || args.test_keys.length === 0) return { error: "test_keys must be a non-empty array" };
419
+ if (args.test_keys.length > 50) return { error: "Cannot delete more than 50 test cases at once" };
420
+ const { data: existing, error: lookupError } = await supabase.from("test_cases").select("id, test_key").eq("project_id", projectId).in("test_key", args.test_keys);
421
+ if (lookupError) return { error: lookupError.message };
422
+ const foundKeys = (existing || []).map((tc) => tc.test_key);
423
+ const notFound = args.test_keys.filter((k) => !foundKeys.includes(k));
424
+ if (notFound.length > 0) return { error: `Test case(s) not found: ${notFound.join(", ")}` };
425
+ idsToDelete = (existing || []).map((tc) => tc.id);
426
+ }
427
+ const { data: toDelete } = await supabase.from("test_cases").select("id, test_key, title").eq("project_id", projectId).in("id", idsToDelete);
428
+ if (!toDelete || toDelete.length === 0) return { error: "No matching test cases found in this project" };
429
+ const { error: deleteError } = await supabase.from("test_cases").delete().eq("project_id", projectId).in("id", idsToDelete);
430
+ if (deleteError) return { error: deleteError.message };
431
+ return {
432
+ success: true,
433
+ deletedCount: toDelete.length,
434
+ deleted: toDelete.map((tc) => ({ id: tc.id, testKey: tc.test_key, title: tc.title })),
435
+ message: toDelete.length === 1 ? `Test case ${toDelete[0].test_key} deleted successfully` : `${toDelete.length} test cases deleted successfully`,
436
+ warning: "Associated test_assignments, test_feedback, and ai_test_runs have been cascade-deleted. Reports and qa_findings referencing these tests now have null test_case_id."
437
+ };
438
+ }
439
+ async function listTestCases(supabase, projectId, args) {
440
+ let query = supabase.from("test_cases").select(`id, test_key, title, description, priority, target_route, preconditions, expected_result, steps, track:qa_tracks(id, name, icon, color)`).eq("project_id", projectId).order("test_key", { ascending: true });
441
+ if (args.priority) query = query.eq("priority", args.priority);
442
+ if (args.missing_target_route) query = query.is("target_route", null);
443
+ const limit = args.limit || 100;
444
+ const offset = args.offset || 0;
445
+ query = query.range(offset, offset + limit - 1);
446
+ const { data, error } = await query;
447
+ if (error) return { error: error.message };
448
+ let testCases = data || [];
449
+ if (args.track) {
450
+ testCases = testCases.filter(
451
+ (tc) => tc.track?.name?.toLowerCase().includes(args.track.toLowerCase())
452
+ );
453
+ }
454
+ return {
455
+ count: testCases.length,
456
+ testCases: testCases.map((tc) => ({
457
+ id: tc.id,
458
+ testKey: tc.test_key,
459
+ title: tc.title,
460
+ description: tc.description,
461
+ priority: tc.priority,
462
+ targetRoute: tc.target_route,
463
+ hasTargetRoute: !!tc.target_route,
464
+ track: tc.track?.name || null,
465
+ stepsCount: tc.steps?.length || 0
466
+ })),
467
+ pagination: { limit, offset, hasMore: testCases.length === limit }
468
+ };
469
+ }
470
+ async function listTestRuns(supabase, projectId, args) {
471
+ const limit = Math.min(args.limit || 20, 50);
472
+ let query = supabase.from("test_runs").select("id, name, description, status, total_tests, passed_tests, failed_tests, started_at, completed_at, created_at").eq("project_id", projectId).order("created_at", { ascending: false }).limit(limit);
473
+ if (args.status) query = query.eq("status", args.status);
474
+ const { data, error } = await query;
475
+ if (error) return { error: error.message };
476
+ return {
477
+ count: (data || []).length,
478
+ testRuns: (data || []).map((r) => ({
479
+ id: r.id,
480
+ name: r.name,
481
+ description: r.description,
482
+ status: r.status,
483
+ totalTests: r.total_tests,
484
+ passedTests: r.passed_tests,
485
+ failedTests: r.failed_tests,
486
+ passRate: r.total_tests > 0 ? Math.round(r.passed_tests / r.total_tests * 100) : 0,
487
+ startedAt: r.started_at,
488
+ completedAt: r.completed_at,
489
+ createdAt: r.created_at
490
+ }))
491
+ };
492
+ }
493
+ async function createTestRun(supabase, projectId, args) {
494
+ if (!args.name || args.name.trim().length === 0) return { error: "Test run name is required" };
495
+ const { data, error } = await supabase.from("test_runs").insert({ project_id: projectId, name: args.name.trim(), description: args.description?.trim() || null, status: "draft" }).select("id, name, description, status, created_at").single();
496
+ if (error) return { error: error.message };
497
+ return {
498
+ success: true,
499
+ testRun: { id: data.id, name: data.name, description: data.description, status: data.status, createdAt: data.created_at },
500
+ message: `Test run "${data.name}" created. Use assign_tests to add test assignments.`
501
+ };
502
+ }
503
+ async function listTestAssignments(supabase, projectId, args) {
504
+ const limit = Math.min(args.limit || 50, 200);
505
+ if (args.tester_id && !isValidUUID(args.tester_id)) return { error: "Invalid tester_id format" };
506
+ if (args.test_run_id && !isValidUUID(args.test_run_id)) return { error: "Invalid test_run_id format" };
507
+ let query = supabase.from("test_assignments").select(`id, status, assigned_at, started_at, completed_at, duration_seconds, is_verification, notes,
508
+ test_case:test_cases(id, test_key, title, priority, target_route),
509
+ tester:testers(id, name, email), test_run:test_runs(id, name)`).eq("project_id", projectId).order("assigned_at", { ascending: false }).limit(limit);
510
+ if (args.tester_id) query = query.eq("tester_id", args.tester_id);
511
+ if (args.test_run_id) query = query.eq("test_run_id", args.test_run_id);
512
+ if (args.status) query = query.eq("status", args.status);
513
+ const { data, error } = await query;
514
+ if (error) return { error: error.message };
515
+ return {
516
+ count: (data || []).length,
517
+ assignments: (data || []).map((a) => ({
518
+ id: a.id,
519
+ status: a.status,
520
+ assignedAt: a.assigned_at,
521
+ startedAt: a.started_at,
522
+ completedAt: a.completed_at,
523
+ durationSeconds: a.duration_seconds,
524
+ isVerification: a.is_verification,
525
+ notes: a.notes,
526
+ testCase: a.test_case ? { id: a.test_case.id, testKey: a.test_case.test_key, title: a.test_case.title, priority: a.test_case.priority, targetRoute: a.test_case.target_route } : null,
527
+ tester: a.tester ? { id: a.tester.id, name: a.tester.name, email: a.tester.email } : null,
528
+ testRun: a.test_run ? { id: a.test_run.id, name: a.test_run.name } : null
529
+ }))
530
+ };
531
+ }
532
+ async function assignTests(supabase, projectId, args) {
533
+ if (!isValidUUID(args.tester_id)) return { error: "Invalid tester_id format" };
534
+ if (!args.test_case_ids || args.test_case_ids.length === 0) return { error: "At least one test_case_id is required" };
535
+ if (args.test_case_ids.length > 50) return { error: "Maximum 50 test cases per assignment batch" };
536
+ for (const id of args.test_case_ids) {
537
+ if (!isValidUUID(id)) return { error: `Invalid test_case_id format: ${id}` };
538
+ }
539
+ if (args.test_run_id && !isValidUUID(args.test_run_id)) return { error: "Invalid test_run_id format" };
540
+ const { data: tester, error: testerErr } = await supabase.from("testers").select("id, name, email, status").eq("id", args.tester_id).eq("project_id", projectId).single();
541
+ if (testerErr || !tester) return { error: "Tester not found in this project" };
542
+ if (tester.status !== "active") return { error: `Tester "${tester.name}" is ${tester.status}, not active` };
543
+ const { data: testCases, error: tcErr } = await supabase.from("test_cases").select("id, test_key, title").eq("project_id", projectId).in("id", args.test_case_ids);
544
+ if (tcErr) return { error: tcErr.message };
545
+ const foundIds = new Set((testCases || []).map((tc) => tc.id));
546
+ const missingIds = args.test_case_ids.filter((id) => !foundIds.has(id));
547
+ if (missingIds.length > 0) return { error: `Test cases not found in this project: ${missingIds.join(", ")}` };
548
+ if (args.test_run_id) {
549
+ const { data: run, error: runErr } = await supabase.from("test_runs").select("id").eq("id", args.test_run_id).eq("project_id", projectId).single();
550
+ if (runErr || !run) return { error: "Test run not found in this project" };
551
+ }
552
+ const rows = args.test_case_ids.map((tcId) => ({
553
+ project_id: projectId,
554
+ test_case_id: tcId,
555
+ tester_id: args.tester_id,
556
+ test_run_id: args.test_run_id || null,
557
+ status: "pending"
558
+ }));
559
+ async function syncRunCounter() {
560
+ if (!args.test_run_id) return;
561
+ const { count } = await supabase.from("test_assignments").select("id", { count: "exact", head: true }).eq("test_run_id", args.test_run_id).eq("project_id", projectId);
562
+ if (count !== null) {
563
+ await supabase.from("test_runs").update({ total_tests: count }).eq("id", args.test_run_id);
564
+ }
565
+ }
566
+ const { data: inserted, error: insertErr } = await supabase.from("test_assignments").insert(rows).select("id, test_case_id");
567
+ if (insertErr) {
568
+ if (insertErr.message.includes("duplicate") || insertErr.message.includes("unique")) {
569
+ const created = [];
570
+ const skipped = [];
571
+ for (const row of rows) {
572
+ const { data: single, error: singleErr } = await supabase.from("test_assignments").insert(row).select("id, test_case_id").single();
573
+ if (singleErr) {
574
+ const tc = testCases?.find((t) => t.id === row.test_case_id);
575
+ skipped.push(tc?.test_key || row.test_case_id);
576
+ } else if (single) {
577
+ created.push(single);
578
+ }
579
+ }
580
+ await syncRunCounter();
581
+ return {
582
+ success: true,
583
+ created: created.length,
584
+ skipped: skipped.length,
585
+ skippedTests: skipped,
586
+ tester: { id: tester.id, name: tester.name },
587
+ message: `Assigned ${created.length} test(s) to ${tester.name}. ${skipped.length} skipped (already assigned).`
588
+ };
589
+ }
590
+ return { error: insertErr.message };
591
+ }
592
+ await syncRunCounter();
593
+ return {
594
+ success: true,
595
+ created: (inserted || []).length,
596
+ skipped: 0,
597
+ tester: { id: tester.id, name: tester.name },
598
+ testRun: args.test_run_id || null,
599
+ message: `Assigned ${(inserted || []).length} test(s) to ${tester.name}.`
600
+ };
601
+ }
602
+
603
+ // src/intelligence.ts
604
+ async function tryRefreshRouteStats(supabase, projectId) {
605
+ try {
606
+ await supabase.rpc("refresh_route_test_stats", { p_project_id: projectId });
607
+ } catch {
608
+ }
609
+ }
610
+ async function getBugPatterns(supabase, projectId, args) {
611
+ const query = supabase.from("reports").select("app_context, severity, status, created_at").eq("project_id", projectId).eq("report_type", "bug").order("created_at", { ascending: false }).limit(100);
612
+ const { data: bugs, error } = await query;
613
+ if (error) return { error: error.message };
614
+ const routePatterns = {};
615
+ for (const bug of bugs || []) {
616
+ const route = bug.app_context?.currentRoute || "unknown";
617
+ if (args.route && !route.includes(args.route)) continue;
618
+ if (!routePatterns[route]) routePatterns[route] = { total: 0, critical: 0, open: 0, resolved: 0 };
619
+ routePatterns[route].total++;
620
+ if (bug.severity === "critical" || bug.severity === "high") routePatterns[route].critical++;
621
+ if (["new", "confirmed", "in_progress"].includes(bug.status)) routePatterns[route].open++;
622
+ else routePatterns[route].resolved++;
623
+ }
624
+ const hotspots = Object.entries(routePatterns).map(([route, stats]) => ({ route, ...stats })).sort((a, b) => b.total - a.total).slice(0, 10);
625
+ return {
626
+ hotspots,
627
+ summary: {
628
+ totalRoutes: Object.keys(routePatterns).length,
629
+ totalBugs: bugs?.length || 0,
630
+ criticalRoutes: hotspots.filter((h) => h.critical > 0).length
631
+ },
632
+ recommendations: hotspots.filter((h) => h.open > 0).map((h) => `Route "${h.route}" has ${h.open} open bugs - prioritize testing here`)
633
+ };
634
+ }
635
+ async function suggestTestCases(supabase, projectId, args) {
636
+ const count = args.count || 5;
637
+ const { data: existingTests } = await supabase.from("test_cases").select("test_key, title").eq("project_id", projectId).order("test_key", { ascending: false }).limit(1);
638
+ const lastKey = existingTests?.[0]?.test_key || "TC-000";
639
+ const lastNum = parseInt(lastKey.replace("TC-", "")) || 0;
640
+ const patterns = await getBugPatterns(supabase, projectId, { route: args.route });
641
+ const suggestions = [];
642
+ const track = args.track || "functional";
643
+ const route = args.route || "/";
644
+ const templates = getTrackTemplates(track);
645
+ for (let i = 0; i < count; i++) {
646
+ const keyNum = lastNum + i + 1;
647
+ const template = templates[i % templates.length];
648
+ suggestions.push({
649
+ test_key: `TC-${String(keyNum).padStart(3, "0")}`,
650
+ title: template.title.replace("{route}", route),
651
+ description: template.description.replace("{route}", route),
652
+ track,
653
+ priority: i === 0 ? "P1" : "P2",
654
+ steps: template.steps.map((s, idx) => ({
655
+ stepNumber: idx + 1,
656
+ action: s.action.replace("{route}", route),
657
+ expectedResult: s.expectedResult.replace("{route}", route)
658
+ })),
659
+ expected_result: template.expected_result.replace("{route}", route)
660
+ });
661
+ }
662
+ const { data: relatedBugs } = await supabase.from("reports").select("id, description, severity").eq("project_id", projectId).eq("report_type", "bug").limit(10);
663
+ const routeBugs = (relatedBugs || []).slice(0, 5);
664
+ return {
665
+ suggestions,
666
+ context: { route: args.route || "all", track, bugHotspots: patterns.hotspots?.slice(0, 3) || [] },
667
+ historicalContext: {
668
+ relatedBugs: routeBugs.map((b) => ({ id: b.id, description: b.description.slice(0, 100), severity: b.severity })),
669
+ recommendation: routeBugs.length > 0 ? `These test suggestions are informed by ${routeBugs.length} historical bug(s) in this area.` : "No historical bugs found for this route."
670
+ },
671
+ instructions: `Review these suggestions and use create_test_case to add them to BugBear.
672
+ You can modify the suggestions before creating them.`
673
+ };
674
+ }
675
+ async function getTestPriorities(supabase, projectId, args) {
676
+ const limit = args.limit || 10;
677
+ const minScore = args.min_score || 0;
678
+ const includeFactors = args.include_factors !== false;
679
+ await tryRefreshRouteStats(supabase, projectId);
680
+ const { data: routes, error } = await supabase.from("route_test_stats").select("*").eq("project_id", projectId).gte("priority_score", minScore).order("priority_score", { ascending: false }).limit(limit);
681
+ if (error) return { error: error.message };
682
+ if (!routes || routes.length === 0) {
683
+ return { priorities: [], summary: { totalRoutes: 0, criticalCount: 0, highCount: 0, mediumCount: 0 }, guidance: "No routes found with test data." };
684
+ }
685
+ const getUrgency = (score) => score >= 70 ? "critical" : score >= 50 ? "high" : score >= 30 ? "medium" : "low";
686
+ const getRecommendation = (route) => {
687
+ if (route.critical_bugs > 0) return `Critical bugs exist - prioritize immediate testing and bug fixes`;
688
+ if (route.open_bugs >= 3) return `Multiple open bugs (${route.open_bugs}) - comprehensive testing needed`;
689
+ if (route.last_tested_at === null) return `Never tested - establish baseline coverage`;
690
+ const daysSinceTest = Math.floor((Date.now() - new Date(route.last_tested_at).getTime()) / (1e3 * 60 * 60 * 24));
691
+ if (daysSinceTest > 14) return `Stale coverage (${daysSinceTest} days) - refresh testing`;
692
+ if (route.regression_count > 0) return `Regression risk (${route.regression_count} past regressions) - add regression tests`;
693
+ if (route.test_case_count < 3) return `Low test coverage (${route.test_case_count} tests) - add more test cases`;
694
+ return "Maintain current testing cadence";
695
+ };
696
+ const priorities = routes.map((route, idx) => {
697
+ const daysSinceTest = route.last_tested_at ? Math.floor((Date.now() - new Date(route.last_tested_at).getTime()) / (1e3 * 60 * 60 * 24)) : null;
698
+ const priority = {
699
+ rank: idx + 1,
700
+ route: route.route,
701
+ priorityScore: route.priority_score,
702
+ urgency: getUrgency(route.priority_score),
703
+ stats: { openBugs: route.open_bugs, criticalBugs: route.critical_bugs, highBugs: route.high_bugs, testCases: route.test_case_count, daysSinceTest, regressions: route.regression_count, recentBugs: route.bugs_last_7_days },
704
+ recommendation: getRecommendation(route)
705
+ };
706
+ if (includeFactors) {
707
+ const bugFreqScore = Math.min(route.open_bugs * 5, 30);
708
+ const criticalScore = Math.min(route.critical_bugs * 25 + route.high_bugs * 10, 25);
709
+ const stalenessScore = daysSinceTest === null ? 20 : Math.min(daysSinceTest, 20);
710
+ const coverageScore = Math.max(15 - route.test_case_count * 5, 0);
711
+ const regressionScore = Math.min(route.regression_count * 10, 10);
712
+ priority.factors = {
713
+ bugFrequency: { score: bugFreqScore, openBugs: route.open_bugs, bugs7d: route.bugs_last_7_days },
714
+ criticalSeverity: { score: criticalScore, critical: route.critical_bugs, high: route.high_bugs },
715
+ staleness: { score: stalenessScore, daysSinceTest },
716
+ coverageGap: { score: coverageScore, testCount: route.test_case_count },
717
+ regressionRisk: { score: regressionScore, regressionCount: route.regression_count }
718
+ };
719
+ }
720
+ return priority;
721
+ });
722
+ const criticalCount = priorities.filter((p) => p.urgency === "critical").length;
723
+ const highCount = priorities.filter((p) => p.urgency === "high").length;
724
+ const mediumCount = priorities.filter((p) => p.urgency === "medium").length;
725
+ return {
726
+ priorities,
727
+ summary: { totalRoutes: routes.length, criticalCount, highCount, mediumCount },
728
+ guidance: criticalCount > 0 ? `URGENT: ${criticalCount} route(s) need immediate attention.` : highCount > 0 ? `${highCount} route(s) have high priority.` : "QA coverage is in good shape."
729
+ };
730
+ }
731
+ async function getCoverageGaps(supabase, projectId, args) {
732
+ const gapType = args.gap_type || "all";
733
+ const staleDays = args.stale_days || 14;
734
+ const gaps = { untested: [], missingTracks: [], stale: [] };
735
+ const { data: routesFromReports } = await supabase.from("reports").select("app_context").eq("project_id", projectId).not("app_context->currentRoute", "is", null);
736
+ const allRoutes = /* @__PURE__ */ new Set();
737
+ (routesFromReports || []).forEach((r) => {
738
+ const route = r.app_context?.currentRoute;
739
+ if (route) allRoutes.add(route);
740
+ });
741
+ const { data: testCases } = await supabase.from("test_cases").select("target_route, category, track_id").eq("project_id", projectId);
742
+ const coveredRoutes = /* @__PURE__ */ new Set();
743
+ const routeTrackCoverage = {};
744
+ (testCases || []).forEach((tc) => {
745
+ const route = tc.target_route || tc.category;
746
+ if (route) {
747
+ coveredRoutes.add(route);
748
+ if (!routeTrackCoverage[route]) routeTrackCoverage[route] = /* @__PURE__ */ new Set();
749
+ if (tc.track_id) routeTrackCoverage[route].add(tc.track_id);
750
+ }
751
+ });
752
+ const { data: tracks } = await supabase.from("qa_tracks").select("id, name").eq("project_id", projectId);
753
+ const trackMap = new Map((tracks || []).map((t) => [t.id, t.name]));
754
+ const { data: routeStats } = await supabase.from("route_test_stats").select("route, last_tested_at, open_bugs, critical_bugs").eq("project_id", projectId);
755
+ const routeStatsMap = new Map((routeStats || []).map((r) => [r.route, r]));
756
+ if (gapType === "all" || gapType === "untested_routes") {
757
+ allRoutes.forEach((route) => {
758
+ if (!coveredRoutes.has(route)) {
759
+ const stats = routeStatsMap.get(route);
760
+ const severity = (stats?.critical_bugs || 0) > 0 ? "critical" : (stats?.open_bugs || 0) > 2 ? "high" : "medium";
761
+ gaps.untested.push({ route, severity, type: "untested", details: { openBugs: stats?.open_bugs || 0, criticalBugs: stats?.critical_bugs || 0 }, recommendation: `Create test cases for ${route}` });
762
+ }
763
+ });
764
+ gaps.untested.sort((a, b) => {
765
+ const o = { critical: 0, high: 1, medium: 2 };
766
+ return (o[a.severity] || 2) - (o[b.severity] || 2);
767
+ });
768
+ }
769
+ if (gapType === "all" || gapType === "missing_tracks") {
770
+ coveredRoutes.forEach((route) => {
771
+ const coveredTracks = routeTrackCoverage[route] || /* @__PURE__ */ new Set();
772
+ const missingTracks = [];
773
+ trackMap.forEach((name, id) => {
774
+ if (!coveredTracks.has(id)) missingTracks.push(name);
775
+ });
776
+ if (missingTracks.length > 0 && missingTracks.length < trackMap.size) {
777
+ gaps.missingTracks.push({ route, severity: missingTracks.length > trackMap.size / 2 ? "high" : "medium", type: "missing_tracks", details: { missingTracks }, recommendation: `Add tests for tracks: ${missingTracks.join(", ")}` });
778
+ }
779
+ });
780
+ }
781
+ if (gapType === "all" || gapType === "stale_coverage") {
782
+ (routeStats || []).forEach((stat) => {
783
+ if (stat.last_tested_at) {
784
+ const daysSince = Math.floor((Date.now() - new Date(stat.last_tested_at).getTime()) / 864e5);
785
+ if (daysSince >= staleDays) {
786
+ gaps.stale.push({ route: stat.route, severity: daysSince >= staleDays * 2 ? "high" : "medium", type: "stale", details: { daysSinceTest: daysSince, openBugs: stat.open_bugs, criticalBugs: stat.critical_bugs }, recommendation: `Re-run tests for ${stat.route}` });
787
+ }
788
+ }
789
+ });
790
+ gaps.stale.sort((a, b) => (b.details.daysSinceTest || 0) - (a.details.daysSinceTest || 0));
791
+ }
792
+ const recommendations = [];
793
+ if (gaps.untested.length > 0) recommendations.push(`${gaps.untested.length} route(s) have bugs but no test coverage`);
794
+ if (gaps.missingTracks.length > 0) recommendations.push(`${gaps.missingTracks.length} route(s) are missing track-specific tests`);
795
+ if (gaps.stale.length > 0) recommendations.push(`${gaps.stale.length} route(s) have stale coverage (>${staleDays} days)`);
796
+ return { gaps, summary: { untestedRoutes: gaps.untested.length, routesMissingTracks: gaps.missingTracks.length, staleRoutes: gaps.stale.length }, recommendations };
797
+ }
798
+ async function getRegressions(supabase, projectId, args) {
799
+ const days = args.days || 30;
800
+ const includeHistory = args.include_history || false;
801
+ const cutoff = new Date(Date.now() - days * 864e5).toISOString();
802
+ const { data: resolvedBugs } = await supabase.from("reports").select("id, description, severity, app_context, resolved_at").eq("project_id", projectId).eq("report_type", "bug").in("status", ["resolved", "fixed", "verified", "closed"]).gte("resolved_at", cutoff);
803
+ const { data: newBugs } = await supabase.from("reports").select("id, description, severity, app_context, created_at").eq("project_id", projectId).eq("report_type", "bug").in("status", ["new", "triaging", "confirmed", "in_progress", "reviewed"]).gte("created_at", cutoff);
804
+ const routeData = {};
805
+ (resolvedBugs || []).forEach((bug) => {
806
+ const route = bug.app_context?.currentRoute;
807
+ if (route) {
808
+ if (!routeData[route]) routeData[route] = { resolved: [], new: [] };
809
+ routeData[route].resolved.push(bug);
810
+ }
811
+ });
812
+ (newBugs || []).forEach((bug) => {
813
+ const route = bug.app_context?.currentRoute;
814
+ if (route) {
815
+ if (!routeData[route]) routeData[route] = { resolved: [], new: [] };
816
+ routeData[route].new.push(bug);
817
+ }
818
+ });
819
+ const regressions = [];
820
+ let criticalCount = 0;
821
+ const recurringPatterns = [];
822
+ Object.entries(routeData).forEach(([route, data]) => {
823
+ if (data.resolved.length > 0 && data.new.length > 0) {
824
+ const latestResolved = data.resolved.sort((a, b) => new Date(b.resolved_at).getTime() - new Date(a.resolved_at).getTime())[0];
825
+ const daysSinceResolution = latestResolved.resolved_at ? Math.floor((Date.now() - new Date(latestResolved.resolved_at).getTime()) / 864e5) : null;
826
+ const severity = data.new.some((b) => b.severity === "critical") ? "critical" : data.new.some((b) => b.severity === "high") ? "high" : "medium";
827
+ if (severity === "critical") criticalCount++;
828
+ if (data.resolved.length >= 2 && data.new.length >= 2) recurringPatterns.push(route);
829
+ const regression = {
830
+ route,
831
+ severity,
832
+ originalBug: { id: latestResolved.id, description: latestResolved.description?.slice(0, 100) || "", resolvedAt: latestResolved.resolved_at },
833
+ newBugs: data.new.map((b) => ({ id: b.id, description: b.description?.slice(0, 100) || "", severity: b.severity, createdAt: b.created_at })),
834
+ daysSinceResolution,
835
+ regressionCount: data.resolved.length
836
+ };
837
+ if (includeHistory) regression.history = { totalResolved: data.resolved.length, totalNew: data.new.length, resolvedBugs: data.resolved.map((b) => ({ id: b.id, severity: b.severity, resolvedAt: b.resolved_at })) };
838
+ regressions.push(regression);
839
+ }
840
+ });
841
+ regressions.sort((a, b) => {
842
+ const o = { critical: 0, high: 1, medium: 2 };
843
+ return (o[a.severity] || 2) - (o[b.severity] || 2) || b.newBugs.length - a.newBugs.length;
844
+ });
845
+ const recommendations = [];
846
+ if (criticalCount > 0) recommendations.push(`URGENT: ${criticalCount} regression(s) involve critical bugs`);
847
+ if (recurringPatterns.length > 0) recommendations.push(`Recurring patterns in ${recurringPatterns.length} route(s): ${recurringPatterns.slice(0, 3).join(", ")}`);
848
+ if (regressions.length > 0) recommendations.push("Add regression tests to prevent recurrence");
849
+ return { regressions, summary: { totalRegressions: regressions.length, criticalRegressions: criticalCount, recurringPatterns: recurringPatterns.length }, recommendations };
850
+ }
851
+ async function getCoverageMatrix(supabase, projectId, args) {
852
+ const includeExecution = args.include_execution_data !== false;
853
+ const includeBugs = args.include_bug_counts !== false;
854
+ const { data: tracks } = await supabase.from("qa_tracks").select("id, name, icon, color").eq("project_id", projectId).order("sort_order");
855
+ const { data: testCases } = await supabase.from("test_cases").select("id, target_route, category, track_id").eq("project_id", projectId);
856
+ let assignments = [];
857
+ if (includeExecution) {
858
+ const { data } = await supabase.from("test_assignments").select("test_case_id, status, completed_at").eq("project_id", projectId).in("status", ["passed", "failed"]).order("completed_at", { ascending: false }).limit(2e3);
859
+ assignments = data || [];
860
+ }
861
+ let routeStats = [];
862
+ if (includeBugs) {
863
+ const { data } = await supabase.from("route_test_stats").select("route, open_bugs, critical_bugs").eq("project_id", projectId);
864
+ routeStats = data || [];
865
+ }
866
+ const routeStatsMap = new Map(routeStats.map((r) => [r.route, r]));
867
+ const assignmentsByTestCase = {};
868
+ assignments.forEach((a) => {
869
+ if (!assignmentsByTestCase[a.test_case_id]) assignmentsByTestCase[a.test_case_id] = { passed: 0, failed: 0, lastTested: null };
870
+ if (a.status === "passed") assignmentsByTestCase[a.test_case_id].passed++;
871
+ if (a.status === "failed") assignmentsByTestCase[a.test_case_id].failed++;
872
+ if (a.completed_at) {
873
+ const current = assignmentsByTestCase[a.test_case_id].lastTested;
874
+ if (!current || new Date(a.completed_at) > new Date(current)) assignmentsByTestCase[a.test_case_id].lastTested = a.completed_at;
875
+ }
876
+ });
877
+ const routeMap = {};
878
+ (testCases || []).forEach((tc) => {
879
+ const route = tc.target_route || tc.category || "Uncategorized";
880
+ if (!routeMap[route]) routeMap[route] = { testCases: [], trackCoverage: {} };
881
+ routeMap[route].testCases.push(tc);
882
+ const trackId = tc.track_id || "none";
883
+ if (!routeMap[route].trackCoverage[trackId]) routeMap[route].trackCoverage[trackId] = [];
884
+ routeMap[route].trackCoverage[trackId].push(tc);
885
+ });
886
+ const matrix = [];
887
+ const now = Date.now();
888
+ Object.entries(routeMap).forEach(([route, data]) => {
889
+ const row = { route, totalTests: data.testCases.length, tracks: {} };
890
+ if (includeBugs) {
891
+ const stats = routeStatsMap.get(route);
892
+ row.openBugs = stats?.open_bugs || 0;
893
+ row.criticalBugs = stats?.critical_bugs || 0;
894
+ }
895
+ let latestTest = null;
896
+ data.testCases.forEach((tc) => {
897
+ const execData = assignmentsByTestCase[tc.id];
898
+ if (execData?.lastTested && (!latestTest || new Date(execData.lastTested) > new Date(latestTest))) latestTest = execData.lastTested;
899
+ });
900
+ row.lastTestedAt = latestTest;
901
+ (tracks || []).forEach((track) => {
902
+ const trackTests = data.trackCoverage[track.id] || [];
903
+ const cell = { testCount: trackTests.length };
904
+ if (includeExecution && trackTests.length > 0) {
905
+ let passCount = 0, failCount = 0;
906
+ let trackLastTested = null;
907
+ trackTests.forEach((tc) => {
908
+ const execData = assignmentsByTestCase[tc.id];
909
+ if (execData) {
910
+ passCount += execData.passed;
911
+ failCount += execData.failed;
912
+ if (execData.lastTested && (!trackLastTested || new Date(execData.lastTested) > new Date(trackLastTested))) trackLastTested = execData.lastTested;
913
+ }
914
+ });
915
+ cell.passCount = passCount;
916
+ cell.failCount = failCount;
917
+ cell.passRate = passCount + failCount > 0 ? Math.round(passCount / (passCount + failCount) * 100) : null;
918
+ cell.lastTestedAt = trackLastTested;
919
+ cell.staleDays = trackLastTested ? Math.floor((now - new Date(trackLastTested).getTime()) / 864e5) : null;
920
+ }
921
+ row.tracks[track.id] = cell;
922
+ });
923
+ const unassignedTests = data.trackCoverage["none"] || [];
924
+ if (unassignedTests.length > 0) {
925
+ const cell = { testCount: unassignedTests.length };
926
+ if (includeExecution) {
927
+ let passCount = 0, failCount = 0;
928
+ unassignedTests.forEach((tc) => {
929
+ const execData = assignmentsByTestCase[tc.id];
930
+ if (execData) {
931
+ passCount += execData.passed;
932
+ failCount += execData.failed;
933
+ }
934
+ });
935
+ cell.passCount = passCount;
936
+ cell.failCount = failCount;
937
+ cell.passRate = passCount + failCount > 0 ? Math.round(passCount / (passCount + failCount) * 100) : null;
938
+ }
939
+ row.tracks["none"] = cell;
940
+ }
941
+ matrix.push(row);
942
+ });
943
+ matrix.sort((a, b) => a.route.localeCompare(b.route));
944
+ return {
945
+ matrix,
946
+ tracks: (tracks || []).map((t) => ({ id: t.id, name: t.name, icon: t.icon, color: t.color })),
947
+ summary: { totalRoutes: matrix.length, totalTests: matrix.reduce((sum, r) => sum + r.totalTests, 0), routesWithCriticalBugs: includeBugs ? matrix.filter((r) => r.criticalBugs > 0).length : void 0 }
948
+ };
949
+ }
950
+ async function getStaleCoverage(supabase, projectId, args) {
951
+ const daysThreshold = args.days_threshold || 14;
952
+ const limit = args.limit || 20;
953
+ await tryRefreshRouteStats(supabase, projectId);
954
+ const { data: routes, error } = await supabase.from("route_test_stats").select("route, last_tested_at, open_bugs, critical_bugs, test_case_count, priority_score").eq("project_id", projectId).order("last_tested_at", { ascending: true, nullsFirst: true }).limit(limit * 2);
955
+ if (error) return { error: error.message };
956
+ const now = Date.now();
957
+ const staleRoutes = [];
958
+ (routes || []).forEach((route) => {
959
+ let daysSinceTest = null;
960
+ if (route.last_tested_at) {
961
+ daysSinceTest = Math.floor((now - new Date(route.last_tested_at).getTime()) / 864e5);
962
+ if (daysSinceTest < daysThreshold) return;
963
+ }
964
+ const riskLevel = route.critical_bugs > 0 ? "critical" : route.open_bugs > 2 ? "high" : daysSinceTest === null ? "high" : "medium";
965
+ staleRoutes.push({
966
+ route: route.route,
967
+ daysSinceTest,
968
+ neverTested: route.last_tested_at === null,
969
+ lastTestedAt: route.last_tested_at,
970
+ openBugs: route.open_bugs,
971
+ criticalBugs: route.critical_bugs,
972
+ testCaseCount: route.test_case_count,
973
+ riskLevel,
974
+ priorityScore: route.priority_score,
975
+ recommendation: route.last_tested_at === null ? "Never tested - establish baseline coverage immediately" : `Last tested ${daysSinceTest} days ago - refresh testing`
976
+ });
977
+ });
978
+ staleRoutes.sort((a, b) => {
979
+ const o = { critical: 0, high: 1, medium: 2 };
980
+ const riskDiff = (o[a.riskLevel] || 2) - (o[b.riskLevel] || 2);
981
+ if (riskDiff !== 0) return riskDiff;
982
+ if (a.neverTested && !b.neverTested) return -1;
983
+ if (!a.neverTested && b.neverTested) return 1;
984
+ return (b.daysSinceTest || 999) - (a.daysSinceTest || 999);
985
+ });
986
+ return {
987
+ staleRoutes: staleRoutes.slice(0, limit),
988
+ summary: { totalStale: staleRoutes.length, neverTested: staleRoutes.filter((r) => r.neverTested).length, withCriticalBugs: staleRoutes.filter((r) => r.criticalBugs > 0).length, threshold: daysThreshold },
989
+ guidance: staleRoutes.length > 0 ? `${staleRoutes.length} route(s) have stale or missing test coverage.` : "All routes have been tested within the threshold period."
990
+ };
991
+ }
992
+ async function generateDeployChecklist(supabase, projectId, args) {
993
+ const routes = args.routes;
994
+ const deploymentType = args.deployment_type || "feature";
995
+ const allRoutes = new Set(routes);
996
+ if (args.changed_files) {
997
+ args.changed_files.forEach((file) => {
998
+ const matches = [/\/app\/(.+?)\/page\./, /\/pages\/(.+?)\./, /\/routes\/(.+?)\./, /\/screens\/(.+?)\./];
999
+ for (const pattern of matches) {
1000
+ const match = file.match(pattern);
1001
+ if (match) {
1002
+ allRoutes.add("/" + match[1].replace(/\[.*?\]/g, ":id"));
1003
+ }
1004
+ }
1005
+ });
1006
+ }
1007
+ const safeRoutes = routes.slice(0, 100);
1008
+ const [{ data: byRoute }, { data: byCategory }] = await Promise.all([
1009
+ supabase.from("test_cases").select("id, test_key, title, target_route, category, priority, track:qa_tracks(name)").eq("project_id", projectId).in("target_route", safeRoutes),
1010
+ supabase.from("test_cases").select("id, test_key, title, target_route, category, priority, track:qa_tracks(name)").eq("project_id", projectId).in("category", safeRoutes)
1011
+ ]);
1012
+ const seenIds = /* @__PURE__ */ new Set();
1013
+ const testCases = [...byRoute || [], ...byCategory || []].filter((tc) => {
1014
+ if (seenIds.has(tc.id)) return false;
1015
+ seenIds.add(tc.id);
1016
+ return true;
1017
+ });
1018
+ const { data: routeStats } = await supabase.from("route_test_stats").select("*").eq("project_id", projectId).in("route", Array.from(allRoutes));
1019
+ const routeStatsMap = new Map((routeStats || []).map((r) => [r.route, r]));
1020
+ const checklist = { critical: [], recommended: [], optional: [], gaps: [] };
1021
+ const coveredRoutes = /* @__PURE__ */ new Set();
1022
+ (testCases || []).forEach((tc) => {
1023
+ const route = tc.target_route || tc.category || "";
1024
+ coveredRoutes.add(route);
1025
+ const stats = routeStatsMap.get(route);
1026
+ const item = { testCaseId: tc.id, testKey: tc.test_key, title: tc.title, route, track: tc.track?.name, priority: tc.priority, hasCriticalBugs: (stats?.critical_bugs || 0) > 0, lastTested: stats?.last_tested_at, reason: "" };
1027
+ if (tc.priority === "P0" || (stats?.critical_bugs || 0) > 0) {
1028
+ item.reason = tc.priority === "P0" ? "P0 priority test case" : `Route has ${stats?.critical_bugs} critical bug(s)`;
1029
+ checklist.critical.push(item);
1030
+ } else if (tc.priority === "P1" || deploymentType === "hotfix") {
1031
+ item.reason = deploymentType === "hotfix" ? "Hotfix deployment - verify fix" : "P1 priority test case";
1032
+ checklist.recommended.push(item);
1033
+ } else if (deploymentType === "release") {
1034
+ item.reason = "Release deployment - full verification";
1035
+ checklist.recommended.push(item);
1036
+ } else {
1037
+ item.reason = "Standard test coverage";
1038
+ checklist.optional.push(item);
1039
+ }
1040
+ });
1041
+ allRoutes.forEach((route) => {
1042
+ if (!coveredRoutes.has(route)) {
1043
+ const stats = routeStatsMap.get(route);
1044
+ checklist.gaps.push({ route, title: `No test coverage for ${route}`, reason: "Route is being deployed but has no test cases", hasCriticalBugs: (stats?.critical_bugs || 0) > 0, openBugs: stats?.open_bugs || 0, recommendation: `Create test cases for ${route} before deploying` });
1045
+ }
1046
+ });
1047
+ const thoroughness = allRoutes.size > 0 ? Math.round(coveredRoutes.size / allRoutes.size * 100) : 100;
1048
+ let guidance = "";
1049
+ if (checklist.critical.length > 0) guidance = `MUST RUN: ${checklist.critical.length} critical test(s) before deploying. `;
1050
+ if (checklist.gaps.length > 0) guidance += `WARNING: ${checklist.gaps.length} route(s) have no test coverage. `;
1051
+ if (deploymentType === "hotfix") guidance += "Hotfix mode: Focus on critical and recommended tests.";
1052
+ else if (deploymentType === "release") guidance += "Release mode: Run all tests for comprehensive verification.";
1053
+ return { checklist, summary: { criticalTests: checklist.critical.length, recommendedTests: checklist.recommended.length, optionalTests: checklist.optional.length, coverageGaps: checklist.gaps.length, thoroughness, deploymentType }, guidance: guidance || "Ready to deploy with standard test coverage." };
1054
+ }
1055
+ async function getQAHealth(supabase, projectId, args) {
1056
+ const periodDays = args.period_days || 30;
1057
+ const comparePrevious = args.compare_previous !== false;
1058
+ const now = /* @__PURE__ */ new Date();
1059
+ const periodStart = new Date(now.getTime() - periodDays * 864e5);
1060
+ const previousStart = new Date(periodStart.getTime() - periodDays * 864e5);
1061
+ const { data: currentTests } = await supabase.from("test_assignments").select("id, status, completed_at").eq("project_id", projectId).gte("completed_at", periodStart.toISOString()).in("status", ["passed", "failed"]);
1062
+ const { data: currentBugs } = await supabase.from("reports").select("id, severity, status, created_at").eq("project_id", projectId).eq("report_type", "bug").gte("created_at", periodStart.toISOString());
1063
+ const { data: resolvedBugs } = await supabase.from("reports").select("id, created_at, resolved_at").eq("project_id", projectId).eq("report_type", "bug").in("status", ["resolved", "fixed", "verified", "closed"]).gte("resolved_at", periodStart.toISOString());
1064
+ const { data: testers } = await supabase.from("testers").select("id, status").eq("project_id", projectId);
1065
+ const { data: routeStats } = await supabase.from("route_test_stats").select("route, test_case_count").eq("project_id", projectId);
1066
+ let previousTests = [], previousBugs = [], previousResolved = [];
1067
+ if (comparePrevious) {
1068
+ const { data: pt } = await supabase.from("test_assignments").select("id, status").eq("project_id", projectId).gte("completed_at", previousStart.toISOString()).lt("completed_at", periodStart.toISOString()).in("status", ["passed", "failed"]);
1069
+ previousTests = pt || [];
1070
+ const { data: pb } = await supabase.from("reports").select("id, severity").eq("project_id", projectId).eq("report_type", "bug").gte("created_at", previousStart.toISOString()).lt("created_at", periodStart.toISOString());
1071
+ previousBugs = pb || [];
1072
+ const { data: pr } = await supabase.from("reports").select("id").eq("project_id", projectId).in("status", ["resolved", "fixed", "verified", "closed"]).gte("resolved_at", previousStart.toISOString()).lt("resolved_at", periodStart.toISOString());
1073
+ previousResolved = pr || [];
1074
+ }
1075
+ const testsCompleted = (currentTests || []).length;
1076
+ const testsPerWeek = Math.round(testsCompleted / (periodDays / 7));
1077
+ const prevTestsPerWeek = comparePrevious ? Math.round(previousTests.length / (periodDays / 7)) : 0;
1078
+ const bugsFound = (currentBugs || []).length;
1079
+ const criticalBugs = (currentBugs || []).filter((b) => b.severity === "critical").length;
1080
+ const bugsPerTest = testsCompleted > 0 ? Math.round(bugsFound / testsCompleted * 100) / 100 : 0;
1081
+ const bugsResolvedCount = (resolvedBugs || []).length;
1082
+ let avgResolutionDays = 0;
1083
+ if (resolvedBugs && resolvedBugs.length > 0) {
1084
+ const totalDays = resolvedBugs.reduce((sum, bug) => {
1085
+ if (bug.created_at && bug.resolved_at) return sum + (new Date(bug.resolved_at).getTime() - new Date(bug.created_at).getTime()) / 864e5;
1086
+ return sum;
1087
+ }, 0);
1088
+ avgResolutionDays = Math.round(totalDays / resolvedBugs.length);
1089
+ }
1090
+ const totalRoutes = (routeStats || []).length;
1091
+ const routesWithTests = (routeStats || []).filter((r) => r.test_case_count > 0).length;
1092
+ const routeCoverage = totalRoutes > 0 ? Math.round(routesWithTests / totalRoutes * 100) : 0;
1093
+ const totalTesters = (testers || []).length;
1094
+ const activeTesters = (testers || []).filter((t) => t.status === "active").length;
1095
+ const utilizationPercent = totalTesters > 0 ? Math.round(activeTesters / totalTesters * 100) : 0;
1096
+ const getTrend = (current, previous) => {
1097
+ if (!comparePrevious || previous === 0) return "stable";
1098
+ const change = (current - previous) / previous * 100;
1099
+ return change > 10 ? "up" : change < -10 ? "down" : "stable";
1100
+ };
1101
+ const getChangePercent = (current, previous) => !comparePrevious || previous === 0 ? void 0 : Math.round((current - previous) / previous * 100);
1102
+ const coverageScore = routeCoverage;
1103
+ const velocityScore = Math.min(testsPerWeek * 10, 100);
1104
+ const resolutionScore = avgResolutionDays <= 3 ? 100 : avgResolutionDays <= 7 ? 75 : avgResolutionDays <= 14 ? 50 : 25;
1105
+ const stabilityScore = criticalBugs === 0 ? 100 : criticalBugs === 1 ? 75 : criticalBugs <= 3 ? 50 : 25;
1106
+ const overallScore = Math.round(coverageScore * 0.3 + velocityScore * 0.25 + resolutionScore * 0.25 + stabilityScore * 0.2);
1107
+ const getGrade = (score) => score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : score >= 60 ? "D" : "F";
1108
+ const recommendations = [];
1109
+ if (coverageScore < 70) recommendations.push(`Increase test coverage (currently ${routeCoverage}%)`);
1110
+ if (velocityScore < 50) recommendations.push(`Increase testing velocity (${testsPerWeek}/week)`);
1111
+ if (avgResolutionDays > 7) recommendations.push(`Improve bug resolution time (currently ${avgResolutionDays} days)`);
1112
+ if (criticalBugs > 0) recommendations.push(`Address ${criticalBugs} critical bug(s) immediately`);
1113
+ if (utilizationPercent < 50) recommendations.push(`Improve tester utilization (${utilizationPercent}%)`);
1114
+ return {
1115
+ metrics: {
1116
+ velocity: { testsPerWeek, testsCompleted, trend: getTrend(testsPerWeek, prevTestsPerWeek), changePercent: getChangePercent(testsPerWeek, prevTestsPerWeek) },
1117
+ bugDiscovery: { bugsFound, bugsPerTest, criticalBugs, trend: getTrend(bugsFound, previousBugs.length), changePercent: getChangePercent(bugsFound, previousBugs.length) },
1118
+ resolution: { bugsResolved: bugsResolvedCount, avgResolutionDays, trend: getTrend(bugsResolvedCount, previousResolved.length), changePercent: getChangePercent(bugsResolvedCount, previousResolved.length) },
1119
+ coverage: { routeCoverage, routesWithTests, totalRoutes },
1120
+ testerHealth: { activeTesters, totalTesters, utilizationPercent }
1121
+ },
1122
+ healthScore: { score: overallScore, grade: getGrade(overallScore), breakdown: { coverage: coverageScore, velocity: velocityScore, resolution: resolutionScore, stability: stabilityScore } },
1123
+ recommendations,
1124
+ period: { days: periodDays, start: periodStart.toISOString(), end: now.toISOString() }
1125
+ };
1126
+ }
1127
+ async function getQASessions(supabase, projectId, args) {
1128
+ const status = args.status || "all";
1129
+ const limit = Math.min(args.limit || 20, 50);
1130
+ const includeFindings = args.include_findings !== false;
1131
+ let query = supabase.from("qa_sessions").select(`id, focus_area, track, platform, started_at, ended_at, notes, routes_covered, status, duration_minutes, findings_count, bugs_filed, created_at, tester:testers(id, name, email)`).eq("project_id", projectId).order("started_at", { ascending: false }).limit(limit);
1132
+ if (status !== "all") query = query.eq("status", status);
1133
+ if (args.tester_id && isValidUUID(args.tester_id)) query = query.eq("tester_id", args.tester_id);
1134
+ const { data: sessions, error } = await query;
1135
+ if (error) return { error: error.message };
1136
+ let sessionsWithFindings = sessions || [];
1137
+ if (includeFindings && sessions && sessions.length > 0) {
1138
+ const sessionIds = sessions.map((s) => s.id);
1139
+ const { data: findings } = await supabase.from("qa_findings").select("id, session_id, type, severity, title, description, route, converted_to_bug_id, dismissed").in("session_id", sessionIds);
1140
+ const findingsBySession = (findings || []).reduce((acc, f) => {
1141
+ acc[f.session_id] = acc[f.session_id] || [];
1142
+ acc[f.session_id].push(f);
1143
+ return acc;
1144
+ }, {});
1145
+ sessionsWithFindings = sessions.map((s) => ({ ...s, findings: findingsBySession[s.id] || [] }));
1146
+ }
1147
+ return {
1148
+ sessions: sessionsWithFindings,
1149
+ summary: { total: sessionsWithFindings.length, active: sessionsWithFindings.filter((s) => s.status === "active").length, completed: sessionsWithFindings.filter((s) => s.status === "completed").length, totalFindings: sessionsWithFindings.reduce((sum, s) => sum + (s.findings_count || 0), 0), totalBugsFiled: sessionsWithFindings.reduce((sum, s) => sum + (s.bugs_filed || 0), 0) }
1150
+ };
1151
+ }
1152
+ async function getQAAlerts(supabase, projectId, args) {
1153
+ const severity = args.severity || "all";
1154
+ const type = args.type || "all";
1155
+ const status = args.status || "active";
1156
+ if (args.refresh) await supabase.rpc("detect_all_alerts", { p_project_id: projectId });
1157
+ let query = supabase.from("qa_alerts").select("*").eq("project_id", projectId).order("severity", { ascending: true }).order("created_at", { ascending: false });
1158
+ if (severity !== "all") query = query.eq("severity", severity);
1159
+ if (type !== "all") query = query.eq("type", type);
1160
+ if (status !== "all") query = query.eq("status", status);
1161
+ const { data: alerts, error } = await query;
1162
+ if (error) return { error: error.message };
1163
+ return {
1164
+ alerts: alerts?.map((a) => ({ id: a.id, type: a.type, severity: a.severity, title: a.title, description: a.description, route: a.trigger_route, track: a.trigger_track, recommendation: a.recommendation, action_type: a.action_type, status: a.status, created_at: a.created_at })),
1165
+ summary: { total: alerts?.length || 0, critical: alerts?.filter((a) => a.severity === "critical").length || 0, warning: alerts?.filter((a) => a.severity === "warning").length || 0, info: alerts?.filter((a) => a.severity === "info").length || 0, byType: { hot_spot: alerts?.filter((a) => a.type === "hot_spot").length || 0, coverage_gap: alerts?.filter((a) => a.type === "coverage_gap").length || 0, track_gap: alerts?.filter((a) => a.type === "track_gap").length || 0 } }
1166
+ };
1167
+ }
1168
+
1169
+ // src/deploy.ts
1170
+ async function getDeploymentAnalysis(supabase, projectId, args) {
1171
+ const limit = Math.min(args.limit || 10, 50);
1172
+ const includeTestingPriority = args.include_testing_priority !== false;
1173
+ if (args.deployment_id && isValidUUID(args.deployment_id)) {
1174
+ const { data: deployment, error: error2 } = await supabase.from("deployments").select("*").eq("id", args.deployment_id).eq("project_id", projectId).single();
1175
+ if (error2) return { error: error2.message };
1176
+ return { deployment };
1177
+ }
1178
+ let query = supabase.from("deployments").select("*").eq("project_id", projectId).order("deployed_at", { ascending: false }).limit(limit);
1179
+ if (args.environment && args.environment !== "all") {
1180
+ query = query.eq("environment", args.environment);
1181
+ }
1182
+ const { data: deployments, error } = await query;
1183
+ if (error) return { error: error.message };
1184
+ const summary = {
1185
+ total: deployments?.length || 0,
1186
+ avgRiskScore: deployments?.length ? Math.round(deployments.reduce((sum, d) => sum + (d.risk_score || 0), 0) / deployments.length) : 0,
1187
+ highRisk: deployments?.filter((d) => (d.risk_score || 0) >= 70).length || 0,
1188
+ verified: deployments?.filter((d) => d.verified_at).length || 0
1189
+ };
1190
+ return {
1191
+ deployments: deployments?.map((d) => ({
1192
+ id: d.id,
1193
+ environment: d.environment,
1194
+ commit_sha: d.commit_sha,
1195
+ commit_message: d.commit_message,
1196
+ branch: d.branch,
1197
+ deployed_at: d.deployed_at,
1198
+ risk_score: d.risk_score,
1199
+ routes_affected: d.routes_affected,
1200
+ testing_priority: includeTestingPriority ? d.testing_priority : void 0,
1201
+ verified: !!d.verified_at
1202
+ })),
1203
+ summary
1204
+ };
1205
+ }
1206
+ async function getTesterRecommendations(supabase, _projectId, args) {
1207
+ if (!isValidUUID(args.test_case_id)) {
1208
+ return { error: "Invalid test_case_id format" };
1209
+ }
1210
+ const limit = Math.min(args.limit || 5, 10);
1211
+ const { data: recommendations, error } = await supabase.rpc("get_tester_recommendations", {
1212
+ p_test_case_id: args.test_case_id,
1213
+ p_limit: limit
1214
+ });
1215
+ if (error) return { error: error.message };
1216
+ const { data: testCase } = await supabase.from("test_cases").select("test_key, title, track:qa_tracks(name)").eq("id", args.test_case_id).single();
1217
+ return {
1218
+ test_case: testCase ? {
1219
+ id: args.test_case_id,
1220
+ test_key: testCase.test_key,
1221
+ title: testCase.title,
1222
+ track: testCase.track?.name
1223
+ } : null,
1224
+ recommendations: recommendations?.map((r) => ({
1225
+ tester_id: r.tester_id,
1226
+ name: r.tester_name,
1227
+ email: r.tester_email,
1228
+ match_score: r.match_score,
1229
+ reasons: r.match_reasons
1230
+ })) || []
1231
+ };
1232
+ }
1233
+ async function analyzeCommitForTesting(supabase, projectId, args) {
1234
+ const filesChanged = args.files_changed || [];
1235
+ const { data: mappings } = await supabase.from("file_route_mapping").select("file_pattern, route, feature, confidence").eq("project_id", projectId);
1236
+ const affectedRoutes = [];
1237
+ for (const mapping of mappings || []) {
1238
+ const matchedFiles = filesChanged.filter((file) => {
1239
+ try {
1240
+ if (mapping.file_pattern.length > 200) return false;
1241
+ const pattern = mapping.file_pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*\\\*/g, ".*").replace(/\\\*/g, "[^/]*");
1242
+ return new RegExp(`^${pattern}$`).test(file);
1243
+ } catch {
1244
+ return false;
1245
+ }
1246
+ });
1247
+ if (matchedFiles.length > 0) {
1248
+ affectedRoutes.push({
1249
+ route: mapping.route,
1250
+ feature: mapping.feature || void 0,
1251
+ confidence: mapping.confidence,
1252
+ matched_files: matchedFiles
1253
+ });
1254
+ }
1255
+ }
1256
+ const routes = affectedRoutes.map((r) => r.route);
1257
+ let bugHistory = [];
1258
+ if (routes.length > 0) {
1259
+ const { data: bugs } = await supabase.from("reports").select("id, severity, description, route, created_at").eq("project_id", projectId).eq("report_type", "bug").in("route", routes).gte("created_at", new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3).toISOString());
1260
+ bugHistory = bugs || [];
1261
+ }
1262
+ let riskScore = 0;
1263
+ riskScore += Math.min(filesChanged.length * 2, 20);
1264
+ riskScore += Math.min(affectedRoutes.length * 5, 30);
1265
+ riskScore += Math.min(bugHistory.length * 10, 50);
1266
+ const recommendations = affectedRoutes.map((r) => {
1267
+ const routeBugs = bugHistory.filter((b) => b.route === r.route);
1268
+ const priority = routeBugs.length >= 3 ? "critical" : routeBugs.length >= 1 ? "high" : "medium";
1269
+ return {
1270
+ route: r.route,
1271
+ feature: r.feature,
1272
+ priority,
1273
+ reason: routeBugs.length > 0 ? `${routeBugs.length} bug(s) in last 30 days` : "Code changed in this area",
1274
+ recent_bugs: routeBugs.length
1275
+ };
1276
+ }).sort((a, b) => {
1277
+ const order = { critical: 0, high: 1, medium: 2 };
1278
+ return (order[a.priority] || 2) - (order[b.priority] || 2);
1279
+ });
1280
+ if (args.record_deployment) {
1281
+ await supabase.rpc("record_deployment", {
1282
+ p_project_id: projectId,
1283
+ p_environment: "production",
1284
+ p_commit_sha: args.commit_sha || null,
1285
+ p_commit_message: args.commit_message || null,
1286
+ p_files_changed: filesChanged,
1287
+ p_webhook_source: "mcp"
1288
+ });
1289
+ }
1290
+ return {
1291
+ commit_sha: args.commit_sha,
1292
+ files_analyzed: filesChanged.length,
1293
+ risk_score: Math.min(riskScore, 100),
1294
+ affected_routes: affectedRoutes,
1295
+ bug_history_summary: {
1296
+ total: bugHistory.length,
1297
+ critical: bugHistory.filter((b) => b.severity === "critical").length,
1298
+ high: bugHistory.filter((b) => b.severity === "high").length
1299
+ },
1300
+ testing_recommendations: recommendations,
1301
+ deployment_recorded: args.record_deployment || false
1302
+ };
1303
+ }
1304
+ async function getTestingPatterns(supabase, _projectId, args) {
1305
+ let patterns = [];
1306
+ if (args.search) {
1307
+ const { data, error } = await supabase.rpc("search_patterns", {
1308
+ p_query: args.search,
1309
+ p_limit: 20
1310
+ });
1311
+ if (!error) patterns = data || [];
1312
+ } else if (args.feature_type) {
1313
+ const { data, error } = await supabase.rpc("get_patterns_for_feature", {
1314
+ p_category: args.feature_type,
1315
+ p_framework: args.framework || null,
1316
+ p_tracks: args.tracks || null
1317
+ });
1318
+ if (!error) patterns = data || [];
1319
+ } else {
1320
+ const { data, error } = await supabase.from("qa_patterns").select("*").eq("is_active", true).order("severity").limit(30);
1321
+ if (!error) patterns = data || [];
1322
+ }
1323
+ const summary = {
1324
+ total: patterns.length,
1325
+ by_severity: {
1326
+ critical: patterns.filter((p) => p.severity === "critical").length,
1327
+ high: patterns.filter((p) => p.severity === "high").length,
1328
+ medium: patterns.filter((p) => p.severity === "medium").length,
1329
+ low: patterns.filter((p) => p.severity === "low").length
1330
+ },
1331
+ by_track: patterns.reduce((acc, p) => {
1332
+ acc[p.track] = (acc[p.track] || 0) + 1;
1333
+ return acc;
1334
+ }, {})
1335
+ };
1336
+ return {
1337
+ patterns: patterns.map((p) => ({
1338
+ title: p.title,
1339
+ category: p.category,
1340
+ track: p.track,
1341
+ severity: p.severity,
1342
+ description: p.description,
1343
+ why_it_happens: p.why_it_happens,
1344
+ suggested_tests: p.suggested_tests,
1345
+ common_fix: p.common_fix,
1346
+ source: p.source,
1347
+ frameworks: p.frameworks
1348
+ })),
1349
+ summary,
1350
+ note: "Patterns are from public knowledge (OWASP, WCAG, framework docs), not customer data."
1351
+ };
1352
+ }
1353
+ async function analyzeChangesForTests(supabase, projectId, args) {
1354
+ const { data: existingTests } = await supabase.from("test_cases").select("test_key, title, target_route, description").eq("project_id", projectId);
1355
+ const { data: lastTest } = await supabase.from("test_cases").select("test_key").eq("project_id", projectId).order("test_key", { ascending: false }).limit(1);
1356
+ const lastKey = lastTest?.[0]?.test_key || "TC-000";
1357
+ const lastNum = parseInt(lastKey.replace("TC-", "")) || 0;
1358
+ const routes = args.affected_routes || [];
1359
+ let relatedBugs = [];
1360
+ if (routes.length > 0) {
1361
+ const { data: bugs } = await supabase.from("reports").select("id, description, severity, app_context").eq("project_id", projectId).eq("report_type", "bug").limit(50);
1362
+ relatedBugs = (bugs || []).filter((bug) => {
1363
+ const bugRoute = bug.app_context?.currentRoute;
1364
+ return bugRoute && routes.some((r) => bugRoute.includes(r) || r.includes(bugRoute));
1365
+ });
1366
+ }
1367
+ const fileAnalysis = analyzeFileTypes(args.changed_files);
1368
+ const existingCoverage = (existingTests || []).filter(
1369
+ (test) => routes.some((r) => test.target_route?.includes(r) || test.title.toLowerCase().includes(r.toLowerCase()))
1370
+ );
1371
+ const suggestions = [];
1372
+ let testNum = lastNum;
1373
+ if (args.change_type === "feature") {
1374
+ for (const route of routes.slice(0, 2)) {
1375
+ testNum++;
1376
+ suggestions.push({
1377
+ test_key: `TC-${String(testNum).padStart(3, "0")}`,
1378
+ title: `Verify new feature: ${args.change_summary.slice(0, 40)}`,
1379
+ description: `Test the new functionality added: ${args.change_summary}`,
1380
+ track: "functional",
1381
+ priority: "P1",
1382
+ target_route: route,
1383
+ rationale: "New features require verification that they work as intended",
1384
+ steps: [
1385
+ { stepNumber: 1, action: `Navigate to ${route}`, expectedResult: "Page loads successfully" },
1386
+ { stepNumber: 2, action: "Locate the new feature/element", expectedResult: "Feature is visible and accessible" },
1387
+ { stepNumber: 3, action: "Interact with the new feature", expectedResult: "Feature responds as expected" },
1388
+ { stepNumber: 4, action: "Verify the expected outcome", expectedResult: "Correct result is produced" }
1389
+ ],
1390
+ expected_result: "New feature functions correctly without errors"
1391
+ });
1392
+ }
1393
+ if (routes.length > 0) {
1394
+ testNum++;
1395
+ suggestions.push({
1396
+ test_key: `TC-${String(testNum).padStart(3, "0")}`,
1397
+ title: `Edge cases: ${args.change_summary.slice(0, 35)}`,
1398
+ description: `Test edge cases and error handling for the new feature`,
1399
+ track: "functional",
1400
+ priority: "P2",
1401
+ target_route: routes[0],
1402
+ rationale: "Edge cases often reveal bugs that happy-path testing misses",
1403
+ steps: [
1404
+ { stepNumber: 1, action: `Navigate to ${routes[0]}`, expectedResult: "Page loads" },
1405
+ { stepNumber: 2, action: "Test with empty/null input", expectedResult: "Graceful handling, no crash" },
1406
+ { stepNumber: 3, action: "Test with invalid input", expectedResult: "Appropriate error message" },
1407
+ { stepNumber: 4, action: "Test boundary conditions", expectedResult: "Correct behavior at limits" }
1408
+ ],
1409
+ expected_result: "Feature handles edge cases gracefully without errors"
1410
+ });
1411
+ }
1412
+ }
1413
+ if (args.change_type === "bugfix") {
1414
+ for (const route of routes.slice(0, 1)) {
1415
+ testNum++;
1416
+ suggestions.push({
1417
+ test_key: `TC-${String(testNum).padStart(3, "0")}`,
1418
+ title: `Regression: ${args.change_summary.slice(0, 40)}`,
1419
+ description: `Verify the bug fix works and hasn't regressed: ${args.change_summary}`,
1420
+ track: "functional",
1421
+ priority: "P1",
1422
+ target_route: route,
1423
+ rationale: "Bug fixes should have regression tests to prevent recurrence",
1424
+ steps: [
1425
+ { stepNumber: 1, action: `Navigate to ${route}`, expectedResult: "Page loads" },
1426
+ { stepNumber: 2, action: "Reproduce the original bug scenario", expectedResult: "Bug no longer occurs" },
1427
+ { stepNumber: 3, action: "Test related functionality", expectedResult: "No side effects from fix" }
1428
+ ],
1429
+ expected_result: "Bug is fixed and related functionality still works"
1430
+ });
1431
+ }
1432
+ }
1433
+ if (args.change_type === "ui_change") {
1434
+ for (const route of routes.slice(0, 1)) {
1435
+ testNum++;
1436
+ suggestions.push({
1437
+ test_key: `TC-${String(testNum).padStart(3, "0")}`,
1438
+ title: `UI verification: ${args.change_summary.slice(0, 35)}`,
1439
+ description: `Verify UI changes display correctly across devices`,
1440
+ track: "design",
1441
+ priority: "P2",
1442
+ target_route: route,
1443
+ rationale: "UI changes should be verified visually and for responsiveness",
1444
+ steps: [
1445
+ { stepNumber: 1, action: `Navigate to ${route}`, expectedResult: "Page loads" },
1446
+ { stepNumber: 2, action: "Verify visual appearance matches design", expectedResult: "UI looks correct" },
1447
+ { stepNumber: 3, action: "Test on mobile viewport (375px)", expectedResult: "Responsive layout works" },
1448
+ { stepNumber: 4, action: "Test interactive elements", expectedResult: "Buttons, links work correctly" }
1449
+ ],
1450
+ expected_result: "UI changes look correct and are responsive"
1451
+ });
1452
+ }
1453
+ }
1454
+ if (args.change_type === "api_change") {
1455
+ for (const route of routes.slice(0, 1)) {
1456
+ testNum++;
1457
+ suggestions.push({
1458
+ test_key: `TC-${String(testNum).padStart(3, "0")}`,
1459
+ title: `API integration: ${args.change_summary.slice(0, 35)}`,
1460
+ description: `Verify API changes work correctly end-to-end`,
1461
+ track: "functional",
1462
+ priority: "P1",
1463
+ target_route: route,
1464
+ rationale: "API changes can break frontend functionality",
1465
+ steps: [
1466
+ { stepNumber: 1, action: `Navigate to ${route}`, expectedResult: "Page loads" },
1467
+ { stepNumber: 2, action: "Trigger the API call", expectedResult: "Request is sent" },
1468
+ { stepNumber: 3, action: "Verify response handling", expectedResult: "Data displays correctly" },
1469
+ { stepNumber: 4, action: "Test error scenarios", expectedResult: "Errors handled gracefully" }
1470
+ ],
1471
+ expected_result: "API integration works correctly"
1472
+ });
1473
+ }
1474
+ }
1475
+ if (fileAnalysis.hasUIComponents && (args.change_type === "feature" || args.change_type === "ui_change")) {
1476
+ testNum++;
1477
+ suggestions.push({
1478
+ test_key: `TC-${String(testNum).padStart(3, "0")}`,
1479
+ title: `Accessibility: ${args.change_summary.slice(0, 35)}`,
1480
+ description: `Verify changes meet accessibility standards`,
1481
+ track: "accessibility",
1482
+ priority: "P2",
1483
+ target_route: routes[0] || null,
1484
+ rationale: "UI changes should maintain accessibility compliance",
1485
+ steps: [
1486
+ { stepNumber: 1, action: "Navigate using keyboard only (Tab)", expectedResult: "All interactive elements reachable" },
1487
+ { stepNumber: 2, action: "Check focus indicators", expectedResult: "Focus is visible on all elements" },
1488
+ { stepNumber: 3, action: "Verify with screen reader", expectedResult: "Content is announced correctly" }
1489
+ ],
1490
+ expected_result: "Changes are accessible to all users"
1491
+ });
1492
+ }
1493
+ const shouldCreateTests = suggestions.length > 0 && (args.change_type !== "config" && args.change_type !== "refactor");
1494
+ const coverageRatio = existingCoverage.length / Math.max(routes.length, 1);
1495
+ return {
1496
+ analysis: {
1497
+ change_type: args.change_type,
1498
+ files_changed: args.changed_files.length,
1499
+ file_types: fileAnalysis,
1500
+ affected_routes: routes,
1501
+ existing_coverage: existingCoverage.length,
1502
+ related_bugs: relatedBugs.length,
1503
+ coverage_ratio: coverageRatio
1504
+ },
1505
+ recommendation: {
1506
+ should_create_tests: shouldCreateTests,
1507
+ urgency: args.change_type === "feature" || args.change_type === "bugfix" ? "high" : args.change_type === "api_change" ? "medium" : "low",
1508
+ reason: shouldCreateTests ? `${args.change_type === "feature" ? "New features" : args.change_type === "bugfix" ? "Bug fixes" : "Changes"} should have QA coverage to catch issues early.` : args.change_type === "config" ? "Config changes typically don't need manual QA tests." : "Refactoring with good existing coverage may not need new tests."
1509
+ },
1510
+ suggestions: suggestions.map((s) => ({
1511
+ ...s,
1512
+ create_command: `create_test_case with test_key="${s.test_key}", title="${s.title}", target_route="${s.target_route}"`
1513
+ })),
1514
+ existing_tests: existingCoverage.slice(0, 5).map((t) => ({
1515
+ test_key: t.test_key,
1516
+ title: t.title,
1517
+ target_route: t.target_route
1518
+ })),
1519
+ next_steps: shouldCreateTests ? [
1520
+ "Review the suggested tests above",
1521
+ "Modify titles/steps as needed for your specific implementation",
1522
+ "Use create_test_case to add the ones that make sense",
1523
+ "Skip any that duplicate existing coverage"
1524
+ ] : ["No new tests recommended for this change type"]
1525
+ };
1526
+ }
1527
+
1528
+ // src/bugs.ts
1529
+ async function createBugReport(supabase, projectId, args) {
1530
+ const codeContext = {};
1531
+ if (args.file_path) {
1532
+ codeContext.file_path = args.file_path;
1533
+ codeContext.line_number = args.line_number;
1534
+ codeContext.code_snippet = args.code_snippet;
1535
+ codeContext.function_name = extractFunctionName(args.code_snippet);
1536
+ }
1537
+ if (args.related_files) codeContext.related_files = args.related_files;
1538
+ if (args.suggested_fix) codeContext.suggested_fix = args.suggested_fix;
1539
+ let reporterId = null;
1540
+ const { data: project } = await supabase.from("projects").select("owner_id").eq("id", projectId).single();
1541
+ if (project?.owner_id) {
1542
+ reporterId = project.owner_id;
1543
+ } else {
1544
+ const { data: testers } = await supabase.from("testers").select("id").eq("project_id", projectId).limit(1);
1545
+ if (testers && testers.length > 0) reporterId = testers[0].id;
1546
+ }
1547
+ const report = {
1548
+ project_id: projectId,
1549
+ report_type: "bug",
1550
+ title: args.title,
1551
+ description: args.description,
1552
+ severity: args.severity,
1553
+ status: "new",
1554
+ app_context: {
1555
+ currentRoute: args.file_path || "code",
1556
+ source: "claude_code",
1557
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1558
+ },
1559
+ device_info: {
1560
+ platform: "claude_code",
1561
+ environment: "development"
1562
+ },
1563
+ code_context: codeContext
1564
+ };
1565
+ if (reporterId) report.reporter_id = reporterId;
1566
+ const { data, error } = await supabase.from("reports").insert(report).select("id").single();
1567
+ if (error) return { error: error.message };
1568
+ return {
1569
+ success: true,
1570
+ report_id: data.id,
1571
+ message: `Bug report created: ${args.title}`,
1572
+ details: { id: data.id, severity: args.severity, file: args.file_path, line: args.line_number }
1573
+ };
1574
+ }
1575
+ async function getBugsForFile(supabase, projectId, args) {
1576
+ const normalizedPath = args.file_path.replace(/^\.\//, "").replace(/^\//, "");
1577
+ let query = supabase.from("reports").select("id, title, description, severity, status, created_at, code_context").eq("project_id", projectId).eq("report_type", "bug");
1578
+ if (!args.include_resolved) {
1579
+ query = query.in("status", ["new", "confirmed", "in_progress", "reviewed"]);
1580
+ }
1581
+ const { data, error } = await query.order("created_at", { ascending: false });
1582
+ if (error) return { error: error.message };
1583
+ const matchingBugs = (data || []).filter((bug) => {
1584
+ const codeContext = bug.code_context;
1585
+ if (!codeContext) return false;
1586
+ const bugFilePath = codeContext.file_path;
1587
+ if (!bugFilePath) return false;
1588
+ const normalizedBugPath = bugFilePath.replace(/^\.\//, "").replace(/^\//, "");
1589
+ return normalizedBugPath.includes(normalizedPath) || normalizedPath.includes(normalizedBugPath);
1590
+ });
1591
+ const relatedBugs = (data || []).filter((bug) => {
1592
+ if (matchingBugs.includes(bug)) return false;
1593
+ const codeContext = bug.code_context;
1594
+ const relatedFiles = codeContext?.related_files;
1595
+ if (!relatedFiles) return false;
1596
+ return relatedFiles.some((f) => f.includes(normalizedPath) || normalizedPath.includes(f));
1597
+ });
1598
+ return {
1599
+ file: args.file_path,
1600
+ direct_bugs: matchingBugs.map((b) => ({
1601
+ id: b.id,
1602
+ title: b.title,
1603
+ severity: b.severity,
1604
+ status: b.status,
1605
+ line: b.code_context?.line_number,
1606
+ description: b.description.slice(0, 200)
1607
+ })),
1608
+ related_bugs: relatedBugs.map((b) => ({
1609
+ id: b.id,
1610
+ title: b.title,
1611
+ severity: b.severity,
1612
+ status: b.status,
1613
+ source_file: b.code_context?.file_path
1614
+ })),
1615
+ summary: {
1616
+ total: matchingBugs.length + relatedBugs.length,
1617
+ critical: matchingBugs.filter((b) => b.severity === "critical").length,
1618
+ open: matchingBugs.filter((b) => ["new", "confirmed", "in_progress"].includes(b.status)).length
1619
+ },
1620
+ recommendation: matchingBugs.length > 0 ? `Found ${matchingBugs.length} bug(s) in this file. Consider fixing them while you're here.` : "No known bugs in this file."
1621
+ };
1622
+ }
1623
+ async function markFixedWithCommit(supabase, projectId, args) {
1624
+ if (!isValidUUID(args.report_id)) return { error: "Invalid report_id format" };
1625
+ const { data: existing, error: fetchError } = await supabase.from("reports").select("code_context").eq("id", args.report_id).eq("project_id", projectId).single();
1626
+ if (fetchError) return { error: fetchError.message };
1627
+ const existingContext = existing?.code_context || {};
1628
+ const updates = {
1629
+ status: "resolved",
1630
+ resolved_at: (/* @__PURE__ */ new Date()).toISOString(),
1631
+ resolution_notes: args.resolution_notes || `Fixed in commit ${args.commit_sha.slice(0, 7)}`,
1632
+ notify_tester: args.notify_tester === true,
1633
+ code_context: {
1634
+ ...existingContext,
1635
+ fix: {
1636
+ commit_sha: args.commit_sha,
1637
+ commit_message: args.commit_message,
1638
+ files_changed: args.files_changed,
1639
+ fixed_at: (/* @__PURE__ */ new Date()).toISOString(),
1640
+ fixed_by: "claude_code"
1641
+ }
1642
+ }
1643
+ };
1644
+ const { error } = await supabase.from("reports").update(updates).eq("id", args.report_id).eq("project_id", projectId);
1645
+ if (error) return { error: error.message };
1646
+ const notificationStatus = args.notify_tester ? "The original tester will be notified and assigned a verification task." : "No notification sent (silent resolve). A verification task was created.";
1647
+ return {
1648
+ success: true,
1649
+ message: `Bug marked as fixed in commit ${args.commit_sha.slice(0, 7)}. ${notificationStatus}`,
1650
+ report_id: args.report_id,
1651
+ commit: args.commit_sha,
1652
+ tester_notified: args.notify_tester === true,
1653
+ next_steps: [
1654
+ "Consider running create_regression_test to prevent this bug from recurring",
1655
+ "Push your changes to trigger CI/CD"
1656
+ ]
1657
+ };
1658
+ }
1659
+ async function getBugsAffectingCode(supabase, projectId, args) {
1660
+ const includeRelated = args.include_related !== false;
1661
+ const { data, error } = await supabase.from("reports").select("id, title, description, severity, status, code_context, app_context").eq("project_id", projectId).eq("report_type", "bug").in("status", ["new", "confirmed", "in_progress", "reviewed"]).order("severity", { ascending: true });
1662
+ if (error) return { error: error.message };
1663
+ const normalizedPaths = args.file_paths.map((p) => p.replace(/^\.\//, "").replace(/^\//, ""));
1664
+ const affectedBugs = [];
1665
+ for (const bug of data || []) {
1666
+ const codeContext = bug.code_context;
1667
+ const appContext = bug.app_context;
1668
+ const bugFile = codeContext?.file_path;
1669
+ if (bugFile) {
1670
+ const normalizedBugFile = bugFile.replace(/^\.\//, "").replace(/^\//, "");
1671
+ for (const path of normalizedPaths) {
1672
+ if (normalizedBugFile.includes(path) || path.includes(normalizedBugFile)) {
1673
+ affectedBugs.push({ id: bug.id, title: bug.title, severity: bug.severity, status: bug.status, matched_file: path, match_type: "direct" });
1674
+ break;
1675
+ }
1676
+ }
1677
+ }
1678
+ if (includeRelated && codeContext?.related_files) {
1679
+ const relatedFiles = codeContext.related_files;
1680
+ for (const relatedFile of relatedFiles) {
1681
+ for (const path of normalizedPaths) {
1682
+ if (relatedFile.includes(path) || path.includes(relatedFile)) {
1683
+ if (!affectedBugs.find((b) => b.id === bug.id)) {
1684
+ affectedBugs.push({ id: bug.id, title: bug.title, severity: bug.severity, status: bug.status, matched_file: path, match_type: "related" });
1685
+ }
1686
+ break;
1687
+ }
1688
+ }
1689
+ }
1690
+ }
1691
+ const route = appContext?.currentRoute;
1692
+ if (route && route !== "code") {
1693
+ for (const path of normalizedPaths) {
1694
+ if (path.includes("page") || path.includes("route") || path.includes("component")) {
1695
+ const pathParts = path.split("/");
1696
+ const fileName = pathParts[pathParts.length - 1].replace(/\.(tsx?|jsx?)$/, "");
1697
+ if (route.toLowerCase().includes(fileName.toLowerCase())) {
1698
+ if (!affectedBugs.find((b) => b.id === bug.id)) {
1699
+ affectedBugs.push({ id: bug.id, title: bug.title, severity: bug.severity, status: bug.status, matched_file: path, match_type: "route" });
1700
+ }
1701
+ }
1702
+ }
1703
+ }
1704
+ }
1705
+ }
1706
+ const critical = affectedBugs.filter((b) => b.severity === "critical");
1707
+ return {
1708
+ files_checked: args.file_paths,
1709
+ affected_bugs: affectedBugs,
1710
+ summary: {
1711
+ total: affectedBugs.length,
1712
+ critical: critical.length,
1713
+ high: affectedBugs.filter((b) => b.severity === "high").length,
1714
+ direct_matches: affectedBugs.filter((b) => b.match_type === "direct").length
1715
+ },
1716
+ warnings: critical.length > 0 ? [`${critical.length} CRITICAL bug(s) may be affected by your changes!`] : [],
1717
+ recommendation: affectedBugs.length > 0 ? `Review ${affectedBugs.length} potentially affected bug(s) before pushing.` : "No known bugs affected by these changes."
1718
+ };
1719
+ }
1720
+ async function linkBugToCode(supabase, projectId, args) {
1721
+ if (!isValidUUID(args.report_id)) return { error: "Invalid report_id format" };
1722
+ const { data: existing, error: fetchError } = await supabase.from("reports").select("code_context").eq("id", args.report_id).eq("project_id", projectId).single();
1723
+ if (fetchError) return { error: fetchError.message };
1724
+ const existingContext = existing?.code_context || {};
1725
+ const { error } = await supabase.from("reports").update({
1726
+ code_context: {
1727
+ ...existingContext,
1728
+ file_path: args.file_path,
1729
+ line_number: args.line_number,
1730
+ code_snippet: args.code_snippet,
1731
+ function_name: args.function_name || extractFunctionName(args.code_snippet),
1732
+ linked_at: (/* @__PURE__ */ new Date()).toISOString(),
1733
+ linked_by: "claude_code"
1734
+ }
1735
+ }).eq("id", args.report_id).eq("project_id", projectId);
1736
+ if (error) return { error: error.message };
1737
+ return {
1738
+ success: true,
1739
+ message: `Bug linked to ${args.file_path}${args.line_number ? `:${args.line_number}` : ""}`,
1740
+ report_id: args.report_id
1741
+ };
1742
+ }
1743
+ async function createRegressionTest(supabase, projectId, args) {
1744
+ if (!isValidUUID(args.report_id)) return { error: "Invalid report_id format" };
1745
+ const { data: report, error: fetchError } = await supabase.from("reports").select("*").eq("id", args.report_id).eq("project_id", projectId).single();
1746
+ if (fetchError) return { error: fetchError.message };
1747
+ if (report.status !== "resolved") {
1748
+ return { error: "Bug must be resolved before creating a regression test", current_status: report.status };
1749
+ }
1750
+ const codeContext = report.code_context;
1751
+ const testType = args.test_type || "integration";
1752
+ const { data: existingTests } = await supabase.from("test_cases").select("test_key").eq("project_id", projectId).order("test_key", { ascending: false }).limit(1);
1753
+ const lastKey = existingTests?.[0]?.test_key || "TC-000";
1754
+ const lastNum = parseInt(lastKey.replace("TC-", "")) || 0;
1755
+ const newKey = `TC-${String(lastNum + 1).padStart(3, "0")}`;
1756
+ const appContext = report.app_context;
1757
+ const targetRoute = appContext?.currentRoute;
1758
+ const testCase = {
1759
+ project_id: projectId,
1760
+ test_key: newKey,
1761
+ title: `Regression: ${report.title}`,
1762
+ description: `Regression test to prevent recurrence of bug #${args.report_id.slice(0, 8)}
1763
+
1764
+ Original bug: ${report.description}`,
1765
+ priority: report.severity === "critical" ? "P0" : report.severity === "high" ? "P1" : "P2",
1766
+ steps: [
1767
+ {
1768
+ stepNumber: 1,
1769
+ action: codeContext?.file_path ? `Navigate to the code/feature in ${codeContext.file_path}` : "Navigate to the affected feature",
1770
+ expectedResult: "Feature loads correctly"
1771
+ },
1772
+ { stepNumber: 2, action: "Reproduce the original bug scenario", expectedResult: "The bug should NOT occur" },
1773
+ { stepNumber: 3, action: "Verify the fix is working", expectedResult: report.resolution_notes || "Feature works as expected" }
1774
+ ],
1775
+ expected_result: `The bug "${report.title}" should not recur`,
1776
+ preconditions: codeContext?.fix ? `Requires commit ${codeContext.fix.commit_sha?.slice(0, 7) || "unknown"} or later` : "",
1777
+ target_route: targetRoute,
1778
+ metadata: {
1779
+ source: "regression_from_bug",
1780
+ original_bug_id: args.report_id,
1781
+ test_type: testType,
1782
+ created_by: "claude_code"
1783
+ }
1784
+ };
1785
+ const { data, error } = await supabase.from("test_cases").insert(testCase).select("id, test_key, title").single();
1786
+ if (error) return { error: error.message };
1787
+ return {
1788
+ success: true,
1789
+ test_case: { id: data.id, test_key: data.test_key, title: data.title, type: testType },
1790
+ message: `Regression test ${data.test_key} created from bug report`,
1791
+ original_bug: { id: args.report_id, title: report.title }
1792
+ };
1793
+ }
1794
+
1795
+ // src/fixes.ts
1796
+ async function getPendingFixes(supabase, projectId, args) {
1797
+ const limit = args.limit || 10;
1798
+ let query = supabase.from("fix_requests").select(`
1799
+ id, title, description, prompt, file_path, status, claimed_at, claimed_by, created_at,
1800
+ report:reports(id, title, severity, description)
1801
+ `).eq("project_id", projectId).order("created_at", { ascending: true }).limit(limit);
1802
+ if (!args.include_claimed) {
1803
+ query = query.eq("status", "pending");
1804
+ } else {
1805
+ query = query.in("status", ["pending", "claimed"]);
1806
+ }
1807
+ const { data, error } = await query;
1808
+ if (error) return { error: error.message };
1809
+ if (!data || data.length === 0) {
1810
+ return {
1811
+ fix_requests: [],
1812
+ count: 0,
1813
+ message: "No pending fix requests in the queue. Great job keeping up!"
1814
+ };
1815
+ }
1816
+ return {
1817
+ fix_requests: data.map((fr) => ({
1818
+ id: fr.id,
1819
+ title: fr.title,
1820
+ description: fr.description,
1821
+ prompt: fr.prompt,
1822
+ file_path: fr.file_path,
1823
+ status: fr.status,
1824
+ claimed_by: fr.claimed_by,
1825
+ claimed_at: fr.claimed_at,
1826
+ created_at: fr.created_at,
1827
+ related_report: fr.report ? {
1828
+ id: fr.report.id,
1829
+ title: fr.report.title,
1830
+ severity: fr.report.severity
1831
+ } : null
1832
+ })),
1833
+ count: data.length,
1834
+ message: `Found ${data.length} fix request(s) waiting. Use claim_fix_request to start working on one.`
1835
+ };
1836
+ }
1837
+ async function claimFixRequest(supabase, projectId, args) {
1838
+ if (!isValidUUID(args.fix_request_id)) return { error: "Invalid fix_request_id format" };
1839
+ const { data: existing, error: checkError } = await supabase.from("fix_requests").select("id, status, claimed_by, prompt, title").eq("id", args.fix_request_id).eq("project_id", projectId).single();
1840
+ if (checkError) return { error: checkError.message };
1841
+ if (existing.status === "claimed") {
1842
+ return { error: `This fix request is already claimed by ${existing.claimed_by || "another instance"}`, status: existing.status };
1843
+ }
1844
+ if (existing.status === "completed") {
1845
+ return { error: "This fix request has already been completed", status: existing.status };
1846
+ }
1847
+ const claimedBy = args.claimed_by || `claude-code-${Date.now()}`;
1848
+ const { error: updateError } = await supabase.from("fix_requests").update({
1849
+ status: "claimed",
1850
+ claimed_at: (/* @__PURE__ */ new Date()).toISOString(),
1851
+ claimed_by: claimedBy
1852
+ }).eq("id", args.fix_request_id).eq("project_id", projectId).eq("status", "pending");
1853
+ if (updateError) return { error: updateError.message };
1854
+ return {
1855
+ success: true,
1856
+ message: `Fix request claimed successfully. Here's your task:`,
1857
+ fix_request: { id: args.fix_request_id, title: existing.title, prompt: existing.prompt },
1858
+ next_steps: [
1859
+ "1. Read and understand the prompt below",
1860
+ "2. Implement the fix",
1861
+ "3. Test your changes",
1862
+ "4. Use complete_fix_request when done"
1863
+ ]
1864
+ };
1865
+ }
1866
+ async function completeFixRequest(supabase, projectId, args) {
1867
+ if (!isValidUUID(args.fix_request_id)) return { error: "Invalid fix_request_id format" };
1868
+ const isSuccess = args.success !== false;
1869
+ const updates = {
1870
+ status: isSuccess ? "completed" : "cancelled",
1871
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
1872
+ completion_notes: args.completion_notes || (isSuccess ? "Fix completed" : "Could not complete fix")
1873
+ };
1874
+ const { error } = await supabase.from("fix_requests").update(updates).eq("id", args.fix_request_id).eq("project_id", projectId);
1875
+ if (error) return { error: error.message };
1876
+ return {
1877
+ success: true,
1878
+ message: isSuccess ? "Fix request marked as completed!" : "Fix request marked as cancelled.",
1879
+ fix_request_id: args.fix_request_id,
1880
+ status: updates.status
1881
+ };
1882
+ }
1883
+
1884
+ // src/team.ts
1885
+ async function listTesters(supabase, projectId, args) {
1886
+ let query = supabase.from("testers").select("id, name, email, status, platforms, tier, assigned_count, completed_count, notes, created_at").eq("project_id", projectId).order("name", { ascending: true });
1887
+ if (args.status) query = query.eq("status", args.status);
1888
+ const { data, error } = await query;
1889
+ if (error) return { error: error.message };
1890
+ let testers = data || [];
1891
+ if (args.platform) {
1892
+ testers = testers.filter((t) => t.platforms && t.platforms.includes(args.platform));
1893
+ }
1894
+ return {
1895
+ count: testers.length,
1896
+ testers: testers.map((t) => ({
1897
+ id: t.id,
1898
+ name: t.name,
1899
+ email: t.email,
1900
+ status: t.status,
1901
+ platforms: t.platforms,
1902
+ tier: t.tier,
1903
+ assignedCount: t.assigned_count,
1904
+ completedCount: t.completed_count,
1905
+ notes: t.notes
1906
+ }))
1907
+ };
1908
+ }
1909
+ async function createTester(supabase, projectId, args) {
1910
+ if (!args.name || args.name.trim().length === 0) return { error: "Tester name is required" };
1911
+ if (!args.email || !args.email.includes("@")) return { error: "A valid email address is required" };
1912
+ if (args.tier !== void 0 && (args.tier < 1 || args.tier > 3)) return { error: "Tier must be 1, 2, or 3" };
1913
+ const validPlatforms = ["ios", "android", "web"];
1914
+ if (args.platforms) {
1915
+ for (const p of args.platforms) {
1916
+ if (!validPlatforms.includes(p)) {
1917
+ return { error: `Invalid platform "${p}". Must be one of: ${validPlatforms.join(", ")}` };
1918
+ }
1919
+ }
1920
+ }
1921
+ const { data, error } = await supabase.from("testers").insert({
1922
+ project_id: projectId,
1923
+ name: args.name.trim(),
1924
+ email: args.email.trim().toLowerCase(),
1925
+ platforms: args.platforms || ["ios", "web"],
1926
+ tier: args.tier ?? 1,
1927
+ notes: args.notes?.trim() || null,
1928
+ status: "active"
1929
+ }).select("id, name, email, status, platforms, tier, notes, created_at").single();
1930
+ if (error) {
1931
+ if (error.message.includes("duplicate") || error.message.includes("unique")) {
1932
+ return { error: `A tester with email "${args.email}" already exists in this project` };
1933
+ }
1934
+ return { error: error.message };
1935
+ }
1936
+ return {
1937
+ success: true,
1938
+ tester: {
1939
+ id: data.id,
1940
+ name: data.name,
1941
+ email: data.email,
1942
+ status: data.status,
1943
+ platforms: data.platforms,
1944
+ tier: data.tier,
1945
+ notes: data.notes,
1946
+ createdAt: data.created_at
1947
+ },
1948
+ message: `Tester "${data.name}" added to the project. Use assign_tests to give them test cases.`
1949
+ };
1950
+ }
1951
+ async function updateTester(supabase, projectId, args) {
1952
+ if (!isValidUUID(args.tester_id)) return { error: "Invalid tester_id format" };
1953
+ const updates = {};
1954
+ if (args.status) updates.status = args.status;
1955
+ if (args.platforms) updates.platforms = args.platforms;
1956
+ if (args.tier !== void 0) {
1957
+ if (args.tier < 1 || args.tier > 3) return { error: "Tier must be 1, 2, or 3" };
1958
+ updates.tier = args.tier;
1959
+ }
1960
+ if (args.notes !== void 0) updates.notes = args.notes.trim() || null;
1961
+ if (args.name) updates.name = args.name.trim();
1962
+ if (Object.keys(updates).length === 0) {
1963
+ return { error: "No fields to update. Provide at least one of: status, platforms, tier, notes, name" };
1964
+ }
1965
+ const { data, error } = await supabase.from("testers").update(updates).eq("id", args.tester_id).eq("project_id", projectId).select("id, name, email, status, platforms, tier, notes").single();
1966
+ if (error) return { error: error.message };
1967
+ if (!data) return { error: "Tester not found in this project" };
1968
+ return {
1969
+ success: true,
1970
+ tester: {
1971
+ id: data.id,
1972
+ name: data.name,
1973
+ email: data.email,
1974
+ status: data.status,
1975
+ platforms: data.platforms,
1976
+ tier: data.tier,
1977
+ notes: data.notes
1978
+ },
1979
+ updatedFields: Object.keys(updates)
1980
+ };
1981
+ }
1982
+ async function getTesterWorkload(supabase, projectId, args) {
1983
+ if (!isValidUUID(args.tester_id)) return { error: "Invalid tester_id format" };
1984
+ const { data: tester, error: testerErr } = await supabase.from("testers").select("id, name, email, status, platforms, tier").eq("id", args.tester_id).eq("project_id", projectId).single();
1985
+ if (testerErr || !tester) return { error: "Tester not found in this project" };
1986
+ const { data: assignments, error: assignErr } = await supabase.from("test_assignments").select(`
1987
+ id, status, assigned_at, completed_at,
1988
+ test_case:test_cases(test_key, title, priority),
1989
+ test_run:test_runs(name)
1990
+ `).eq("project_id", projectId).eq("tester_id", args.tester_id).order("assigned_at", { ascending: false });
1991
+ if (assignErr) return { error: assignErr.message };
1992
+ const all = assignments || [];
1993
+ const counts = {
1994
+ pending: 0,
1995
+ in_progress: 0,
1996
+ passed: 0,
1997
+ failed: 0,
1998
+ blocked: 0,
1999
+ skipped: 0
2000
+ };
2001
+ for (const a of all) counts[a.status] = (counts[a.status] || 0) + 1;
2002
+ return {
2003
+ tester: {
2004
+ id: tester.id,
2005
+ name: tester.name,
2006
+ email: tester.email,
2007
+ status: tester.status,
2008
+ platforms: tester.platforms,
2009
+ tier: tester.tier
2010
+ },
2011
+ totalAssignments: all.length,
2012
+ counts,
2013
+ activeLoad: counts.pending + counts.in_progress,
2014
+ recentAssignments: all.slice(0, 10).map((a) => ({
2015
+ id: a.id,
2016
+ status: a.status,
2017
+ assignedAt: a.assigned_at,
2018
+ completedAt: a.completed_at,
2019
+ testCase: a.test_case ? { testKey: a.test_case.test_key, title: a.test_case.title, priority: a.test_case.priority } : null,
2020
+ testRun: a.test_run?.name || null
2021
+ }))
2022
+ };
2023
+ }
2024
+
2025
+ // src/analytics.ts
2026
+ async function getBugTrends(supabase, projectId, args) {
2027
+ const days = Math.min(args.days || 30, 180);
2028
+ const groupBy = args.group_by || "week";
2029
+ const since = new Date(Date.now() - days * 864e5).toISOString();
2030
+ const { data, error } = await supabase.from("reports").select("id, severity, category, status, report_type, created_at").eq("project_id", projectId).gte("created_at", since).order("created_at", { ascending: true });
2031
+ if (error) return { error: error.message };
2032
+ const reports = data || [];
2033
+ if (groupBy === "week") {
2034
+ const weeks = {};
2035
+ for (const r of reports) {
2036
+ const d = new Date(r.created_at);
2037
+ const day = d.getDay();
2038
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1);
2039
+ const monday = new Date(d.setDate(diff));
2040
+ const weekKey = monday.toISOString().slice(0, 10);
2041
+ if (!weeks[weekKey]) weeks[weekKey] = { count: 0, critical: 0, high: 0, medium: 0, low: 0 };
2042
+ weeks[weekKey].count++;
2043
+ const sev = r.severity || "low";
2044
+ weeks[weekKey][sev]++;
2045
+ }
2046
+ return {
2047
+ period: `${days} days`,
2048
+ groupBy: "week",
2049
+ totalReports: reports.length,
2050
+ weeks: Object.entries(weeks).map(([week, data2]) => ({ week, ...data2 }))
2051
+ };
2052
+ }
2053
+ if (groupBy === "severity") {
2054
+ const groups = { critical: 0, high: 0, medium: 0, low: 0 };
2055
+ for (const r of reports) groups[r.severity || "low"]++;
2056
+ return { period: `${days} days`, groupBy: "severity", totalReports: reports.length, breakdown: groups };
2057
+ }
2058
+ if (groupBy === "category") {
2059
+ const groups = {};
2060
+ for (const r of reports) {
2061
+ const cat = r.category || "uncategorized";
2062
+ groups[cat] = (groups[cat] || 0) + 1;
2063
+ }
2064
+ return { period: `${days} days`, groupBy: "category", totalReports: reports.length, breakdown: groups };
2065
+ }
2066
+ if (groupBy === "status") {
2067
+ const groups = {};
2068
+ for (const r of reports) groups[r.status] = (groups[r.status] || 0) + 1;
2069
+ return { period: `${days} days`, groupBy: "status", totalReports: reports.length, breakdown: groups };
2070
+ }
2071
+ return { error: `Invalid group_by: ${groupBy}. Must be one of: week, severity, category, status` };
2072
+ }
2073
+ async function getTesterLeaderboard(supabase, projectId, args) {
2074
+ const days = Math.min(args.days || 30, 180);
2075
+ const sortBy = args.sort_by || "tests_completed";
2076
+ const since = new Date(Date.now() - days * 864e5).toISOString();
2077
+ const { data: testers, error: testerErr } = await supabase.from("testers").select("id, name, email, status, platforms, tier").eq("project_id", projectId).eq("status", "active");
2078
+ if (testerErr) return { error: testerErr.message };
2079
+ const { data: assignments, error: assignErr } = await supabase.from("test_assignments").select("tester_id, status, completed_at, duration_seconds").eq("project_id", projectId).gte("completed_at", since).in("status", ["passed", "failed"]);
2080
+ if (assignErr) return { error: assignErr.message };
2081
+ const { data: bugs, error: bugErr } = await supabase.from("reports").select("tester_id, severity").eq("project_id", projectId).gte("created_at", since).not("tester_id", "is", null);
2082
+ if (bugErr) return { error: bugErr.message };
2083
+ const testerMap = /* @__PURE__ */ new Map();
2084
+ for (const t of testers || []) {
2085
+ testerMap.set(t.id, {
2086
+ id: t.id,
2087
+ name: t.name,
2088
+ email: t.email,
2089
+ tier: t.tier,
2090
+ testsCompleted: 0,
2091
+ testsPassed: 0,
2092
+ testsFailed: 0,
2093
+ bugsFound: 0,
2094
+ criticalBugs: 0,
2095
+ avgDurationSeconds: 0,
2096
+ totalDuration: 0
2097
+ });
2098
+ }
2099
+ for (const a of assignments || []) {
2100
+ const entry = testerMap.get(a.tester_id);
2101
+ if (!entry) continue;
2102
+ entry.testsCompleted++;
2103
+ if (a.status === "passed") entry.testsPassed++;
2104
+ if (a.status === "failed") entry.testsFailed++;
2105
+ if (a.duration_seconds) entry.totalDuration += a.duration_seconds;
2106
+ }
2107
+ for (const b of bugs || []) {
2108
+ const entry = testerMap.get(b.tester_id);
2109
+ if (!entry) continue;
2110
+ entry.bugsFound++;
2111
+ if (b.severity === "critical") entry.criticalBugs++;
2112
+ }
2113
+ let leaderboard = Array.from(testerMap.values()).map((t) => ({
2114
+ ...t,
2115
+ passRate: t.testsCompleted > 0 ? Math.round(t.testsPassed / t.testsCompleted * 100) : 0,
2116
+ avgDurationSeconds: t.testsCompleted > 0 ? Math.round(t.totalDuration / t.testsCompleted) : 0,
2117
+ totalDuration: void 0
2118
+ }));
2119
+ if (sortBy === "bugs_found") leaderboard.sort((a, b) => b.bugsFound - a.bugsFound);
2120
+ else if (sortBy === "pass_rate") leaderboard.sort((a, b) => b.passRate - a.passRate);
2121
+ else leaderboard.sort((a, b) => b.testsCompleted - a.testsCompleted);
2122
+ return { period: `${days} days`, sortedBy: sortBy, leaderboard };
2123
+ }
2124
+ async function exportTestResults(supabase, projectId, args) {
2125
+ if (!isValidUUID(args.test_run_id)) return { error: "Invalid test_run_id format" };
2126
+ const { data: run, error: runErr } = await supabase.from("test_runs").select("id, name, description, status, total_tests, passed_tests, failed_tests, started_at, completed_at, created_at").eq("id", args.test_run_id).eq("project_id", projectId).single();
2127
+ if (runErr || !run) return { error: "Test run not found in this project" };
2128
+ const { data: assignments, error: assignErr } = await supabase.from("test_assignments").select(`
2129
+ id, status, assigned_at, started_at, completed_at, duration_seconds,
2130
+ is_verification, notes, skip_reason, test_result, feedback_rating, feedback_note,
2131
+ test_case:test_cases(id, test_key, title, priority, description, target_route),
2132
+ tester:testers(id, name, email)
2133
+ `).eq("test_run_id", args.test_run_id).eq("project_id", projectId).order("assigned_at", { ascending: true });
2134
+ if (assignErr) return { error: assignErr.message };
2135
+ const all = assignments || [];
2136
+ const passCount = all.filter((a) => a.status === "passed").length;
2137
+ const failCount = all.filter((a) => a.status === "failed").length;
2138
+ return {
2139
+ testRun: {
2140
+ id: run.id,
2141
+ name: run.name,
2142
+ description: run.description,
2143
+ status: run.status,
2144
+ startedAt: run.started_at,
2145
+ completedAt: run.completed_at,
2146
+ createdAt: run.created_at
2147
+ },
2148
+ summary: {
2149
+ totalAssignments: all.length,
2150
+ passed: passCount,
2151
+ failed: failCount,
2152
+ blocked: all.filter((a) => a.status === "blocked").length,
2153
+ skipped: all.filter((a) => a.status === "skipped").length,
2154
+ pending: all.filter((a) => a.status === "pending").length,
2155
+ inProgress: all.filter((a) => a.status === "in_progress").length,
2156
+ passRate: all.length > 0 ? Math.round(passCount / all.length * 100) : 0
2157
+ },
2158
+ assignments: all.map((a) => ({
2159
+ id: a.id,
2160
+ status: a.status,
2161
+ assignedAt: a.assigned_at,
2162
+ startedAt: a.started_at,
2163
+ completedAt: a.completed_at,
2164
+ durationSeconds: a.duration_seconds,
2165
+ isVerification: a.is_verification,
2166
+ notes: a.notes,
2167
+ skipReason: a.skip_reason,
2168
+ testResult: a.test_result,
2169
+ feedbackRating: a.feedback_rating,
2170
+ feedbackNote: a.feedback_note,
2171
+ testCase: a.test_case ? {
2172
+ id: a.test_case.id,
2173
+ testKey: a.test_case.test_key,
2174
+ title: a.test_case.title,
2175
+ priority: a.test_case.priority,
2176
+ description: a.test_case.description,
2177
+ targetRoute: a.test_case.target_route
2178
+ } : null,
2179
+ tester: a.tester ? { id: a.tester.id, name: a.tester.name, email: a.tester.email } : null
2180
+ }))
2181
+ };
2182
+ }
2183
+ async function getTestingVelocity(supabase, projectId, args) {
2184
+ const days = Math.min(args.days || 14, 90);
2185
+ const since = new Date(Date.now() - days * 864e5).toISOString();
2186
+ const { data, error } = await supabase.from("test_assignments").select("completed_at, status").eq("project_id", projectId).gte("completed_at", since).in("status", ["passed", "failed"]).order("completed_at", { ascending: true });
2187
+ if (error) return { error: error.message };
2188
+ const completions = data || [];
2189
+ const dailyCounts = {};
2190
+ for (let i = 0; i < days; i++) {
2191
+ const d = new Date(Date.now() - (days - 1 - i) * 864e5);
2192
+ dailyCounts[d.toISOString().slice(0, 10)] = 0;
2193
+ }
2194
+ for (const c of completions) {
2195
+ const day = new Date(c.completed_at).toISOString().slice(0, 10);
2196
+ if (dailyCounts[day] !== void 0) dailyCounts[day]++;
2197
+ }
2198
+ const dailyArray = Object.entries(dailyCounts).map(([date, count]) => ({ date, count }));
2199
+ const totalCompleted = completions.length;
2200
+ const avgPerDay = days > 0 ? Math.round(totalCompleted / days * 10) / 10 : 0;
2201
+ const mid = Math.floor(dailyArray.length / 2);
2202
+ const firstHalf = dailyArray.slice(0, mid).reduce((sum, d) => sum + d.count, 0);
2203
+ const secondHalf = dailyArray.slice(mid).reduce((sum, d) => sum + d.count, 0);
2204
+ const trend = secondHalf > firstHalf ? "increasing" : secondHalf < firstHalf ? "decreasing" : "stable";
2205
+ return { period: `${days} days`, totalCompleted, averagePerDay: avgPerDay, trend, daily: dailyArray };
2206
+ }
2207
+
2208
+ // src/index.ts
2209
+ var BugBearAdmin = class {
2210
+ supabase;
2211
+ projectId;
2212
+ constructor(opts) {
2213
+ this.supabase = createClient(opts.supabaseUrl, opts.supabaseKey);
2214
+ this.projectId = opts.projectId;
2215
+ }
2216
+ /** Switch the active project. */
2217
+ setProjectId(projectId) {
2218
+ this.projectId = projectId;
2219
+ }
2220
+ /** Get the current project ID. */
2221
+ getProjectId() {
2222
+ return this.projectId;
2223
+ }
2224
+ // ── Reports ──────────────────────────────────────────
2225
+ listReports(args) {
2226
+ return listReports(this.supabase, this.projectId, args);
2227
+ }
2228
+ getReport(args) {
2229
+ return getReport(this.supabase, this.projectId, args);
2230
+ }
2231
+ searchReports(args) {
2232
+ return searchReports(this.supabase, this.projectId, args);
2233
+ }
2234
+ updateReportStatus(args) {
2235
+ return updateReportStatus(this.supabase, this.projectId, args);
2236
+ }
2237
+ getReportContext(args) {
2238
+ return getReportContext(this.supabase, this.projectId, args);
2239
+ }
2240
+ bulkUpdateReports(args) {
2241
+ return bulkUpdateReports(this.supabase, this.projectId, args);
2242
+ }
2243
+ // ── Project ──────────────────────────────────────────
2244
+ getProjectInfo() {
2245
+ return getProjectInfo(this.supabase, this.projectId);
2246
+ }
2247
+ getQaTracks() {
2248
+ return getQaTracks(this.supabase, this.projectId);
2249
+ }
2250
+ // ── Tests ────────────────────────────────────────────
2251
+ createTestCase(args) {
2252
+ return createTestCase(this.supabase, this.projectId, args);
2253
+ }
2254
+ updateTestCase(args) {
2255
+ return updateTestCase(this.supabase, this.projectId, args);
2256
+ }
2257
+ deleteTestCases(args) {
2258
+ return deleteTestCases(this.supabase, this.projectId, args);
2259
+ }
2260
+ listTestCases(args) {
2261
+ return listTestCases(this.supabase, this.projectId, args);
2262
+ }
2263
+ listTestRuns(args) {
2264
+ return listTestRuns(this.supabase, this.projectId, args);
2265
+ }
2266
+ createTestRun(args) {
2267
+ return createTestRun(this.supabase, this.projectId, args);
2268
+ }
2269
+ listTestAssignments(args) {
2270
+ return listTestAssignments(this.supabase, this.projectId, args);
2271
+ }
2272
+ assignTests(args) {
2273
+ return assignTests(this.supabase, this.projectId, args);
2274
+ }
2275
+ // ── Intelligence ─────────────────────────────────────
2276
+ getBugPatterns(args) {
2277
+ return getBugPatterns(this.supabase, this.projectId, args);
2278
+ }
2279
+ suggestTestCases(args) {
2280
+ return suggestTestCases(this.supabase, this.projectId, args);
2281
+ }
2282
+ getTestPriorities(args) {
2283
+ return getTestPriorities(this.supabase, this.projectId, args);
2284
+ }
2285
+ getCoverageGaps(args) {
2286
+ return getCoverageGaps(this.supabase, this.projectId, args);
2287
+ }
2288
+ getRegressions(args) {
2289
+ return getRegressions(this.supabase, this.projectId, args);
2290
+ }
2291
+ getCoverageMatrix(args) {
2292
+ return getCoverageMatrix(this.supabase, this.projectId, args);
2293
+ }
2294
+ getStaleCoverage(args) {
2295
+ return getStaleCoverage(this.supabase, this.projectId, args);
2296
+ }
2297
+ generateDeployChecklist(args) {
2298
+ return generateDeployChecklist(this.supabase, this.projectId, args);
2299
+ }
2300
+ getQAHealth(args) {
2301
+ return getQAHealth(this.supabase, this.projectId, args);
2302
+ }
2303
+ getQASessions(args) {
2304
+ return getQASessions(this.supabase, this.projectId, args);
2305
+ }
2306
+ getQAAlerts(args) {
2307
+ return getQAAlerts(this.supabase, this.projectId, args);
2308
+ }
2309
+ // ── Deploy & Analysis ────────────────────────────────
2310
+ getDeploymentAnalysis(args) {
2311
+ return getDeploymentAnalysis(this.supabase, this.projectId, args);
2312
+ }
2313
+ getTesterRecommendations(args) {
2314
+ return getTesterRecommendations(this.supabase, this.projectId, args);
2315
+ }
2316
+ analyzeCommitForTesting(args) {
2317
+ return analyzeCommitForTesting(this.supabase, this.projectId, args);
2318
+ }
2319
+ getTestingPatterns(args) {
2320
+ return getTestingPatterns(this.supabase, this.projectId, args);
2321
+ }
2322
+ analyzeChangesForTests(args) {
2323
+ return analyzeChangesForTests(this.supabase, this.projectId, args);
2324
+ }
2325
+ // ── Bug Lifecycle ────────────────────────────────────
2326
+ createBugReport(args) {
2327
+ return createBugReport(this.supabase, this.projectId, args);
2328
+ }
2329
+ getBugsForFile(args) {
2330
+ return getBugsForFile(this.supabase, this.projectId, args);
2331
+ }
2332
+ markFixedWithCommit(args) {
2333
+ return markFixedWithCommit(this.supabase, this.projectId, args);
2334
+ }
2335
+ getBugsAffectingCode(args) {
2336
+ return getBugsAffectingCode(this.supabase, this.projectId, args);
2337
+ }
2338
+ linkBugToCode(args) {
2339
+ return linkBugToCode(this.supabase, this.projectId, args);
2340
+ }
2341
+ createRegressionTest(args) {
2342
+ return createRegressionTest(this.supabase, this.projectId, args);
2343
+ }
2344
+ // ── Fix Queue ────────────────────────────────────────
2345
+ getPendingFixes(args) {
2346
+ return getPendingFixes(this.supabase, this.projectId, args);
2347
+ }
2348
+ claimFixRequest(args) {
2349
+ return claimFixRequest(this.supabase, this.projectId, args);
2350
+ }
2351
+ completeFixRequest(args) {
2352
+ return completeFixRequest(this.supabase, this.projectId, args);
2353
+ }
2354
+ // ── Team ─────────────────────────────────────────────
2355
+ listTesters(args) {
2356
+ return listTesters(this.supabase, this.projectId, args);
2357
+ }
2358
+ createTester(args) {
2359
+ return createTester(this.supabase, this.projectId, args);
2360
+ }
2361
+ updateTester(args) {
2362
+ return updateTester(this.supabase, this.projectId, args);
2363
+ }
2364
+ getTesterWorkload(args) {
2365
+ return getTesterWorkload(this.supabase, this.projectId, args);
2366
+ }
2367
+ // ── Analytics ────────────────────────────────────────
2368
+ getBugTrends(args) {
2369
+ return getBugTrends(this.supabase, this.projectId, args);
2370
+ }
2371
+ getTesterLeaderboard(args) {
2372
+ return getTesterLeaderboard(this.supabase, this.projectId, args);
2373
+ }
2374
+ exportTestResults(args) {
2375
+ return exportTestResults(this.supabase, this.projectId, args);
2376
+ }
2377
+ getTestingVelocity(args) {
2378
+ return getTestingVelocity(this.supabase, this.projectId, args);
2379
+ }
2380
+ };
2381
+ export {
2382
+ BugBearAdmin,
2383
+ analyzeChangesForTests,
2384
+ analyzeCommitForTesting,
2385
+ analyzeFileTypes,
2386
+ assignTests,
2387
+ bulkUpdateReports,
2388
+ claimFixRequest,
2389
+ completeFixRequest,
2390
+ createBugReport,
2391
+ createRegressionTest,
2392
+ createTestCase,
2393
+ createTestRun,
2394
+ createTester,
2395
+ deleteTestCases,
2396
+ exportTestResults,
2397
+ extractFunctionName,
2398
+ generateDeployChecklist,
2399
+ getBugPatterns,
2400
+ getBugTrends,
2401
+ getBugsAffectingCode,
2402
+ getBugsForFile,
2403
+ getCoverageGaps,
2404
+ getCoverageMatrix,
2405
+ getDeploymentAnalysis,
2406
+ getPendingFixes,
2407
+ getProjectInfo,
2408
+ getQAAlerts,
2409
+ getQAHealth,
2410
+ getQASessions,
2411
+ getQaTracks,
2412
+ getRegressions,
2413
+ getReport,
2414
+ getReportContext,
2415
+ getStaleCoverage,
2416
+ getTestPriorities,
2417
+ getTesterLeaderboard,
2418
+ getTesterRecommendations,
2419
+ getTesterWorkload,
2420
+ getTestingPatterns,
2421
+ getTestingVelocity,
2422
+ getTrackTemplates,
2423
+ isValidUUID,
2424
+ linkBugToCode,
2425
+ listReports,
2426
+ listTestAssignments,
2427
+ listTestCases,
2428
+ listTestRuns,
2429
+ listTesters,
2430
+ markFixedWithCommit,
2431
+ sanitizeSearchQuery,
2432
+ searchReports,
2433
+ suggestTestCases,
2434
+ updateReportStatus,
2435
+ updateTestCase,
2436
+ updateTester
2437
+ };