@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,386 @@
1
+ import { v } from "convex/values";
2
+ import type { Doc } from "./_generated/dataModel";
3
+ import { mutation, query, internalMutation } from "./_generated/server";
4
+ import { internal, api } from "./_generated/api";
5
+ import { creatorValidator } from "./schema";
6
+ import { requireId } from "./lib/ids";
7
+
8
+ // Issue #1064 slice-6 (FINAL) — same hint for all five single-id handlers
9
+ // below, all reads/writes on the recurringTasks table.
10
+ const RECURRING_TASK_ID_HINT =
11
+ "Use the full 32-char id returned by list_recurring_tasks or create_recurring_task.";
12
+
13
+ // Open string — any orchestrator name accepted (issue #132)
14
+ const assigneeValidator = v.string();
15
+
16
+ const priorityValidator = v.union(
17
+ v.literal("urgent"),
18
+ v.literal("high"),
19
+ v.literal("medium"),
20
+ v.literal("low"),
21
+ );
22
+
23
+ // ─────────────────────────────────────────────────────────────────────────────
24
+ // Simple cron expression → next run time calculator
25
+ // Supports: "0 9 * * *" (daily at 9), "0 9 * * 1" (Monday 9am),
26
+ // "0 */6 * * *" (every 6 hours), "*/30 * * * *" (every 30 min)
27
+ // ─────────────────────────────────────────────────────────────────────────────
28
+
29
+ function getNextRunTime(cronExpression: string, after: number = Date.now()): number {
30
+ const parts = cronExpression.trim().split(/\s+/);
31
+ if (parts.length !== 5) {
32
+ throw new Error(`Invalid cron expression: "${cronExpression}" — must have 5 fields`);
33
+ }
34
+
35
+ const [minStr, hourStr, , , dowStr] = parts;
36
+
37
+ // Parse a cron field value (supports: *, N, */N)
38
+ function parseField(field: string, current: number, max: number): number[] {
39
+ if (field === "*") {
40
+ return Array.from({ length: max }, (_, i) => i);
41
+ }
42
+ if (field.startsWith("*/")) {
43
+ const step = parseInt(field.slice(2), 10);
44
+ const values: number[] = [];
45
+ for (let i = 0; i < max; i += step) {
46
+ values.push(i);
47
+ }
48
+ return values;
49
+ }
50
+ const vals = field.split(",").map((s) => parseInt(s, 10));
51
+ return vals.filter((n) => !isNaN(n));
52
+ }
53
+
54
+ const minutes = parseField(minStr, 0, 60);
55
+ const hours = parseField(hourStr, 0, 24);
56
+ const dows = dowStr === "*" ? null : parseField(dowStr, 0, 7);
57
+
58
+ // Start from `after` and scan forward up to 8 days
59
+ const start = new Date(after + 60_000); // at least 1 minute in the future
60
+ const maxScan = after + 8 * 24 * 60 * 60 * 1000;
61
+
62
+ const candidate = new Date(start);
63
+ candidate.setSeconds(0, 0);
64
+
65
+ while (candidate.getTime() < maxScan) {
66
+ const m = candidate.getMinutes();
67
+ const h = candidate.getHours();
68
+ const dow = candidate.getDay();
69
+
70
+ if (
71
+ minutes.includes(m) &&
72
+ hours.includes(h) &&
73
+ (dows === null || dows.includes(dow))
74
+ ) {
75
+ return candidate.getTime();
76
+ }
77
+
78
+ // Advance by 1 minute
79
+ candidate.setTime(candidate.getTime() + 60_000);
80
+ }
81
+
82
+ // Fallback: 24 hours from now
83
+ return after + 24 * 60 * 60 * 1000;
84
+ }
85
+
86
+ // ─────────────────────────────────────────────────────────────────────────────
87
+ // create — create a new recurring task
88
+ // ─────────────────────────────────────────────────────────────────────────────
89
+
90
+ export const create = mutation({
91
+ args: {
92
+ title: v.string(),
93
+ description: v.optional(v.string()),
94
+ assignedTo: assigneeValidator,
95
+ priority: priorityValidator,
96
+ project: v.optional(v.string()),
97
+ tags: v.optional(v.array(v.string())),
98
+ cronExpression: v.string(),
99
+ createdBy: creatorValidator,
100
+ },
101
+ returns: v.id("recurringTasks"),
102
+ handler: async (ctx, args) => {
103
+ const now = Date.now();
104
+ const nextRunAt = getNextRunTime(args.cronExpression, now);
105
+
106
+ return await ctx.db.insert("recurringTasks", {
107
+ title: args.title,
108
+ description: args.description,
109
+ assignedTo: args.assignedTo,
110
+ priority: args.priority,
111
+ project: args.project,
112
+ tags: args.tags,
113
+ cronExpression: args.cronExpression,
114
+ lastCreatedAt: undefined,
115
+ nextRunAt,
116
+ active: true,
117
+ createdBy: args.createdBy,
118
+ createdAt: now,
119
+ updatedAt: now,
120
+ });
121
+ },
122
+ });
123
+
124
+ // ─────────────────────────────────────────────────────────────────────────────
125
+ // list — list recurring tasks with optional filters
126
+ // ─────────────────────────────────────────────────────────────────────────────
127
+
128
+ // PR #635 wide-scan-cap pattern (see convex/tasks.ts TASK_LIST_SCAN_CAP,
129
+ // convex/profiles.ts PROFILES_LIST_SCAN_CAP, lot 1 mission k574p02m). When
130
+ // paginating via `createdBefore`, the post-take filter only finds rows
131
+ // older than the cursor if the FETCH is wide enough to include them —
132
+ // mission k574p02m DEFECT 2, lot 2.
133
+ export const RECURRING_TASKS_LIST_SCAN_CAP = 2000;
134
+
135
+ export const list = query({
136
+ args: {
137
+ 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
138
+ assignedTo: v.optional(assigneeValidator),
139
+ active: v.optional(v.boolean()),
140
+ limit: v.optional(v.number()),
141
+ // S3.3 B8 follow-up batch 1 — cursor paging anchor (forward, newest-first).
142
+ createdBefore: v.optional(v.number()),
143
+ },
144
+ handler: async (ctx, args) => {
145
+ const limit = args.limit ?? 50;
146
+ const needsWideScan = args.createdBefore !== undefined;
147
+ const fetchCap = needsWideScan
148
+ ? RECURRING_TASKS_LIST_SCAN_CAP + 1
149
+ : limit;
150
+
151
+ let rows: Doc<"recurringTasks">[];
152
+ if (args.assignedTo !== undefined) {
153
+ rows = await ctx.db
154
+ .query("recurringTasks")
155
+ .withIndex("by_assignee", (q) => q.eq("assignedTo", args.assignedTo!))
156
+ .order("desc")
157
+ .take(fetchCap);
158
+ if (args.active !== undefined) {
159
+ rows = rows.filter((t) => t.active === args.active);
160
+ }
161
+ } else if (args.active !== undefined) {
162
+ rows = await ctx.db
163
+ .query("recurringTasks")
164
+ .withIndex("by_active", (q) => q.eq("active", args.active!))
165
+ .order("desc")
166
+ .take(fetchCap);
167
+ } else {
168
+ rows = await ctx.db.query("recurringTasks").order("desc").take(fetchCap);
169
+ }
170
+
171
+ // S3.3 B8 follow-up batch 1 — cursor paging anchor: drop rows newer-or-equal to before.
172
+ if (args.createdBefore !== undefined) {
173
+ const before = args.createdBefore;
174
+ rows = rows.filter((r) => r._creationTime < before);
175
+ }
176
+ return rows.slice(0, limit);
177
+ },
178
+ });
179
+
180
+ // ─────────────────────────────────────────────────────────────────────────────
181
+ // update — update a recurring task's fields
182
+ // ─────────────────────────────────────────────────────────────────────────────
183
+
184
+ export const update = mutation({
185
+ args: {
186
+ recurringTaskId: v.string(),
187
+ title: v.optional(v.string()),
188
+ description: v.optional(v.string()),
189
+ assignedTo: v.optional(assigneeValidator),
190
+ priority: v.optional(priorityValidator),
191
+ project: v.optional(v.string()),
192
+ tags: v.optional(v.array(v.string())),
193
+ cronExpression: v.optional(v.string()),
194
+ },
195
+ returns: v.id("recurringTasks"),
196
+ handler: async (ctx, args) => {
197
+ const recurringTaskId = requireId(
198
+ ctx,
199
+ "recurringTasks",
200
+ args.recurringTaskId,
201
+ "recurringTaskId",
202
+ RECURRING_TASK_ID_HINT,
203
+ );
204
+ const existing = await ctx.db.get(recurringTaskId);
205
+ if (!existing) throw new Error("Recurring task not found");
206
+
207
+ const patch: Record<string, any> = { updatedAt: Date.now() };
208
+ if (args.title !== undefined) patch.title = args.title;
209
+ if (args.description !== undefined) patch.description = args.description;
210
+ if (args.assignedTo !== undefined) patch.assignedTo = args.assignedTo;
211
+ if (args.priority !== undefined) patch.priority = args.priority;
212
+ if (args.project !== undefined) patch.project = args.project;
213
+ if (args.tags !== undefined) patch.tags = args.tags;
214
+ if (args.cronExpression !== undefined) {
215
+ patch.cronExpression = args.cronExpression;
216
+ patch.nextRunAt = getNextRunTime(args.cronExpression, Date.now());
217
+ }
218
+
219
+ await ctx.db.patch(recurringTaskId, patch);
220
+ return recurringTaskId;
221
+ },
222
+ });
223
+
224
+ // ─────────────────────────────────────────────────────────────────────────────
225
+ // pause — set active=false
226
+ // ─────────────────────────────────────────────────────────────────────────────
227
+
228
+ export const pause = mutation({
229
+ args: { taskId: v.string() },
230
+ handler: async (ctx, args) => {
231
+ const taskId = requireId(
232
+ ctx,
233
+ "recurringTasks",
234
+ args.taskId,
235
+ "taskId",
236
+ RECURRING_TASK_ID_HINT,
237
+ );
238
+ await ctx.db.patch(taskId, { active: false, updatedAt: Date.now() });
239
+ return { taskId, active: false };
240
+ },
241
+ });
242
+
243
+ // ─────────────────────────────────────────────────────────────────────────────
244
+ // resume — set active=true, recalculate nextRunAt
245
+ // ─────────────────────────────────────────────────────────────────────────────
246
+
247
+ export const resume = mutation({
248
+ args: { taskId: v.string() },
249
+ handler: async (ctx, args) => {
250
+ const taskId = requireId(
251
+ ctx,
252
+ "recurringTasks",
253
+ args.taskId,
254
+ "taskId",
255
+ RECURRING_TASK_ID_HINT,
256
+ );
257
+ const task = await ctx.db.get(taskId);
258
+ if (!task) throw new Error("Recurring task not found");
259
+
260
+ const nextRunAt = getNextRunTime(task.cronExpression, Date.now());
261
+ await ctx.db.patch(taskId, {
262
+ active: true,
263
+ nextRunAt,
264
+ updatedAt: Date.now(),
265
+ });
266
+ return { taskId, active: true, nextRunAt };
267
+ },
268
+ });
269
+
270
+ // ─────────────────────────────────────────────────────────────────────────────
271
+ // remove — hard delete
272
+ // ─────────────────────────────────────────────────────────────────────────────
273
+
274
+ export const remove = mutation({
275
+ args: { taskId: v.string() },
276
+ handler: async (ctx, args) => {
277
+ const taskId = requireId(
278
+ ctx,
279
+ "recurringTasks",
280
+ args.taskId,
281
+ "taskId",
282
+ RECURRING_TASK_ID_HINT,
283
+ );
284
+ await ctx.db.delete(taskId);
285
+ return { deleted: true };
286
+ },
287
+ });
288
+
289
+ // ─────────────────────────────────────────────────────────────────────────────
290
+ // processDueTasks (internal — called by cron every 15 min)
291
+ // Creates tasks from recurring templates when nextRunAt <= now
292
+ // ─────────────────────────────────────────────────────────────────────────────
293
+
294
+ export const processDueTasks = internalMutation({
295
+ args: {},
296
+ handler: async (ctx) => {
297
+ const now = Date.now();
298
+
299
+ const dueTasks = await ctx.db
300
+ .query("recurringTasks")
301
+ .withIndex("by_active", (q) => q.eq("active", true))
302
+ .collect();
303
+
304
+ let created = 0;
305
+ let failed = 0;
306
+
307
+ for (const recurring of dueTasks) {
308
+ if (recurring.nextRunAt > now) continue;
309
+
310
+ // Per-row isolation (#1167): a single poison recurring row — e.g. a
311
+ // malformed cronExpression that makes getNextRunTime throw, or an
312
+ // insert that fails validation — must NEVER abort the whole batch.
313
+ // Without this guard the entire mutation aborted every 15-min tick,
314
+ // so no task was ever created and the cron surfaced the generic
315
+ // "Your request couldn't be completed" error indefinitely.
316
+ try {
317
+ // Compute the next run FIRST: a malformed cronExpression makes
318
+ // getNextRunTime throw, and it must throw BEFORE any insert so a
319
+ // poison row creates no task at all (the catch below does not
320
+ // roll back an insert that already succeeded).
321
+ const nextRunAt = getNextRunTime(recurring.cronExpression, now);
322
+
323
+ // Create the task
324
+ await ctx.db.insert("tasks", {
325
+ title: recurring.title,
326
+ description: recurring.description,
327
+ assignedTo: recurring.assignedTo,
328
+ priority: recurring.priority,
329
+ project: recurring.project,
330
+ tags: recurring.tags,
331
+ status: "todo",
332
+ createdBy: recurring.createdBy,
333
+ createdAt: now,
334
+ updatedAt: now,
335
+ });
336
+
337
+ // Update the recurring task
338
+ await ctx.db.patch(recurring._id, {
339
+ lastCreatedAt: now,
340
+ nextRunAt,
341
+ updatedAt: now,
342
+ });
343
+
344
+ created++;
345
+ } catch (err) {
346
+ failed++;
347
+ console.error(
348
+ `Recurring tasks: skipped row ${recurring._id} — ${
349
+ err instanceof Error ? err.message : String(err)
350
+ }`,
351
+ );
352
+ continue;
353
+ }
354
+ }
355
+
356
+ if (created > 0) {
357
+ console.log(`Recurring tasks: created ${created} task(s)`);
358
+ }
359
+ if (failed > 0) {
360
+ console.error(
361
+ `Recurring tasks: ${failed} row(s) skipped due to per-row errors`,
362
+ );
363
+ }
364
+
365
+ return { created, failed };
366
+ },
367
+ });
368
+
369
+ // ─────────────────────────────────────────────────────────────────────────────
370
+ // Day 100 — Phase 2 get_by_id surface fix (task k172735brsw6bc3j2dkkkfxqrx88kkjq)
371
+ // Single-row read by Convex doc ID. MCP layer applies scope-aware filter.
372
+ // ─────────────────────────────────────────────────────────────────────────────
373
+
374
+ export const getById = query({
375
+ args: { recurringTaskId: v.string() },
376
+ handler: async (ctx, args) => {
377
+ const recurringTaskId = requireId(
378
+ ctx,
379
+ "recurringTasks",
380
+ args.recurringTaskId,
381
+ "recurringTaskId",
382
+ RECURRING_TASK_ID_HINT,
383
+ );
384
+ return await ctx.db.get(recurringTaskId);
385
+ },
386
+ });