@fatagnus/convex-feedback 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,605 @@
1
+ import { v } from "convex/values";
2
+ import { query, internalQuery } from "./_generated/server";
3
+ import {
4
+ bugSeverityValidator,
5
+ bugStatusValidator,
6
+ feedbackTypeValidator,
7
+ feedbackPriorityValidator,
8
+ feedbackStatusValidator,
9
+ effortValidator,
10
+ } from "./schema";
11
+
12
+ // ===== Template Types =====
13
+
14
+ export type PromptTemplate = "fix" | "implement" | "analyze" | "codebuff";
15
+
16
+ export const promptTemplateValidator = v.union(
17
+ v.literal("fix"),
18
+ v.literal("implement"),
19
+ v.literal("analyze"),
20
+ v.literal("codebuff")
21
+ );
22
+
23
+ // ===== Bug Report Type =====
24
+
25
+ const bugReportDataValidator = v.object({
26
+ _id: v.id("bugReports"),
27
+ ticketNumber: v.string(),
28
+ title: v.string(),
29
+ description: v.string(),
30
+ severity: bugSeverityValidator,
31
+ status: bugStatusValidator,
32
+ url: v.string(),
33
+ route: v.optional(v.string()),
34
+ browserInfo: v.string(),
35
+ consoleErrors: v.optional(v.string()),
36
+ viewportWidth: v.number(),
37
+ viewportHeight: v.number(),
38
+ networkState: v.string(),
39
+ aiSummary: v.optional(v.string()),
40
+ aiRootCauseAnalysis: v.optional(v.string()),
41
+ aiReproductionSteps: v.optional(v.array(v.string())),
42
+ aiSuggestedFix: v.optional(v.string()),
43
+ aiEstimatedEffort: v.optional(effortValidator),
44
+ createdAt: v.number(),
45
+ });
46
+
47
+ // ===== Feedback Type =====
48
+
49
+ const feedbackDataValidator = v.object({
50
+ _id: v.id("feedback"),
51
+ ticketNumber: v.string(),
52
+ type: feedbackTypeValidator,
53
+ title: v.string(),
54
+ description: v.string(),
55
+ priority: feedbackPriorityValidator,
56
+ status: feedbackStatusValidator,
57
+ url: v.string(),
58
+ route: v.optional(v.string()),
59
+ browserInfo: v.string(),
60
+ consoleErrors: v.optional(v.string()),
61
+ viewportWidth: v.number(),
62
+ viewportHeight: v.number(),
63
+ networkState: v.string(),
64
+ aiSummary: v.optional(v.string()),
65
+ aiImpactAnalysis: v.optional(v.string()),
66
+ aiActionItems: v.optional(v.array(v.string())),
67
+ aiEstimatedEffort: v.optional(effortValidator),
68
+ createdAt: v.number(),
69
+ });
70
+
71
+ // ===== Prompt Generation =====
72
+
73
+ function formatDate(timestamp: number): string {
74
+ return new Date(timestamp).toISOString().split("T")[0];
75
+ }
76
+
77
+ function parseBrowserInfo(browserInfoJson: string): Record<string, unknown> {
78
+ try {
79
+ return JSON.parse(browserInfoJson);
80
+ } catch {
81
+ return { raw: browserInfoJson };
82
+ }
83
+ }
84
+
85
+ function parseConsoleErrors(consoleErrorsJson: string | undefined): unknown[] {
86
+ if (!consoleErrorsJson) return [];
87
+ try {
88
+ return JSON.parse(consoleErrorsJson);
89
+ } catch {
90
+ return [consoleErrorsJson];
91
+ }
92
+ }
93
+
94
+ // ===== Bug Report Prompts =====
95
+
96
+ function generateBugFixPrompt(bug: {
97
+ ticketNumber: string;
98
+ title: string;
99
+ description: string;
100
+ severity: string;
101
+ url: string;
102
+ route?: string;
103
+ browserInfo: string;
104
+ consoleErrors?: string;
105
+ viewportWidth: number;
106
+ viewportHeight: number;
107
+ aiSummary?: string;
108
+ aiRootCauseAnalysis?: string;
109
+ aiReproductionSteps?: string[];
110
+ aiSuggestedFix?: string;
111
+ aiEstimatedEffort?: string;
112
+ createdAt: number;
113
+ }): string {
114
+ const browserInfo = parseBrowserInfo(bug.browserInfo);
115
+ const consoleErrors = parseConsoleErrors(bug.consoleErrors);
116
+
117
+ let prompt = `# Bug Fix Request: ${bug.ticketNumber}
118
+
119
+ ## Issue Summary
120
+ **Title:** ${bug.title}
121
+ **Severity:** ${bug.severity.toUpperCase()}
122
+ **Reported:** ${formatDate(bug.createdAt)}
123
+ **URL:** ${bug.url}${bug.route ? `\n**Route:** ${bug.route}` : ""}
124
+
125
+ ## Description
126
+ ${bug.description}
127
+ `;
128
+
129
+ if (bug.aiSummary) {
130
+ prompt += `\n## AI Summary\n${bug.aiSummary}\n`;
131
+ }
132
+
133
+ if (bug.aiRootCauseAnalysis) {
134
+ prompt += `\n## Root Cause Analysis\n${bug.aiRootCauseAnalysis}\n`;
135
+ }
136
+
137
+ if (bug.aiReproductionSteps && bug.aiReproductionSteps.length > 0) {
138
+ prompt += `\n## Steps to Reproduce\n${bug.aiReproductionSteps.map((s, i) => `${i + 1}. ${s}`).join("\n")}\n`;
139
+ }
140
+
141
+ if (bug.aiSuggestedFix) {
142
+ prompt += `\n## Suggested Fix\n${bug.aiSuggestedFix}\n`;
143
+ }
144
+
145
+ if (consoleErrors.length > 0) {
146
+ prompt += `\n## Console Errors\n\`\`\`\n${JSON.stringify(consoleErrors, null, 2)}\n\`\`\`\n`;
147
+ }
148
+
149
+ prompt += `\n## Environment\n- **Browser:** ${browserInfo.userAgent || "Unknown"}\n- **Viewport:** ${bug.viewportWidth}x${bug.viewportHeight}\n`;
150
+
151
+ if (bug.aiEstimatedEffort) {
152
+ prompt += `\n## Estimated Effort: ${bug.aiEstimatedEffort.toUpperCase()}\n`;
153
+ }
154
+
155
+ prompt += `\n## Instructions\nPlease fix the bug described above. Focus on:\n1. Identifying the root cause in the codebase\n2. Implementing a fix that addresses the issue\n3. Ensuring the fix doesn't introduce regressions\n4. Adding appropriate error handling if needed\n`;
156
+
157
+ return prompt;
158
+ }
159
+
160
+ function generateBugAnalyzePrompt(bug: {
161
+ ticketNumber: string;
162
+ title: string;
163
+ description: string;
164
+ severity: string;
165
+ url: string;
166
+ route?: string;
167
+ browserInfo: string;
168
+ consoleErrors?: string;
169
+ viewportWidth: number;
170
+ viewportHeight: number;
171
+ aiSummary?: string;
172
+ createdAt: number;
173
+ }): string {
174
+ const browserInfo = parseBrowserInfo(bug.browserInfo);
175
+ const consoleErrors = parseConsoleErrors(bug.consoleErrors);
176
+
177
+ let prompt = `# Deep Analysis Request: ${bug.ticketNumber}
178
+
179
+ ## Bug Report
180
+ **Title:** ${bug.title}
181
+ **Severity:** ${bug.severity.toUpperCase()}
182
+ **Reported:** ${formatDate(bug.createdAt)}
183
+ **URL:** ${bug.url}${bug.route ? `\n**Route:** ${bug.route}` : ""}
184
+
185
+ ## Description
186
+ ${bug.description}
187
+ `;
188
+
189
+ if (consoleErrors.length > 0) {
190
+ prompt += `\n## Console Errors\n\`\`\`json\n${JSON.stringify(consoleErrors, null, 2)}\n\`\`\`\n`;
191
+ }
192
+
193
+ prompt += `\n## Environment\n\`\`\`json\n${JSON.stringify(browserInfo, null, 2)}\n\`\`\`\n**Viewport:** ${bug.viewportWidth}x${bug.viewportHeight}\n`;
194
+
195
+ prompt += `\n## Analysis Instructions\nPlease perform a deep analysis of this bug report:\n\n1. **Root Cause Analysis**: What is likely causing this issue?\n2. **Impact Assessment**: What parts of the application could be affected?\n3. **Reproduction Steps**: Based on the context, what steps would reproduce this?\n4. **Fix Strategy**: What approach would you recommend to fix this?\n5. **Risk Assessment**: What are the risks of the proposed fix?\n6. **Testing Requirements**: What tests should be added or updated?\n`;
196
+
197
+ return prompt;
198
+ }
199
+
200
+ function generateBugCodebuffPrompt(bug: {
201
+ ticketNumber: string;
202
+ title: string;
203
+ description: string;
204
+ severity: string;
205
+ url: string;
206
+ route?: string;
207
+ browserInfo: string;
208
+ consoleErrors?: string;
209
+ viewportWidth: number;
210
+ viewportHeight: number;
211
+ aiSummary?: string;
212
+ aiRootCauseAnalysis?: string;
213
+ aiReproductionSteps?: string[];
214
+ aiSuggestedFix?: string;
215
+ createdAt: number;
216
+ }): string {
217
+ const consoleErrors = parseConsoleErrors(bug.consoleErrors);
218
+
219
+ let prompt = `Fix bug ${bug.ticketNumber}: ${bug.title}\n\n`;
220
+ prompt += `**Severity:** ${bug.severity}\n`;
221
+ prompt += `**URL:** ${bug.url}\n`;
222
+ if (bug.route) prompt += `**Route:** ${bug.route}\n`;
223
+ prompt += `\n${bug.description}\n`;
224
+
225
+ if (bug.aiRootCauseAnalysis) {
226
+ prompt += `\n**Root Cause:** ${bug.aiRootCauseAnalysis}\n`;
227
+ }
228
+
229
+ if (bug.aiSuggestedFix) {
230
+ prompt += `\n**Suggested Fix:** ${bug.aiSuggestedFix}\n`;
231
+ }
232
+
233
+ if (consoleErrors.length > 0) {
234
+ prompt += `\n**Console Errors:**\n\`\`\`\n${JSON.stringify(consoleErrors, null, 2)}\n\`\`\`\n`;
235
+ }
236
+
237
+ return prompt;
238
+ }
239
+
240
+ // ===== Feedback Prompts =====
241
+
242
+ function generateFeedbackImplementPrompt(feedback: {
243
+ ticketNumber: string;
244
+ type: string;
245
+ title: string;
246
+ description: string;
247
+ priority: string;
248
+ url: string;
249
+ route?: string;
250
+ aiSummary?: string;
251
+ aiImpactAnalysis?: string;
252
+ aiActionItems?: string[];
253
+ aiEstimatedEffort?: string;
254
+ createdAt: number;
255
+ }): string {
256
+ const typeLabel = feedback.type.replace("_", " ").replace(/\b\w/g, (l) => l.toUpperCase());
257
+
258
+ let prompt = `# Implementation Request: ${feedback.ticketNumber}
259
+
260
+ ## Request Summary
261
+ **Title:** ${feedback.title}
262
+ **Type:** ${typeLabel}
263
+ **Priority:** ${feedback.priority.replace("_", " ").toUpperCase()}
264
+ **Requested:** ${formatDate(feedback.createdAt)}
265
+ **Context URL:** ${feedback.url}${feedback.route ? `\n**Route:** ${feedback.route}` : ""}
266
+
267
+ ## Description
268
+ ${feedback.description}
269
+ `;
270
+
271
+ if (feedback.aiSummary) {
272
+ prompt += `\n## Summary\n${feedback.aiSummary}\n`;
273
+ }
274
+
275
+ if (feedback.aiImpactAnalysis) {
276
+ prompt += `\n## Impact Analysis\n${feedback.aiImpactAnalysis}\n`;
277
+ }
278
+
279
+ if (feedback.aiActionItems && feedback.aiActionItems.length > 0) {
280
+ prompt += `\n## Action Items\n${feedback.aiActionItems.map((item, i) => `${i + 1}. ${item}`).join("\n")}\n`;
281
+ }
282
+
283
+ if (feedback.aiEstimatedEffort) {
284
+ prompt += `\n## Estimated Effort: ${feedback.aiEstimatedEffort.toUpperCase()}\n`;
285
+ }
286
+
287
+ prompt += `\n## Implementation Instructions\nPlease implement this ${typeLabel.toLowerCase()}. Focus on:\n1. Understanding the user's need and use case\n2. Designing a solution that fits the existing architecture\n3. Implementing the feature with proper error handling\n4. Adding appropriate tests\n5. Updating documentation if needed\n`;
288
+
289
+ return prompt;
290
+ }
291
+
292
+ function generateFeedbackAnalyzePrompt(feedback: {
293
+ ticketNumber: string;
294
+ type: string;
295
+ title: string;
296
+ description: string;
297
+ priority: string;
298
+ url: string;
299
+ route?: string;
300
+ aiSummary?: string;
301
+ createdAt: number;
302
+ }): string {
303
+ const typeLabel = feedback.type.replace("_", " ").replace(/\b\w/g, (l) => l.toUpperCase());
304
+
305
+ let prompt = `# Analysis Request: ${feedback.ticketNumber}
306
+
307
+ ## Feedback Details
308
+ **Title:** ${feedback.title}
309
+ **Type:** ${typeLabel}
310
+ **Priority:** ${feedback.priority.replace("_", " ").toUpperCase()}
311
+ **Submitted:** ${formatDate(feedback.createdAt)}
312
+ **Context URL:** ${feedback.url}${feedback.route ? `\n**Route:** ${feedback.route}` : ""}
313
+
314
+ ## Description
315
+ ${feedback.description}
316
+ `;
317
+
318
+ prompt += `\n## Analysis Instructions\nPlease analyze this ${typeLabel.toLowerCase()} and provide:\n\n1. **Feasibility Assessment**: Is this technically feasible? What are the constraints?\n2. **Impact Analysis**: How would this affect the existing system?\n3. **Effort Estimation**: How much work would this require?\n4. **Alternative Approaches**: Are there simpler ways to achieve the user's goal?\n5. **Dependencies**: What other features or systems would this depend on?\n6. **Risks**: What could go wrong with this implementation?\n`;
319
+
320
+ return prompt;
321
+ }
322
+
323
+ function generateFeedbackCodebuffPrompt(feedback: {
324
+ ticketNumber: string;
325
+ type: string;
326
+ title: string;
327
+ description: string;
328
+ priority: string;
329
+ url: string;
330
+ route?: string;
331
+ aiSummary?: string;
332
+ aiActionItems?: string[];
333
+ createdAt: number;
334
+ }): string {
335
+ const typeLabel = feedback.type === "feature_request" ? "feature" : feedback.type === "change_request" ? "change" : "feedback";
336
+
337
+ let prompt = `Implement ${typeLabel} ${feedback.ticketNumber}: ${feedback.title}\n\n`;
338
+ prompt += `**Priority:** ${feedback.priority.replace("_", " ")}\n`;
339
+ prompt += `**URL:** ${feedback.url}\n`;
340
+ if (feedback.route) prompt += `**Route:** ${feedback.route}\n`;
341
+ prompt += `\n${feedback.description}\n`;
342
+
343
+ if (feedback.aiActionItems && feedback.aiActionItems.length > 0) {
344
+ prompt += `\n**Action Items:**\n${feedback.aiActionItems.map((item) => `- ${item}`).join("\n")}\n`;
345
+ }
346
+
347
+ return prompt;
348
+ }
349
+
350
+ // ===== Public API =====
351
+
352
+ /**
353
+ * Generate an AI prompt for a bug report.
354
+ */
355
+ export function generateBugPrompt(
356
+ bug: {
357
+ ticketNumber: string;
358
+ title: string;
359
+ description: string;
360
+ severity: string;
361
+ url: string;
362
+ route?: string;
363
+ browserInfo: string;
364
+ consoleErrors?: string;
365
+ viewportWidth: number;
366
+ viewportHeight: number;
367
+ aiSummary?: string;
368
+ aiRootCauseAnalysis?: string;
369
+ aiReproductionSteps?: string[];
370
+ aiSuggestedFix?: string;
371
+ aiEstimatedEffort?: string;
372
+ createdAt: number;
373
+ },
374
+ template: PromptTemplate
375
+ ): string {
376
+ switch (template) {
377
+ case "fix":
378
+ return generateBugFixPrompt(bug);
379
+ case "analyze":
380
+ return generateBugAnalyzePrompt(bug);
381
+ case "codebuff":
382
+ return generateBugCodebuffPrompt(bug);
383
+ case "implement":
384
+ // For bugs, "implement" falls back to "fix"
385
+ return generateBugFixPrompt(bug);
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Generate an AI prompt for feedback.
391
+ */
392
+ export function generateFeedbackPrompt(
393
+ feedback: {
394
+ ticketNumber: string;
395
+ type: string;
396
+ title: string;
397
+ description: string;
398
+ priority: string;
399
+ url: string;
400
+ route?: string;
401
+ aiSummary?: string;
402
+ aiImpactAnalysis?: string;
403
+ aiActionItems?: string[];
404
+ aiEstimatedEffort?: string;
405
+ createdAt: number;
406
+ },
407
+ template: PromptTemplate
408
+ ): string {
409
+ switch (template) {
410
+ case "implement":
411
+ return generateFeedbackImplementPrompt(feedback);
412
+ case "analyze":
413
+ return generateFeedbackAnalyzePrompt(feedback);
414
+ case "codebuff":
415
+ return generateFeedbackCodebuffPrompt(feedback);
416
+ case "fix":
417
+ // For feedback, "fix" falls back to "implement"
418
+ return generateFeedbackImplementPrompt(feedback);
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Get a bug report by ticket number (internal query).
424
+ */
425
+ export const getBugByTicketNumber = internalQuery({
426
+ args: {
427
+ ticketNumber: v.string(),
428
+ },
429
+ returns: v.union(bugReportDataValidator, v.null()),
430
+ handler: async (ctx, args) => {
431
+ const bug = await ctx.db
432
+ .query("bugReports")
433
+ .withIndex("by_ticket_number", (q) => q.eq("ticketNumber", args.ticketNumber))
434
+ .unique();
435
+
436
+ if (!bug || !bug.ticketNumber) {
437
+ return null;
438
+ }
439
+
440
+ return {
441
+ _id: bug._id,
442
+ ticketNumber: bug.ticketNumber,
443
+ title: bug.title,
444
+ description: bug.description,
445
+ severity: bug.severity,
446
+ status: bug.status,
447
+ url: bug.url,
448
+ route: bug.route,
449
+ browserInfo: bug.browserInfo,
450
+ consoleErrors: bug.consoleErrors,
451
+ viewportWidth: bug.viewportWidth,
452
+ viewportHeight: bug.viewportHeight,
453
+ networkState: bug.networkState,
454
+ aiSummary: bug.aiSummary,
455
+ aiRootCauseAnalysis: bug.aiRootCauseAnalysis,
456
+ aiReproductionSteps: bug.aiReproductionSteps,
457
+ aiSuggestedFix: bug.aiSuggestedFix,
458
+ aiEstimatedEffort: bug.aiEstimatedEffort,
459
+ createdAt: bug.createdAt,
460
+ };
461
+ },
462
+ });
463
+
464
+ /**
465
+ * Get feedback by ticket number (internal query).
466
+ */
467
+ export const getFeedbackByTicketNumber = internalQuery({
468
+ args: {
469
+ ticketNumber: v.string(),
470
+ },
471
+ returns: v.union(feedbackDataValidator, v.null()),
472
+ handler: async (ctx, args) => {
473
+ const feedback = await ctx.db
474
+ .query("feedback")
475
+ .withIndex("by_ticket_number", (q) => q.eq("ticketNumber", args.ticketNumber))
476
+ .unique();
477
+
478
+ if (!feedback || !feedback.ticketNumber) {
479
+ return null;
480
+ }
481
+
482
+ return {
483
+ _id: feedback._id,
484
+ ticketNumber: feedback.ticketNumber,
485
+ type: feedback.type,
486
+ title: feedback.title,
487
+ description: feedback.description,
488
+ priority: feedback.priority,
489
+ status: feedback.status,
490
+ url: feedback.url,
491
+ route: feedback.route,
492
+ browserInfo: feedback.browserInfo,
493
+ consoleErrors: feedback.consoleErrors,
494
+ viewportWidth: feedback.viewportWidth,
495
+ viewportHeight: feedback.viewportHeight,
496
+ networkState: feedback.networkState,
497
+ aiSummary: feedback.aiSummary,
498
+ aiImpactAnalysis: feedback.aiImpactAnalysis,
499
+ aiActionItems: feedback.aiActionItems,
500
+ aiEstimatedEffort: feedback.aiEstimatedEffort,
501
+ createdAt: feedback.createdAt,
502
+ };
503
+ },
504
+ });
505
+
506
+ /**
507
+ * Public query to get a prompt by ticket number.
508
+ */
509
+ export const getPrompt = query({
510
+ args: {
511
+ ticketNumber: v.string(),
512
+ template: v.optional(promptTemplateValidator),
513
+ },
514
+ returns: v.union(
515
+ v.object({
516
+ ticketNumber: v.string(),
517
+ type: v.union(v.literal("bug"), v.literal("feedback")),
518
+ template: promptTemplateValidator,
519
+ prompt: v.string(),
520
+ }),
521
+ v.null()
522
+ ),
523
+ handler: async (ctx, args) => {
524
+ const ticketNumber = args.ticketNumber.toUpperCase();
525
+ const isBug = ticketNumber.startsWith("BUG-");
526
+ const isFeedback = ticketNumber.startsWith("FB-");
527
+
528
+ if (!isBug && !isFeedback) {
529
+ return null;
530
+ }
531
+
532
+ if (isBug) {
533
+ const bug = await ctx.db
534
+ .query("bugReports")
535
+ .withIndex("by_ticket_number", (q) => q.eq("ticketNumber", ticketNumber))
536
+ .unique();
537
+
538
+ if (!bug || !bug.ticketNumber) {
539
+ return null;
540
+ }
541
+
542
+ const template = args.template ?? "fix";
543
+ return {
544
+ ticketNumber: bug.ticketNumber,
545
+ type: "bug" as const,
546
+ template,
547
+ prompt: generateBugPrompt(
548
+ {
549
+ ticketNumber: bug.ticketNumber,
550
+ title: bug.title,
551
+ description: bug.description,
552
+ severity: bug.severity,
553
+ url: bug.url,
554
+ route: bug.route,
555
+ browserInfo: bug.browserInfo,
556
+ consoleErrors: bug.consoleErrors,
557
+ viewportWidth: bug.viewportWidth,
558
+ viewportHeight: bug.viewportHeight,
559
+ aiSummary: bug.aiSummary,
560
+ aiRootCauseAnalysis: bug.aiRootCauseAnalysis,
561
+ aiReproductionSteps: bug.aiReproductionSteps,
562
+ aiSuggestedFix: bug.aiSuggestedFix,
563
+ aiEstimatedEffort: bug.aiEstimatedEffort,
564
+ createdAt: bug.createdAt,
565
+ },
566
+ template
567
+ ),
568
+ };
569
+ }
570
+
571
+ // Feedback
572
+ const feedback = await ctx.db
573
+ .query("feedback")
574
+ .withIndex("by_ticket_number", (q) => q.eq("ticketNumber", ticketNumber))
575
+ .unique();
576
+
577
+ if (!feedback || !feedback.ticketNumber) {
578
+ return null;
579
+ }
580
+
581
+ const template = args.template ?? "implement";
582
+ return {
583
+ ticketNumber: feedback.ticketNumber,
584
+ type: "feedback" as const,
585
+ template,
586
+ prompt: generateFeedbackPrompt(
587
+ {
588
+ ticketNumber: feedback.ticketNumber,
589
+ type: feedback.type,
590
+ title: feedback.title,
591
+ description: feedback.description,
592
+ priority: feedback.priority,
593
+ url: feedback.url,
594
+ route: feedback.route,
595
+ aiSummary: feedback.aiSummary,
596
+ aiImpactAnalysis: feedback.aiImpactAnalysis,
597
+ aiActionItems: feedback.aiActionItems,
598
+ aiEstimatedEffort: feedback.aiEstimatedEffort,
599
+ createdAt: feedback.createdAt,
600
+ },
601
+ template
602
+ ),
603
+ };
604
+ },
605
+ });
@@ -129,6 +129,18 @@ export const inputConfigValidator = v.object({
129
129
  }))),
130
130
  });
