@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.
- package/README.md +88 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +94 -0
- package/dist/contract.d.ts +18 -0
- package/dist/contract.js +29 -0
- package/dist/coverage-cli.d.ts +2 -0
- package/dist/coverage-cli.js +17 -0
- package/dist/coverage.d.ts +32 -0
- package/dist/coverage.js +77 -0
- package/dist/detectors/oracle-axis1.d.ts +86 -0
- package/dist/detectors/oracle-axis1.js +221 -0
- package/dist/detectors/oracle-axis2.d.ts +22 -0
- package/dist/detectors/oracle-axis2.js +149 -0
- package/dist/detectors/predicates.d.ts +19 -0
- package/dist/detectors/predicates.js +334 -0
- package/dist/detectors/shared.d.ts +56 -0
- package/dist/detectors/shared.js +103 -0
- package/dist/detectors/source/_scan.d.ts +43 -0
- package/dist/detectors/source/_scan.js +82 -0
- package/dist/detectors/source/backfill.d.ts +27 -0
- package/dist/detectors/source/backfill.js +211 -0
- package/dist/detectors/source/contract.d.ts +33 -0
- package/dist/detectors/source/contract.js +385 -0
- package/dist/detectors/source/identity.d.ts +47 -0
- package/dist/detectors/source/identity.js +410 -0
- package/dist/detectors/source/jobs.d.ts +10 -0
- package/dist/detectors/source/jobs.js +209 -0
- package/dist/detectors/source/lifecycle.d.ts +6 -0
- package/dist/detectors/source/lifecycle.js +147 -0
- package/dist/detectors/source/readbudget.d.ts +6 -0
- package/dist/detectors/source/readbudget.js +133 -0
- package/dist/detectors/source/refusal.d.ts +6 -0
- package/dist/detectors/source/refusal.js +125 -0
- package/dist/detectors/source/security.d.ts +37 -0
- package/dist/detectors/source/security.js +392 -0
- package/dist/detectors/source/writesafety.d.ts +14 -0
- package/dist/detectors/source/writesafety.js +345 -0
- package/dist/detectors/standard/_doc.d.ts +66 -0
- package/dist/detectors/standard/_doc.js +152 -0
- package/dist/detectors/standard/r41-frontier.d.ts +58 -0
- package/dist/detectors/standard/r41-frontier.js +176 -0
- package/dist/detectors/standard/r46-returns-validator.d.ts +61 -0
- package/dist/detectors/standard/r46-returns-validator.js +474 -0
- package/dist/detectors/standard/standard-doc.d.ts +12 -0
- package/dist/detectors/standard/standard-doc.js +271 -0
- package/dist/detectors/structural.d.ts +14 -0
- package/dist/detectors/structural.js +83 -0
- package/dist/engine-types.d.ts +10 -0
- package/dist/engine-types.js +1 -0
- package/dist/not-checkable.d.ts +22 -0
- package/dist/not-checkable.js +37 -0
- package/dist/oracle.d.ts +33 -0
- package/dist/oracle.js +133 -0
- package/dist/registry.d.ts +11 -0
- package/dist/registry.js +140 -0
- package/dist/report.d.ts +35 -0
- package/dist/report.js +138 -0
- package/dist/severity.d.ts +30 -0
- package/dist/severity.js +121 -0
- package/dist/tally.d.ts +57 -0
- package/dist/tally.js +152 -0
- package/dist/trigger.d.ts +47 -0
- package/dist/trigger.js +143 -0
- package/fixtures/backend-standard-51ec7af1.md +648 -0
- package/fixtures/backend-standard-a9d7877b.md +629 -0
- package/fixtures/mcp-app-standard-a74a042b.md +279 -0
- package/fixtures/mcp-extension-standard-a74a042b.md +259 -0
- package/fixtures/mcp-server-standard-a74a042b.md +434 -0
- package/fixtures/mcp-standard-a74a042b.md +354 -0
- package/fixtures/vp-1200-green/.pin +1 -0
- package/fixtures/vp-1200-green/convex/migrations.ts +350 -0
- package/fixtures/vp-1200-green/convex/schema.ts +1243 -0
- package/fixtures/vp-1200-red/.pin +1 -0
- package/fixtures/vp-1200-red/convex/migrations.ts +239 -0
- package/fixtures/vp-1200-red/convex/schema.ts +1243 -0
- package/fixtures/vp-by-tool-day158.csv +190 -0
- package/fixtures/vp-convex/.github/workflows/npm-publish.yml +163 -0
- package/fixtures/vp-convex/.pin +1 -0
- package/fixtures/vp-convex/bun.lock +783 -0
- package/fixtures/vp-convex/convex/auth.config.ts +8 -0
- package/fixtures/vp-convex/convex/briefingNotes.ts +321 -0
- package/fixtures/vp-convex/convex/convex.config.ts +14 -0
- package/fixtures/vp-convex/convex/credentials.ts +591 -0
- package/fixtures/vp-convex/convex/crons.ts +72 -0
- package/fixtures/vp-convex/convex/dashboard.ts +199 -0
- package/fixtures/vp-convex/convex/gumroadWebhook.ts +815 -0
- package/fixtures/vp-convex/convex/issues.ts +544 -0
- package/fixtures/vp-convex/convex/kb.ts +305 -0
- package/fixtures/vp-convex/convex/kbMutations.ts +192 -0
- package/fixtures/vp-convex/convex/kbShared.ts +56 -0
- package/fixtures/vp-convex/convex/licenses.ts +443 -0
- package/fixtures/vp-convex/convex/memories.ts +406 -0
- package/fixtures/vp-convex/convex/memoriesScoped.ts +220 -0
- package/fixtures/vp-convex/convex/messages.ts +971 -0
- package/fixtures/vp-convex/convex/migrations.ts +239 -0
- package/fixtures/vp-convex/convex/missionTemplates.ts +369 -0
- package/fixtures/vp-convex/convex/oauth.ts +1608 -0
- package/fixtures/vp-convex/convex/oauthMigrations.ts +96 -0
- package/fixtures/vp-convex/convex/okfBundleNode.ts +1031 -0
- package/fixtures/vp-convex/convex/recurringTasks.ts +386 -0
- package/fixtures/vp-convex/convex/schema.ts +1229 -0
- package/fixtures/vp-convex/convex/tasks.ts +2782 -0
- package/fixtures/vp-r31-green/.pin +1 -0
- package/fixtures/vp-r31-green/convex/migrations.ts +350 -0
- package/fixtures/vp-r31-red/.pin +1 -0
- package/fixtures/vp-r31-red/convex/migrations.ts +286 -0
- package/fixtures/vr-by-tool-day159.csv +89 -0
- 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
|
+
});
|