@vantageos/backend-doctor 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.
Files changed (108) hide show
  1. package/README.md +88 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +94 -0
  4. package/dist/contract.d.ts +18 -0
  5. package/dist/contract.js +29 -0
  6. package/dist/coverage-cli.d.ts +2 -0
  7. package/dist/coverage-cli.js +17 -0
  8. package/dist/coverage.d.ts +32 -0
  9. package/dist/coverage.js +77 -0
  10. package/dist/detectors/oracle-axis1.d.ts +86 -0
  11. package/dist/detectors/oracle-axis1.js +221 -0
  12. package/dist/detectors/oracle-axis2.d.ts +22 -0
  13. package/dist/detectors/oracle-axis2.js +149 -0
  14. package/dist/detectors/predicates.d.ts +19 -0
  15. package/dist/detectors/predicates.js +334 -0
  16. package/dist/detectors/shared.d.ts +56 -0
  17. package/dist/detectors/shared.js +103 -0
  18. package/dist/detectors/source/_scan.d.ts +43 -0
  19. package/dist/detectors/source/_scan.js +82 -0
  20. package/dist/detectors/source/backfill.d.ts +27 -0
  21. package/dist/detectors/source/backfill.js +211 -0
  22. package/dist/detectors/source/contract.d.ts +33 -0
  23. package/dist/detectors/source/contract.js +385 -0
  24. package/dist/detectors/source/identity.d.ts +47 -0
  25. package/dist/detectors/source/identity.js +410 -0
  26. package/dist/detectors/source/jobs.d.ts +10 -0
  27. package/dist/detectors/source/jobs.js +209 -0
  28. package/dist/detectors/source/lifecycle.d.ts +6 -0
  29. package/dist/detectors/source/lifecycle.js +147 -0
  30. package/dist/detectors/source/readbudget.d.ts +6 -0
  31. package/dist/detectors/source/readbudget.js +133 -0
  32. package/dist/detectors/source/refusal.d.ts +6 -0
  33. package/dist/detectors/source/refusal.js +125 -0
  34. package/dist/detectors/source/security.d.ts +37 -0
  35. package/dist/detectors/source/security.js +392 -0
  36. package/dist/detectors/source/writesafety.d.ts +14 -0
  37. package/dist/detectors/source/writesafety.js +345 -0
  38. package/dist/detectors/standard/_doc.d.ts +66 -0
  39. package/dist/detectors/standard/_doc.js +152 -0
  40. package/dist/detectors/standard/r41-frontier.d.ts +58 -0
  41. package/dist/detectors/standard/r41-frontier.js +176 -0
  42. package/dist/detectors/standard/r46-returns-validator.d.ts +61 -0
  43. package/dist/detectors/standard/r46-returns-validator.js +474 -0
  44. package/dist/detectors/standard/standard-doc.d.ts +12 -0
  45. package/dist/detectors/standard/standard-doc.js +271 -0
  46. package/dist/detectors/structural.d.ts +14 -0
  47. package/dist/detectors/structural.js +83 -0
  48. package/dist/engine-types.d.ts +10 -0
  49. package/dist/engine-types.js +1 -0
  50. package/dist/not-checkable.d.ts +22 -0
  51. package/dist/not-checkable.js +37 -0
  52. package/dist/oracle.d.ts +33 -0
  53. package/dist/oracle.js +133 -0
  54. package/dist/registry.d.ts +11 -0
  55. package/dist/registry.js +140 -0
  56. package/dist/report.d.ts +35 -0
  57. package/dist/report.js +138 -0
  58. package/dist/severity.d.ts +30 -0
  59. package/dist/severity.js +121 -0
  60. package/dist/tally.d.ts +57 -0
  61. package/dist/tally.js +152 -0
  62. package/dist/trigger.d.ts +47 -0
  63. package/dist/trigger.js +143 -0
  64. package/fixtures/backend-standard-51ec7af1.md +648 -0
  65. package/fixtures/backend-standard-a9d7877b.md +629 -0
  66. package/fixtures/mcp-app-standard-a74a042b.md +279 -0
  67. package/fixtures/mcp-extension-standard-a74a042b.md +259 -0
  68. package/fixtures/mcp-server-standard-a74a042b.md +434 -0
  69. package/fixtures/mcp-standard-a74a042b.md +354 -0
  70. package/fixtures/vp-1200-green/.pin +1 -0
  71. package/fixtures/vp-1200-green/convex/migrations.ts +350 -0
  72. package/fixtures/vp-1200-green/convex/schema.ts +1243 -0
  73. package/fixtures/vp-1200-red/.pin +1 -0
  74. package/fixtures/vp-1200-red/convex/migrations.ts +239 -0
  75. package/fixtures/vp-1200-red/convex/schema.ts +1243 -0
  76. package/fixtures/vp-by-tool-day158.csv +190 -0
  77. package/fixtures/vp-convex/.github/workflows/npm-publish.yml +163 -0
  78. package/fixtures/vp-convex/.pin +1 -0
  79. package/fixtures/vp-convex/bun.lock +783 -0
  80. package/fixtures/vp-convex/convex/auth.config.ts +8 -0
  81. package/fixtures/vp-convex/convex/briefingNotes.ts +321 -0
  82. package/fixtures/vp-convex/convex/convex.config.ts +14 -0
  83. package/fixtures/vp-convex/convex/credentials.ts +591 -0
  84. package/fixtures/vp-convex/convex/crons.ts +72 -0
  85. package/fixtures/vp-convex/convex/dashboard.ts +199 -0
  86. package/fixtures/vp-convex/convex/gumroadWebhook.ts +815 -0
  87. package/fixtures/vp-convex/convex/issues.ts +544 -0
  88. package/fixtures/vp-convex/convex/kb.ts +305 -0
  89. package/fixtures/vp-convex/convex/kbMutations.ts +192 -0
  90. package/fixtures/vp-convex/convex/kbShared.ts +56 -0
  91. package/fixtures/vp-convex/convex/licenses.ts +443 -0
  92. package/fixtures/vp-convex/convex/memories.ts +406 -0
  93. package/fixtures/vp-convex/convex/memoriesScoped.ts +220 -0
  94. package/fixtures/vp-convex/convex/messages.ts +971 -0
  95. package/fixtures/vp-convex/convex/migrations.ts +239 -0
  96. package/fixtures/vp-convex/convex/missionTemplates.ts +369 -0
  97. package/fixtures/vp-convex/convex/oauth.ts +1608 -0
  98. package/fixtures/vp-convex/convex/oauthMigrations.ts +96 -0
  99. package/fixtures/vp-convex/convex/okfBundleNode.ts +1031 -0
  100. package/fixtures/vp-convex/convex/recurringTasks.ts +386 -0
  101. package/fixtures/vp-convex/convex/schema.ts +1229 -0
  102. package/fixtures/vp-convex/convex/tasks.ts +2782 -0
  103. package/fixtures/vp-r31-green/.pin +1 -0
  104. package/fixtures/vp-r31-green/convex/migrations.ts +350 -0
  105. package/fixtures/vp-r31-red/.pin +1 -0
  106. package/fixtures/vp-r31-red/convex/migrations.ts +286 -0
  107. package/fixtures/vr-by-tool-day159.csv +89 -0
  108. package/package.json +39 -0