131
131
 
132
+ // ===== Counter Type Validator =====
133
+
134
+ /**
135
+ * Counter type for ticket numbers
136
+ */
137
+ export const counterTypeValidator = v.union(
138
+ v.literal("bug"),
139
+ v.literal("feedback")
140
+ );
141
+
142
+ export type CounterType = "bug" | "feedback";
143
+
132
144
  // ===== Schema =====
133
145
 
134
146
  export default defineSchema({
@@ -171,6 +183,8 @@ export default defineSchema({
171
183
  aiAnalyzedAt: v.optional(v.number()),
172
184
  aiThreadId: v.optional(v.string()),
173
185
  notificationsSentAt: v.optional(v.number()),
186
+ // Ticket number (e.g., "BUG-2025-001")
187
+ ticketNumber: v.optional(v.string()),
174
188
  // Timestamps
175
189
  createdAt: v.number(),
176
190
  updatedAt: v.number(),
@@ -180,7 +194,8 @@ export default defineSchema({
180
194
  .index("by_reporter", ["reporterType", "reporterId"])
181
195
  .index("by_created", ["createdAt"])
182
196
  .index("by_archived", ["isArchived"])
183
- .index("by_ai_analyzed", ["aiAnalyzedAt"]),
197
+ .index("by_ai_analyzed", ["aiAnalyzedAt"])
198
+ .index("by_ticket_number", ["ticketNumber"]),
184
199
 
185
200
  /**
186
201
  * Feedback - feature requests, change requests, and general feedback
@@ -222,6 +237,8 @@ export default defineSchema({
222
237
  aiAnalyzedAt: v.optional(v.number()),
223
238
  aiThreadId: v.optional(v.string()),
224
239
  notificationsSentAt: v.optional(v.number()),
240
+ // Ticket number (e.g., "FB-2025-001")
241
+ ticketNumber: v.optional(v.string()),
225
242
  // Timestamps
226
243
  createdAt: v.number(),
227
244
  updatedAt: v.number(),
@@ -232,7 +249,8 @@ export default defineSchema({
232
249
  .index("by_reporter", ["reporterType", "reporterId"])
233
250
  .index("by_created", ["createdAt"])
234
251
  .index("by_archived", ["isArchived"])
235
- .index("by_ai_analyzed", ["aiAnalyzedAt"]),
252
+ .index("by_ai_analyzed", ["aiAnalyzedAt"])
253
+ .index("by_ticket_number", ["ticketNumber"]),
236
254
 
237
255
  /**
238
256
  * Support Teams - configure teams for notification routing
@@ -300,4 +318,36 @@ export default defineSchema({
300
318
  .index("by_thread", ["threadId"])
301
319
  .index("by_reporter", ["reporterType", "reporterId"])
302
320
  .index("by_created", ["createdAt"]),
321
+
322
+ /**
323
+ * Counters - track sequential numbers for ticket IDs
324
+ */
325
+ ticketCounters: defineTable({
326
+ type: counterTypeValidator,
327
+ year: v.number(),
328
+ nextNumber: v.number(),
329
+ })
330
+ .index("by_type_year", ["type", "year"]),
331
+
332
+ /**
333
+ * API Keys - for accessing the HTTP endpoint
334
+ */
335
+ apiKeys: defineTable({
336
+ // The hashed key (SHA-256)
337
+ keyHash: v.string(),
338
+ // First 8 chars of the key for identification (e.g., "fb_abc123")
339
+ keyPrefix: v.string(),
340
+ // Human-readable name
341
+ name: v.string(),
342
+ // Optional expiration
343
+ expiresAt: v.optional(v.number()),
344
+ // Revocation status
345
+ isRevoked: v.boolean(),
346
+ revokedAt: v.optional(v.number()),
347
+ // Timestamps
348
+ createdAt: v.number(),
349
+ })
350
+ .index("by_key_hash", ["keyHash"])
351
+ .index("by_key_prefix", ["keyPrefix"])
352
+ .index("by_revoked", ["isRevoked"]),
303
353
  });
@@ -0,0 +1,4 @@
1
+ // Ticket number generation is inlined in bugReports.ts and feedback.ts
2
+ // to avoid the complexity of calling internal mutations from other mutations.
3
+ // This file is kept as a placeholder and can be removed.
4
+ export {};