@@ -0,0 +1,544 @@
1
+ import { v } from "convex/values";
2
+ import { mutation, query } from "./_generated/server";
3
+ import { api } from "./_generated/api";
4
+ // convex-strict-mode-doc-type-import-needed-when-refactoring-list-query-from-early-return-to-accumulator-post-filter
5
+ import type { Doc } from "./_generated/dataModel";
6
+
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+ // Shared validators
9
+ // ─────────────────────────────────────────────────────────────────────────────
10
+
11
+ const issueStatusValidator = v.union(
12
+ v.literal("open"),
13
+ v.literal("in_progress"),
14
+ v.literal("fixed"),
15
+ v.literal("verified"),
16
+ v.literal("closed"),
17
+ );
18
+
19
+ const issuePriorityValidator = v.union(
20
+ v.literal("urgent"),
21
+ v.literal("high"),
22
+ v.literal("medium"),
23
+ v.literal("low"),
24
+ );
25
+
26
+ // ─────────────────────────────────────────────────────────────────────────────
27
+ // Helper: derive priority from GitHub labels (case-insensitive)
28
+ // ─────────────────────────────────────────────────────────────────────────────
29
+
30
+ function derivePriority(labels: string[]): "urgent" | "high" | "medium" | "low" {
31
+ const lower = labels.map((l) => l.toLowerCase());
32
+ if (lower.some((l) => l.includes("p0") || l.includes("urgent") || l.includes("critical"))) {
33
+ return "urgent";
34
+ }
35
+ if (lower.some((l) => l.includes("p1") || l.includes("high") || l.includes("important"))) {
36
+ return "high";
37
+ }
38
+ if (lower.some((l) => l.includes("p2") || l.includes("medium"))) {
39
+ return "medium";
40
+ }
41
+ return "low";
42
+ }
43
+
44
+ // ─────────────────────────────────────────────────────────────────────────────
45
+ // upsertFromGitHub — upsert by repo+issueNumber
46
+ // ─────────────────────────────────────────────────────────────────────────────
47
+
48
+ export const upsertFromGitHub = mutation({
49
+ args: {
50
+ repo: v.string(),
51
+ issueNumber: v.number(),
52
+ title: v.string(),
53
+ body: v.string(),
54
+ htmlUrl: v.string(),
55
+ labels: v.array(v.string()),
56
+ status: issueStatusValidator,
57
+ githubCreatedAt: v.number(),
58
+ githubUpdatedAt: v.number(),
59
+ },
60
+ handler: async (ctx, args): Promise<string> => {
61
+ // Truncate body to 2000 chars
62
+ const body = args.body.slice(0, 2000);
63
+
64
+ // Get assignedOrchestrator + project from repo mapping
65
+ const mapping = await ctx.runQuery(api.githubRepoMapping.getByRepo, {
66
+ repo: args.repo,
67
+ });
68
+ const assignedOrchestrator: string = mapping?.orchestrator ?? "sigma";
69
+ const project: string = mapping?.project ?? args.repo;
70
+
71
+ // Derive priority from labels
72
+ const priority = derivePriority(args.labels);
73
+
74
+ // Check if issue already exists
75
+ const existing = await ctx.db
76
+ .query("issues")
77
+ .withIndex("by_repo_number", (q) =>
78
+ q.eq("repo", args.repo).eq("issueNumber", args.issueNumber),
79
+ )
80
+ .unique();
81
+
82
+ if (existing) {
83
+ await ctx.db.patch(existing._id, {
84
+ title: args.title,
85
+ body,
86
+ htmlUrl: args.htmlUrl,
87
+ labels: args.labels,
88
+ status: args.status,
89
+ priority,
90
+ assignedOrchestrator,
91
+ project,
92
+ githubUpdatedAt: args.githubUpdatedAt,
93
+ });
94
+ return existing._id;
95
+ }
96
+
97
+ return await ctx.db.insert("issues", {
98
+ repo: args.repo,
99
+ issueNumber: args.issueNumber,
100
+ title: args.title,
101
+ body,
102
+ htmlUrl: args.htmlUrl,
103
+ labels: args.labels,
104
+ status: args.status,
105
+ priority,
106
+ assignedOrchestrator,
107
+ project,
108
+ githubCreatedAt: args.githubCreatedAt,
109
+ githubUpdatedAt: args.githubUpdatedAt,
110
+ });
111
+ },
112
+ });
113
+
114
+ // ─────────────────────────────────────────────────────────────────────────────
115
+ // updateStatus — update issue status by repo+issueNumber
116
+ // ─────────────────────────────────────────────────────────────────────────────
117
+
118
+ export const updateStatus = mutation({
119
+ args: {
120
+ repo: v.string(),
121
+ issueNumber: v.number(),
122
+ status: issueStatusValidator,
123
+ },
124
+ handler: async (ctx, args) => {
125
+ const existing = await ctx.db
126
+ .query("issues")
127
+ .withIndex("by_repo_number", (q) =>
128
+ q.eq("repo", args.repo).eq("issueNumber", args.issueNumber),
129
+ )
130
+ .unique();
131
+ if (!existing) throw new Error(`Issue ${args.repo}#${args.issueNumber} not found`);
132
+ await ctx.db.patch(existing._id, { status: args.status });
133
+ return null;
134
+ },
135
+ });
136
+
137
+ // ─────────────────────────────────────────────────────────────────────────────
138
+ // linkCommit — append a commit SHA to fixCommits, set fixedBy + fixedAt
139
+ // ─────────────────────────────────────────────────────────────────────────────
140
+
141
+ export const linkCommit = mutation({
142
+ args: {
143
+ repo: v.string(),
144
+ issueNumber: v.number(),
145
+ commitSha: v.string(),
146
+ fixedBy: v.string(),
147
+ },
148
+ handler: async (ctx, args) => {
149
+ const existing = await ctx.db
150
+ .query("issues")
151
+ .withIndex("by_repo_number", (q) =>
152
+ q.eq("repo", args.repo).eq("issueNumber", args.issueNumber),
153
+ )
154
+ .unique();
155
+ if (!existing) throw new Error(`Issue ${args.repo}#${args.issueNumber} not found`);
156
+ const fixCommits = [...(existing.fixCommits ?? []), args.commitSha];
157
+ await ctx.db.patch(existing._id, {
158
+ fixCommits,
159
+ fixedBy: args.fixedBy,
160
+ fixedAt: Date.now(),
161
+ });
162
+ return null;
163
+ },
164
+ });
165
+
166
+ // ─────────────────────────────────────────────────────────────────────────────
167
+ // linkTask — append a taskId to linkedTaskIds
168
+ // ─────────────────────────────────────────────────────────────────────────────
169
+
170
+ export const linkTask = mutation({
171
+ args: {
172
+ repo: v.string(),
173
+ issueNumber: v.number(),
174
+ taskId: v.string(),
175
+ },
176
+ handler: async (ctx, args) => {
177
+ const existing = await ctx.db
178
+ .query("issues")
179
+ .withIndex("by_repo_number", (q) =>
180
+ q.eq("repo", args.repo).eq("issueNumber", args.issueNumber),
181
+ )
182
+ .unique();
183
+ if (!existing) throw new Error(`Issue ${args.repo}#${args.issueNumber} not found`);
184
+ const linkedTaskIds = [...(existing.linkedTaskIds ?? []), args.taskId];
185
+ await ctx.db.patch(existing._id, { linkedTaskIds });
186
+ return null;
187
+ },
188
+ });
189
+
190
+ // ─────────────────────────────────────────────────────────────────────────────
191
+ // verify — set verifiedBy + verifiedAt, change status to "verified"
192
+ // ─────────────────────────────────────────────────────────────────────────────
193
+
194
+ export const verify = mutation({
195
+ args: {
196
+ repo: v.string(),
197
+ issueNumber: v.number(),
198
+ verifiedBy: v.string(),
199
+ },
200
+ handler: async (ctx, args) => {
201
+ const existing = await ctx.db
202
+ .query("issues")
203
+ .withIndex("by_repo_number", (q) =>
204
+ q.eq("repo", args.repo).eq("issueNumber", args.issueNumber),
205
+ )
206
+ .unique();
207
+ if (!existing) throw new Error(`Issue ${args.repo}#${args.issueNumber} not found`);
208
+ await ctx.db.patch(existing._id, {
209
+ status: "verified",
210
+ verifiedBy: args.verifiedBy,
211
+ verifiedAt: Date.now(),
212
+ });
213
+ return null;
214
+ },
215
+ });
216
+
217
+ // ─────────────────────────────────────────────────────────────────────────────
218
+ // close — set status to "closed"
219
+ // ─────────────────────────────────────────────────────────────────────────────
220
+
221
+ export const close = mutation({
222
+ args: {
223
+ repo: v.string(),
224
+ issueNumber: v.number(),
225
+ },
226
+ handler: async (ctx, args) => {
227
+ const existing = await ctx.db
228
+ .query("issues")
229
+ .withIndex("by_repo_number", (q) =>
230
+ q.eq("repo", args.repo).eq("issueNumber", args.issueNumber),
231
+ )
232
+ .unique();
233
+ if (!existing) throw new Error(`Issue ${args.repo}#${args.issueNumber} not found`);
234
+ await ctx.db.patch(existing._id, { status: "closed" });
235
+ return null;
236
+ },
237
+ });
238
+
239
+ // ─────────────────────────────────────────────────────────────────────────────
240
+ // getByRepoNumber — fetch a single issue by repo+issueNumber
241
+ // ─────────────────────────────────────────────────────────────────────────────
242
+
243
+ export const getByRepoNumber = query({
244
+ args: {
245
+ repo: v.string(),
246
+ issueNumber: v.number(),
247
+ },
248
+ handler: async (ctx, args) => {
249
+ return await ctx.db
250
+ .query("issues")
251
+ .withIndex("by_repo_number", (q) =>
252
+ q.eq("repo", args.repo).eq("issueNumber", args.issueNumber),
253
+ )
254
+ .unique();
255
+ },
256
+ });
257
+
258
+ // ─────────────────────────────────────────────────────────────────────────────
259
+ // listByProject — list issues by project with optional status filter
260
+ // ─────────────────────────────────────────────────────────────────────────────
261
+
262
+ // PR #635 wide-scan-cap pattern (see convex/tasks.ts TASK_LIST_SCAN_CAP,
263
+ // convex/profiles.ts PROFILES_LIST_SCAN_CAP, lot 1 mission k574p02m). When
264
+ // paginating via `createdBefore`, the post-take filter only finds rows
265
+ // older than the cursor if the FETCH is wide enough to include them —
266
+ // mission k574p02m DEFECT 2, lot 2.
267
+ export const ISSUES_LIST_BY_PROJECT_SCAN_CAP = 2000;
268
+
269
+ export const listByProject = query({
270
+ args: {
271
+ fields: v.optional(v.union(v.literal("lite"), v.literal("full"))), // v2.4.12 accept (no-op for now) — closes ArgumentValidationError from MCP wrappers passing fields
272
+ project: v.string(),
273
+ status: v.optional(issueStatusValidator),
274
+ limit: v.optional(v.number()),
275
+ // S3.3 B8 follow-up batch 2 — cursor paging anchor (newest-first).
276
+ createdBefore: v.optional(v.number()),
277
+ },
278
+ handler: async (ctx, args) => {
279
+ const limit = args.limit ?? 50;
280
+ const needsWideScan = args.createdBefore !== undefined;
281
+ const fetchCap = needsWideScan
282
+ ? ISSUES_LIST_BY_PROJECT_SCAN_CAP + 1
283
+ : limit;
284
+ // The by_project index only has ["project"], so we query by project
285
+ // and filter status in-memory if needed
286
+ let results = await ctx.db
287
+ .query("issues")
288
+ .withIndex("by_project", (q) => q.eq("project", args.project))
289
+ .order("desc")
290
+ .take(fetchCap);
291
+
292
+ if (args.status !== undefined) {
293
+ results = results.filter((r) => r.status === args.status);
294
+ }
295
+ if (args.createdBefore !== undefined) {
296
+ const before = args.createdBefore;
297
+ results = results.filter((r) => r._creationTime < before);
298
+ }
299
+ return results.slice(0, limit);
300
+ },
301
+ });
302
+
303
+ // ─────────────────────────────────────────────────────────────────────────────
304
+ // listByOrchestrator — list issues by assignedOrchestrator with optional status
305
+ // ─────────────────────────────────────────────────────────────────────────────
306
+
307
+ export const ISSUES_LIST_BY_ORCHESTRATOR_SCAN_CAP = 2000;
308
+
309
+ export const listByOrchestrator = query({
310
+ args: {
311
+ fields: v.optional(v.union(v.literal("lite"), v.literal("full"))), // v2.4.12 accept (no-op for now) — closes ArgumentValidationError from MCP wrappers passing fields
312
+ assignedOrchestrator: v.string(),
313
+ status: v.optional(issueStatusValidator),
314
+ limit: v.optional(v.number()),
315
+ // S3.3 B8 follow-up batch 2 — cursor paging anchor (newest-first).
316
+ createdBefore: v.optional(v.number()),
317
+ },
318
+ handler: async (ctx, args) => {
319
+ const limit = args.limit ?? 50;
320
+ const needsWideScan = args.createdBefore !== undefined;
321
+ const fetchCap = needsWideScan
322
+ ? ISSUES_LIST_BY_ORCHESTRATOR_SCAN_CAP + 1
323
+ : limit;
324
+ let rows: Doc<"issues">[];
325
+ if (args.status !== undefined) {
326
+ const statusFilter = args.status;
327
+ rows = await ctx.db
328
+ .query("issues")
329
+ .withIndex("by_assigned", (q) =>
330
+ q.eq("assignedOrchestrator", args.assignedOrchestrator).eq("status", statusFilter),
331
+ )
332
+ .order("desc")
333
+ .take(fetchCap);
334
+ } else {
335
+ rows = await ctx.db
336
+ .query("issues")
337
+ .withIndex("by_assigned", (q) =>
338
+ q.eq("assignedOrchestrator", args.assignedOrchestrator),
339
+ )
340
+ .order("desc")
341
+ .take(fetchCap);
342
+ }
343
+ if (args.createdBefore !== undefined) {
344
+ const before = args.createdBefore;
345
+ rows = rows.filter((r) => r._creationTime < before);
346
+ }
347
+ return rows.slice(0, limit);
348
+ },
349
+ });
350
+
351
+ // ─────────────────────────────────────────────────────────────────────────────
352
+ // listByStatus — list all issues matching a given status (no orchestrator filter)
353
+ // ─────────────────────────────────────────────────────────────────────────────
354
+
355
+ export const ISSUES_LIST_BY_STATUS_SCAN_CAP = 2000;
356
+
357
+ export const listByStatus = query({
358
+ args: {
359
+ fields: v.optional(v.union(v.literal("lite"), v.literal("full"))), // v2.4.12 accept (no-op for now) — closes ArgumentValidationError from MCP wrappers passing fields
360
+ status: issueStatusValidator,
361
+ limit: v.optional(v.number()),
362
+ // S3.3 B8 follow-up batch 2 — cursor paging anchor (newest-first).
363
+ createdBefore: v.optional(v.number()),
364
+ },
365
+ returns: v.array(v.any()),
366
+ handler: async (ctx, args) => {
367
+ const limit = args.limit ?? 50;
368
+ const needsWideScan = args.createdBefore !== undefined;
369
+ const fetchCap = needsWideScan
370
+ ? ISSUES_LIST_BY_STATUS_SCAN_CAP + 1
371
+ : limit;
372
+ let rows: Doc<"issues">[] = await ctx.db
373
+ .query("issues")
374
+ .withIndex("by_status", (q) => q.eq("status", args.status))
375
+ .order("desc")
376
+ .take(fetchCap);
377
+ if (args.createdBefore !== undefined) {
378
+ const before = args.createdBefore;
379
+ rows = rows.filter((r) => r._creationTime < before);
380
+ }
381
+ return rows.slice(0, limit);
382
+ },
383
+ });
384
+
385
+ // ─────────────────────────────────────────────────────────────────────────────
386
+ // getStats — count issues per status, optionally filtered by project
387
+ // ─────────────────────────────────────────────────────────────────────────────
388
+
389
+ export const getStats = query({
390
+ args: {
391
+ project: v.optional(v.string()),
392
+ },
393
+ handler: async (ctx, args) => {
394
+ const stats = { open: 0, in_progress: 0, fixed: 0, verified: 0, closed: 0, total: 0 };
395
+
396
+ let issues;
397
+ if (args.project) {
398
+ issues = await ctx.db
399
+ .query("issues")
400
+ .withIndex("by_project", (q) => q.eq("project", args.project!))
401
+ .take(1000);
402
+ } else {
403
+ issues = await ctx.db.query("issues").take(1000);
404
+ }
405
+
406
+ for (const issue of issues) {
407
+ stats[issue.status as keyof typeof stats]++;
408
+ stats.total++;
409
+ }
410
+
411
+ return stats;
412
+ },
413
+ });
414
+
415
+ // ─────────────────────────────────────────────────────────────────────────────
416
+ // External issue tracking (for Zeta contributions to third-party repos)
417
+ // ─────────────────────────────────────────────────────────────────────────────
418
+
419
+ export const createExternal = mutation({
420
+ args: {
421
+ externalRepo: v.string(),
422
+ externalIssueNumber: v.number(),
423
+ externalIssueUrl: v.string(),
424
+ title: v.string(),
425
+ body: v.string(),
426
+ assignedOrchestrator: v.string(),
427
+ project: v.optional(v.string()),
428
+ priority: v.optional(v.union(v.literal("urgent"), v.literal("high"), v.literal("medium"), v.literal("low"))),
429
+ forkRepo: v.optional(v.string()),
430
+ },
431
+ returns: v.id("issues"),
432
+ handler: async (ctx, args) => {
433
+ const now = Date.now();
434
+ return await ctx.db.insert("issues", {
435
+ repo: args.externalRepo,
436
+ issueNumber: args.externalIssueNumber,
437
+ title: args.title,
438
+ body: args.body.slice(0, 2000),
439
+ htmlUrl: args.externalIssueUrl,
440
+ labels: [],
441
+ status: "open",
442
+ priority: args.priority ?? "medium",
443
+ assignedOrchestrator: args.assignedOrchestrator,
444
+ project: args.project ?? "external",
445
+ githubCreatedAt: now,
446
+ githubUpdatedAt: now,
447
+ externalRepo: args.externalRepo,
448
+ externalIssueNumber: args.externalIssueNumber,
449
+ externalIssueUrl: args.externalIssueUrl,
450
+ forkRepo: args.forkRepo,
451
+ });
452
+ },
453
+ });
454
+
455
+ export const updatePrStatus = mutation({
456
+ args: {
457
+ repo: v.string(),
458
+ issueNumber: v.number(),
459
+ prUrl: v.string(),
460
+ prStatus: v.union(
461
+ v.literal("draft"),
462
+ v.literal("open"),
463
+ v.literal("merged"),
464
+ v.literal("closed"),
465
+ ),
466
+ },
467
+ returns: v.null(),
468
+ handler: async (ctx, args) => {
469
+ const issue = await ctx.db
470
+ .query("issues")
471
+ .withIndex("by_repo_number", (q) =>
472
+ q.eq("repo", args.repo).eq("issueNumber", args.issueNumber),
473
+ )
474
+ .unique();
475
+ if (!issue) return null;
476
+
477
+ await ctx.db.patch(issue._id, {
478
+ prUrl: args.prUrl,
479
+ prStatus: args.prStatus,
480
+ githubUpdatedAt: Date.now(),
481
+ });
482
+ return null;
483
+ },
484
+ });
485
+
486
+ export const listExternalOpen = query({
487
+ args: {
488
+ prStatus: v.optional(v.union(
489
+ v.literal("draft"),
490
+ v.literal("open"),
491
+ v.literal("merged"),
492
+ v.literal("closed"),
493
+ )),
494
+ limit: v.optional(v.number()),
495
+ paginationToken: v.optional(v.union(v.string(), v.null())),
496
+ },
497
+ returns: v.object({
498
+ issues: v.array(v.object({
499
+ _id: v.id("issues"),
500
+ _creationTime: v.number(),
501
+ repo: v.string(),
502
+ issueNumber: v.number(),
503
+ title: v.string(),
504
+ status: v.string(),
505
+ externalRepo: v.optional(v.string()),
506
+ externalIssueUrl: v.optional(v.string()),
507
+ prUrl: v.optional(v.string()),
508
+ prStatus: v.optional(v.string()),
509
+ assignedOrchestrator: v.string(),
510
+ })),
511
+ nextPageToken: v.union(v.string(), v.null()),
512
+ }),
513
+ handler: async (ctx, args) => {
514
+ const numItems = args.limit ?? 50;
515
+
516
+ const result = await ctx.db
517
+ .query("issues")
518
+ .filter((q) => {
519
+ let expr = q.neq(q.field("externalRepo"), undefined);
520
+ if (args.prStatus !== undefined) {
521
+ expr = q.and(expr, q.eq(q.field("prStatus"), args.prStatus));
522
+ }
523
+ return expr;
524
+ })
525
+ .paginate({ numItems, cursor: args.paginationToken ?? null });
526
+
527
+ return {
528
+ issues: result.page.map((i) => ({
529
+ _id: i._id,
530
+ _creationTime: i._creationTime,
531
+ repo: i.repo,
532
+ issueNumber: i.issueNumber,
533
+ title: i.title,
534
+ status: i.status,
535
+ externalRepo: i.externalRepo,
536
+ externalIssueUrl: i.externalIssueUrl,
537
+ prUrl: i.prUrl,
538
+ prStatus: i.prStatus as string | undefined,
539
+ assignedOrchestrator: i.assignedOrchestrator,
540
+ })),
541
+ nextPageToken: result.isDone ? null : result.continueCursor,
542
+ };
543
+ },
544
+ });