@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 @@
|
|
|
1
|
+
8b2c7b6295d3988e383c5b30e942de4d0c9c8ad5
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { internal } from "./_generated/api";
|
|
3
|
+
import { internalMutation } from "./_generated/server";
|
|
4
|
+
import { syncParticipantIndex } from "./briefingNotes";
|
|
5
|
+
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
// Migration: backfill startedAt + completedAt on tasks
|
|
8
|
+
// Run: npx convex run migrations:backfillTaskTimes
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export const backfillTaskTimes = internalMutation({
|
|
12
|
+
args: {},
|
|
13
|
+
handler: async (ctx) => {
|
|
14
|
+
const tasks = await ctx.db.query("tasks").collect();
|
|
15
|
+
let startedFixed = 0;
|
|
16
|
+
let completedFixed = 0;
|
|
17
|
+
|
|
18
|
+
for (const task of tasks) {
|
|
19
|
+
const patch: Record<string, any> = {};
|
|
20
|
+
|
|
21
|
+
// Backfill startedAt for in_progress/done/review tasks
|
|
22
|
+
if (
|
|
23
|
+
!task.startedAt &&
|
|
24
|
+
(task.status === "in_progress" || task.status === "done" || task.status === "review")
|
|
25
|
+
) {
|
|
26
|
+
patch.startedAt = task.createdAt;
|
|
27
|
+
startedFixed++;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Backfill completedAt for done tasks
|
|
31
|
+
if (!task.completedAt && task.status === "done") {
|
|
32
|
+
patch.completedAt = task.updatedAt;
|
|
33
|
+
completedFixed++;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Calculate actualMinutes if both timestamps exist
|
|
37
|
+
const startedAt = patch.startedAt ?? task.startedAt;
|
|
38
|
+
const completedAt = patch.completedAt ?? task.completedAt;
|
|
39
|
+
if (startedAt && completedAt && !task.actualMinutes) {
|
|
40
|
+
patch.actualMinutes = Math.round((completedAt - startedAt) / 60_000);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (Object.keys(patch).length > 0) {
|
|
44
|
+
await ctx.db.patch(task._id, patch);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(`Backfilled: ${startedFixed} startedAt, ${completedFixed} completedAt`);
|
|
49
|
+
return { startedFixed, completedFixed, total: tasks.length };
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
// Cleanup: delete test briefing notes (MCP Test Briefing entries)
|
|
55
|
+
// Run: npx convex run migrations:deleteTestBriefings
|
|
56
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
export const deleteTestBriefings = internalMutation({
|
|
59
|
+
args: {},
|
|
60
|
+
handler: async (ctx) => {
|
|
61
|
+
const notes = await ctx.db.query("briefingNotes").collect();
|
|
62
|
+
let deleted = 0;
|
|
63
|
+
|
|
64
|
+
for (const note of notes) {
|
|
65
|
+
if (note.title === "MCP Test Briefing") {
|
|
66
|
+
await ctx.db.delete(note._id);
|
|
67
|
+
deleted++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(`Deleted ${deleted} test briefing notes`);
|
|
72
|
+
return { deleted };
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
77
|
+
// Cleanup: delete test tasks created by MCP tester
|
|
78
|
+
// Run: npx convex run migrations:deleteTestTasks
|
|
79
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
export const deleteTestTasks = internalMutation({
|
|
82
|
+
args: {},
|
|
83
|
+
handler: async (ctx) => {
|
|
84
|
+
const tasks = await ctx.db.query("tasks").collect();
|
|
85
|
+
let deleted = 0;
|
|
86
|
+
|
|
87
|
+
for (const task of tasks) {
|
|
88
|
+
if (
|
|
89
|
+
task.title === "Test task from MCP tester" ||
|
|
90
|
+
task.title === "Mission-linked task from MCP tester"
|
|
91
|
+
) {
|
|
92
|
+
await ctx.db.delete(task._id);
|
|
93
|
+
deleted++;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(`Deleted ${deleted} test tasks`);
|
|
98
|
+
return { deleted };
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
// Cleanup: delete test missions created by MCP tester
|
|
104
|
+
// Run: npx convex run migrations:deleteTestMissions
|
|
105
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
export const deleteTestMissions = internalMutation({
|
|
108
|
+
args: {},
|
|
109
|
+
handler: async (ctx) => {
|
|
110
|
+
const missions = await ctx.db.query("missions").collect();
|
|
111
|
+
let deleted = 0;
|
|
112
|
+
|
|
113
|
+
for (const mission of missions) {
|
|
114
|
+
if (
|
|
115
|
+
mission.name === "Test mission from MCP tester" ||
|
|
116
|
+
mission.name === "Updated test mission"
|
|
117
|
+
) {
|
|
118
|
+
await ctx.db.delete(mission._id);
|
|
119
|
+
deleted++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log(`Deleted ${deleted} test missions`);
|
|
124
|
+
return { deleted };
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
129
|
+
// Cleanup: delete test memories from test/mcp-tester namespace
|
|
130
|
+
// Run: npx convex run migrations:deleteTestMemories
|
|
131
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
export const deleteTestMemories = internalMutation({
|
|
134
|
+
args: {},
|
|
135
|
+
handler: async (ctx) => {
|
|
136
|
+
const memories = await ctx.db
|
|
137
|
+
.query("memories")
|
|
138
|
+
.withIndex("by_namespace", (q) => q.eq("namespace", "test/mcp-tester"))
|
|
139
|
+
.collect();
|
|
140
|
+
let deleted = 0;
|
|
141
|
+
|
|
142
|
+
for (const mem of memories) {
|
|
143
|
+
await ctx.db.delete(mem._id);
|
|
144
|
+
deleted++;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(`Deleted ${deleted} test memories from test/mcp-tester`);
|
|
148
|
+
return { deleted };
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
153
|
+
// Cleanup: delete test messages from MCP tester
|
|
154
|
+
// Run: npx convex run migrations:deleteTestMessages
|
|
155
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
export const deleteTestMessages = internalMutation({
|
|
158
|
+
args: {},
|
|
159
|
+
handler: async (ctx) => {
|
|
160
|
+
const messages = await ctx.db.query("messages").collect();
|
|
161
|
+
let deleted = 0;
|
|
162
|
+
|
|
163
|
+
for (const msg of messages) {
|
|
164
|
+
if (msg.content === "Test message from MCP tester") {
|
|
165
|
+
// Delete receipts first
|
|
166
|
+
const receipts = await ctx.db
|
|
167
|
+
.query("messageReceipts")
|
|
168
|
+
.withIndex("by_message", (q) => q.eq("messageId", msg._id))
|
|
169
|
+
.collect();
|
|
170
|
+
for (const receipt of receipts) {
|
|
171
|
+
await ctx.db.delete(receipt._id);
|
|
172
|
+
}
|
|
173
|
+
await ctx.db.delete(msg._id);
|
|
174
|
+
deleted++;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log(`Deleted ${deleted} test messages and their receipts`);
|
|
179
|
+
return { deleted };
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
184
|
+
// Migration: rename memory namespace "orchestrator/pi" → "orchestrator/sigma"
|
|
185
|
+
// Run: npx convex run migrations:migrateMemoriesNamespace
|
|
186
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
export const migrateMemoriesNamespace = internalMutation({
|
|
189
|
+
args: {},
|
|
190
|
+
handler: async (ctx) => {
|
|
191
|
+
const memories = await ctx.db
|
|
192
|
+
.query("memories")
|
|
193
|
+
.withIndex("by_namespace", (q) => q.eq("namespace", "orchestrator/pi"))
|
|
194
|
+
.collect();
|
|
195
|
+
let migrated = 0;
|
|
196
|
+
|
|
197
|
+
for (const mem of memories) {
|
|
198
|
+
await ctx.db.patch(mem._id, { namespace: "orchestrator/sigma" });
|
|
199
|
+
migrated++;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
console.log(`Migrated ${migrated} memories from orchestrator/pi to orchestrator/sigma`);
|
|
203
|
+
return { migrated };
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
208
|
+
// Migration: change diary orchestrator "pi" → "sigma" for VPS entries
|
|
209
|
+
// Run: npx convex run migrations:migrateDiaryOrchestrator
|
|
210
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
export const migrateDiaryOrchestrator = internalMutation({
|
|
213
|
+
args: {},
|
|
214
|
+
handler: async (ctx) => {
|
|
215
|
+
const entries = await ctx.db
|
|
216
|
+
.query("diary")
|
|
217
|
+
.withIndex("by_orchestrator_date", (q) => q.eq("orchestrator", "pi"))
|
|
218
|
+
.collect();
|
|
219
|
+
let migrated = 0;
|
|
220
|
+
let skipped = 0;
|
|
221
|
+
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
const content = entry.content.toLowerCase();
|
|
224
|
+
const isVpsRelated =
|
|
225
|
+
content.includes("pi-vps") ||
|
|
226
|
+
content.includes("sigma") ||
|
|
227
|
+
content.includes("vps");
|
|
228
|
+
|
|
229
|
+
if (isVpsRelated) {
|
|
230
|
+
await ctx.db.patch(entry._id, { orchestrator: "sigma" });
|
|
231
|
+
migrated++;
|
|
232
|
+
} else {
|
|
233
|
+
skipped++;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
console.log(
|
|
238
|
+
`Diary migration: ${migrated} migrated to sigma, ${skipped} kept as pi`
|
|
239
|
+
);
|
|
240
|
+
return { migrated, skipped };
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
245
|
+
// Migration: backfill briefingNoteParticipants for pre-existing briefingNotes
|
|
246
|
+
// rows (task k178gg7wp3cre87mgw60trtpfh8cqfn3, Pi, urgent BLOCKER).
|
|
247
|
+
//
|
|
248
|
+
// PRODUCTION FINDING: the junction table is written only by the create/update
|
|
249
|
+
// mutations (Day 165 fix, k175ga65p654z200ydj7s8qv5s8cnxfc). Any note that
|
|
250
|
+
// existed BEFORE that fix deployed has zero rows in briefingNoteParticipants,
|
|
251
|
+
// so a scoped participant reading it is indistinguishable from a scoped
|
|
252
|
+
// caller reading a note they never participated in — both return null.
|
|
253
|
+
//
|
|
254
|
+
// This migration walks every briefingNotes row and re-runs
|
|
255
|
+
// syncParticipantIndex(ctx, note._id, note.participants) — the SAME function
|
|
256
|
+
// create/update call, so there is no second writer and no parallel
|
|
257
|
+
// convention to drift from the live code path.
|
|
258
|
+
//
|
|
259
|
+
// Idempotent: syncParticipantIndex deletes all existing rows for the note
|
|
260
|
+
// (by_note index) before re-inserting the deduped participant set, so
|
|
261
|
+
// running this migration N times leaves the junction table row count
|
|
262
|
+
// unchanged after the first run (verified below and in the paired test).
|
|
263
|
+
//
|
|
264
|
+
// PRODUCTION INCIDENT (task k17byc17kzyyra886h6v08ky3n8cqfqm, Pi, P0):
|
|
265
|
+
// the original one-shot `.collect()` form THREW on prod — "Too many bytes
|
|
266
|
+
// read in a single function execution (limit: 16777216 bytes)". A
|
|
267
|
+
// briefingNotes row carries kilobytes of `content` (schema: v.string(),
|
|
268
|
+
// full briefing text), and Convex reads cannot project fields — even a
|
|
269
|
+
// `.paginate()` page loads full documents. `.collect()` over the whole
|
|
270
|
+
// table exceeds the 16 MiB read-byte budget once the corpus + per-row
|
|
271
|
+
// content size grows past it; the loop below only ever needs `_id` and
|
|
272
|
+
// `participants`, but `content` is read anyway and is what exhausts the
|
|
273
|
+
// budget.
|
|
274
|
+
//
|
|
275
|
+
// FIX: bounded pages + self-scheduling. Each transaction processes at most
|
|
276
|
+
// BATCH rows via `.paginate()`, then reschedules itself with the
|
|
277
|
+
// continuation cursor via ctx.scheduler.runAfter(0, ...) until isDone. No
|
|
278
|
+
// single execution's read-byte footprint depends on corpus size anymore —
|
|
279
|
+
// only on BATCH.
|
|
280
|
+
//
|
|
281
|
+
// BATCH = 16, justified against the 16 MiB (16,777,216 byte) per-execution
|
|
282
|
+
// read budget:
|
|
283
|
+
// - Convex documents are capped at ~1 MiB (1,048,576 bytes) per document
|
|
284
|
+
// (Convex platform limit), so a single briefingNotes row's absolute
|
|
285
|
+
// worst case is ~1 MiB.
|
|
286
|
+
// - BATCH(16) x worst-case-row(1 MiB) = 16 MiB, which is the size of the
|
|
287
|
+
// ENTIRE read budget with the assumption every row hits the document
|
|
288
|
+
// size cap. Real briefingNotes.content rows are "kilobytes", not
|
|
289
|
+
// megabytes (per the Pi finding), so in practice a page of 16 rows
|
|
290
|
+
// reads a small fraction of a MiB — leaving comfortable headroom for
|
|
291
|
+
// the query's own index-read overhead and the paginate() cursor
|
|
292
|
+
// bookkeeping, while still being small enough that even a genuinely
|
|
293
|
+
// pathological page (several rows near the 1 MiB cap) cannot approach
|
|
294
|
+
// the limit.
|
|
295
|
+
// - Kept intentionally conservative (not e.g. 1000) precisely because
|
|
296
|
+
// Convex cannot skip loading `content` — the field that caused the
|
|
297
|
+
// original incident — so the only lever available is page size.
|
|
298
|
+
//
|
|
299
|
+
// Run (kicks off the drain; it self-schedules until the corpus is fully
|
|
300
|
+
// processed):
|
|
301
|
+
// npx convex run migrations:backfillBriefingNoteParticipants
|
|
302
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
const BACKFILL_BATCH_SIZE = 16;
|
|
305
|
+
|
|
306
|
+
export const backfillBriefingNoteParticipants = internalMutation({
|
|
307
|
+
args: {
|
|
308
|
+
cursor: v.optional(v.union(v.string(), v.null())),
|
|
309
|
+
},
|
|
310
|
+
returns: v.object({
|
|
311
|
+
notesProcessed: v.number(),
|
|
312
|
+
participantRowsWritten: v.number(),
|
|
313
|
+
isDone: v.boolean(),
|
|
314
|
+
}),
|
|
315
|
+
handler: async (ctx, args) => {
|
|
316
|
+
const page = await ctx.db
|
|
317
|
+
.query("briefingNotes")
|
|
318
|
+
.paginate({
|
|
319
|
+
cursor: args.cursor ?? null,
|
|
320
|
+
numItems: BACKFILL_BATCH_SIZE,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
let participantRowsWritten = 0;
|
|
324
|
+
for (const note of page.page) {
|
|
325
|
+
await syncParticipantIndex(ctx, note._id, note.participants);
|
|
326
|
+
participantRowsWritten += new Set(note.participants).size;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!page.isDone) {
|
|
330
|
+
await ctx.scheduler.runAfter(
|
|
331
|
+
0,
|
|
332
|
+
internal.migrations.backfillBriefingNoteParticipants,
|
|
333
|
+
{ cursor: page.continueCursor },
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
console.log(
|
|
338
|
+
`Backfilled briefingNoteParticipants page: ${page.page.length} notes processed, ${participantRowsWritten} participant rows written, isDone=${page.isDone}`
|
|
339
|
+
);
|
|
340
|
+
// NOTE (per Pi): this return value is a per-page count, not a
|
|
341
|
+
// corpus-wide total — the drain spans multiple scheduled
|
|
342
|
+
// executions. The proof of completion is an independent read of
|
|
343
|
+
// the briefingNoteParticipants table, never this return value.
|
|
344
|
+
return {
|
|
345
|
+
notesProcessed: page.page.length,
|
|
346
|
+
participantRowsWritten,
|
|
347
|
+
isDone: page.isDone,
|
|
348
|
+
};
|
|
349
|
+
},
|
|
350
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
f19e43ca0c5a59269601a131356eec861a2d1afd
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { internalMutation } from "./_generated/server";
|
|
3
|
+
import { syncParticipantIndex } from "./briefingNotes";
|
|
4
|
+
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
// Migration: backfill startedAt + completedAt on tasks
|
|
7
|
+
// Run: npx convex run migrations:backfillTaskTimes
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export const backfillTaskTimes = internalMutation({
|
|
11
|
+
args: {},
|
|
12
|
+
handler: async (ctx) => {
|
|
13
|
+
const tasks = await ctx.db.query("tasks").collect();
|
|
14
|
+
let startedFixed = 0;
|
|
15
|
+
let completedFixed = 0;
|
|
16
|
+
|
|
17
|
+
for (const task of tasks) {
|
|
18
|
+
const patch: Record<string, any> = {};
|
|
19
|
+
|
|
20
|
+
// Backfill startedAt for in_progress/done/review tasks
|
|
21
|
+
if (
|
|
22
|
+
!task.startedAt &&
|
|
23
|
+
(task.status === "in_progress" || task.status === "done" || task.status === "review")
|
|
24
|
+
) {
|
|
25
|
+
patch.startedAt = task.createdAt;
|
|
26
|
+
startedFixed++;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Backfill completedAt for done tasks
|
|
30
|
+
if (!task.completedAt && task.status === "done") {
|
|
31
|
+
patch.completedAt = task.updatedAt;
|
|
32
|
+
completedFixed++;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Calculate actualMinutes if both timestamps exist
|
|
36
|
+
const startedAt = patch.startedAt ?? task.startedAt;
|
|
37
|
+
const completedAt = patch.completedAt ?? task.completedAt;
|
|
38
|
+
if (startedAt && completedAt && !task.actualMinutes) {
|
|
39
|
+
patch.actualMinutes = Math.round((completedAt - startedAt) / 60_000);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (Object.keys(patch).length > 0) {
|
|
43
|
+
await ctx.db.patch(task._id, patch);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(`Backfilled: ${startedFixed} startedAt, ${completedFixed} completedAt`);
|
|
48
|
+
return { startedFixed, completedFixed, total: tasks.length };
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
53
|
+
// Cleanup: delete test briefing notes (MCP Test Briefing entries)
|
|
54
|
+
// Run: npx convex run migrations:deleteTestBriefings
|
|
55
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
export const deleteTestBriefings = internalMutation({
|
|
58
|
+
args: {},
|
|
59
|
+
handler: async (ctx) => {
|
|
60
|
+
const notes = await ctx.db.query("briefingNotes").collect();
|
|
61
|
+
let deleted = 0;
|
|
62
|
+
|
|
63
|
+
for (const note of notes) {
|
|
64
|
+
if (note.title === "MCP Test Briefing") {
|
|
65
|
+
await ctx.db.delete(note._id);
|
|
66
|
+
deleted++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(`Deleted ${deleted} test briefing notes`);
|
|
71
|
+
return { deleted };
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
76
|
+
// Cleanup: delete test tasks created by MCP tester
|
|
77
|
+
// Run: npx convex run migrations:deleteTestTasks
|
|
78
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
export const deleteTestTasks = internalMutation({
|
|
81
|
+
args: {},
|
|
82
|
+
handler: async (ctx) => {
|
|
83
|
+
const tasks = await ctx.db.query("tasks").collect();
|
|
84
|
+
let deleted = 0;
|
|
85
|
+
|
|
86
|
+
for (const task of tasks) {
|
|
87
|
+
if (
|
|
88
|
+
task.title === "Test task from MCP tester" ||
|
|
89
|
+
task.title === "Mission-linked task from MCP tester"
|
|
90
|
+
) {
|
|
91
|
+
await ctx.db.delete(task._id);
|
|
92
|
+
deleted++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(`Deleted ${deleted} test tasks`);
|
|
97
|
+
return { deleted };
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
102
|
+
// Cleanup: delete test missions created by MCP tester
|
|
103
|
+
// Run: npx convex run migrations:deleteTestMissions
|
|
104
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
export const deleteTestMissions = internalMutation({
|
|
107
|
+
args: {},
|
|
108
|
+
handler: async (ctx) => {
|
|
109
|
+
const missions = await ctx.db.query("missions").collect();
|
|
110
|
+
let deleted = 0;
|
|
111
|
+
|
|
112
|
+
for (const mission of missions) {
|
|
113
|
+
if (
|
|
114
|
+
mission.name === "Test mission from MCP tester" ||
|
|
115
|
+
mission.name === "Updated test mission"
|
|
116
|
+
) {
|
|
117
|
+
await ctx.db.delete(mission._id);
|
|
118
|
+
deleted++;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log(`Deleted ${deleted} test missions`);
|
|
123
|
+
return { deleted };
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
128
|
+
// Cleanup: delete test memories from test/mcp-tester namespace
|
|
129
|
+
// Run: npx convex run migrations:deleteTestMemories
|
|
130
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
export const deleteTestMemories = internalMutation({
|
|
133
|
+
args: {},
|
|
134
|
+
handler: async (ctx) => {
|
|
135
|
+
const memories = await ctx.db
|
|
136
|
+
.query("memories")
|
|
137
|
+
.withIndex("by_namespace", (q) => q.eq("namespace", "test/mcp-tester"))
|
|
138
|
+
.collect();
|
|
139
|
+
let deleted = 0;
|
|
140
|
+
|
|
141
|
+
for (const mem of memories) {
|
|
142
|
+
await ctx.db.delete(mem._id);
|
|
143
|
+
deleted++;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(`Deleted ${deleted} test memories from test/mcp-tester`);
|
|
147
|
+
return { deleted };
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
152
|
+
// Cleanup: delete test messages from MCP tester
|
|
153
|
+
// Run: npx convex run migrations:deleteTestMessages
|
|
154
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
export const deleteTestMessages = internalMutation({
|
|
157
|
+
args: {},
|
|
158
|
+
handler: async (ctx) => {
|
|
159
|
+
const messages = await ctx.db.query("messages").collect();
|
|
160
|
+
let deleted = 0;
|
|
161
|
+
|
|
162
|
+
for (const msg of messages) {
|
|
163
|
+
if (msg.content === "Test message from MCP tester") {
|
|
164
|
+
// Delete receipts first
|
|
165
|
+
const receipts = await ctx.db
|
|
166
|
+
.query("messageReceipts")
|
|
167
|
+
.withIndex("by_message", (q) => q.eq("messageId", msg._id))
|
|
168
|
+
.collect();
|
|
169
|
+
for (const receipt of receipts) {
|
|
170
|
+
await ctx.db.delete(receipt._id);
|
|
171
|
+
}
|
|
172
|
+
await ctx.db.delete(msg._id);
|
|
173
|
+
deleted++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(`Deleted ${deleted} test messages and their receipts`);
|
|
178
|
+
return { deleted };
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
183
|
+
// Migration: rename memory namespace "orchestrator/pi" → "orchestrator/sigma"
|
|
184
|
+
// Run: npx convex run migrations:migrateMemoriesNamespace
|
|
185
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
export const migrateMemoriesNamespace = internalMutation({
|
|
188
|
+
args: {},
|
|
189
|
+
handler: async (ctx) => {
|
|
190
|
+
const memories = await ctx.db
|
|
191
|
+
.query("memories")
|
|
192
|
+
.withIndex("by_namespace", (q) => q.eq("namespace", "orchestrator/pi"))
|
|
193
|
+
.collect();
|
|
194
|
+
let migrated = 0;
|
|
195
|
+
|
|
196
|
+
for (const mem of memories) {
|
|
197
|
+
await ctx.db.patch(mem._id, { namespace: "orchestrator/sigma" });
|
|
198
|
+
migrated++;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log(`Migrated ${migrated} memories from orchestrator/pi to orchestrator/sigma`);
|
|
202
|
+
return { migrated };
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
207
|
+
// Migration: change diary orchestrator "pi" → "sigma" for VPS entries
|
|
208
|
+
// Run: npx convex run migrations:migrateDiaryOrchestrator
|
|
209
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
export const migrateDiaryOrchestrator = internalMutation({
|
|
212
|
+
args: {},
|
|
213
|
+
handler: async (ctx) => {
|
|
214
|
+
const entries = await ctx.db
|
|
215
|
+
.query("diary")
|
|
216
|
+
.withIndex("by_orchestrator_date", (q) => q.eq("orchestrator", "pi"))
|
|
217
|
+
.collect();
|
|
218
|
+
let migrated = 0;
|
|
219
|
+
let skipped = 0;
|
|
220
|
+
|
|
221
|
+
for (const entry of entries) {
|
|
222
|
+
const content = entry.content.toLowerCase();
|
|
223
|
+
const isVpsRelated =
|
|
224
|
+
content.includes("pi-vps") ||
|
|
225
|
+
content.includes("sigma") ||
|
|
226
|
+
content.includes("vps");
|
|
227
|
+
|
|
228
|
+
if (isVpsRelated) {
|
|
229
|
+
await ctx.db.patch(entry._id, { orchestrator: "sigma" });
|
|
230
|
+
migrated++;
|
|
231
|
+
} else {
|
|
232
|
+
skipped++;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
console.log(
|
|
237
|
+
`Diary migration: ${migrated} migrated to sigma, ${skipped} kept as pi`
|
|
238
|
+
);
|
|
239
|
+
return { migrated, skipped };
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
244
|
+
// Migration: backfill briefingNoteParticipants for pre-existing briefingNotes
|
|
245
|
+
// rows (task k178gg7wp3cre87mgw60trtpfh8cqfn3, Pi, urgent BLOCKER).
|
|
246
|
+
//
|
|
247
|
+
// PRODUCTION FINDING: the junction table is written only by the create/update
|
|
248
|
+
// mutations (Day 165 fix, k175ga65p654z200ydj7s8qv5s8cnxfc). Any note that
|
|
249
|
+
// existed BEFORE that fix deployed has zero rows in briefingNoteParticipants,
|
|
250
|
+
// so a scoped participant reading it is indistinguishable from a scoped
|
|
251
|
+
// caller reading a note they never participated in — both return null.
|
|
252
|
+
//
|
|
253
|
+
// This migration walks every briefingNotes row and re-runs
|
|
254
|
+
// syncParticipantIndex(ctx, note._id, note.participants) — the SAME function
|
|
255
|
+
// create/update call, so there is no second writer and no parallel
|
|
256
|
+
// convention to drift from the live code path.
|
|
257
|
+
//
|
|
258
|
+
// Idempotent: syncParticipantIndex deletes all existing rows for the note
|
|
259
|
+
// (by_note index) before re-inserting the deduped participant set, so
|
|
260
|
+
// running this migration N times leaves the junction table row count
|
|
261
|
+
// unchanged after the first run (verified below and in the paired test).
|
|
262
|
+
//
|
|
263
|
+
// Run: npx convex run migrations:backfillBriefingNoteParticipants
|
|
264
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
export const backfillBriefingNoteParticipants = internalMutation({
|
|
267
|
+
args: {},
|
|
268
|
+
returns: v.object({
|
|
269
|
+
notesProcessed: v.number(),
|
|
270
|
+
participantRowsWritten: v.number(),
|
|
271
|
+
}),
|
|
272
|
+
handler: async (ctx) => {
|
|
273
|
+
const notes = await ctx.db.query("briefingNotes").collect();
|
|
274
|
+
let participantRowsWritten = 0;
|
|
275
|
+
|
|
276
|
+
for (const note of notes) {
|
|
277
|
+
await syncParticipantIndex(ctx, note._id, note.participants);
|
|
278
|
+
participantRowsWritten += new Set(note.participants).size;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
console.log(
|
|
282
|
+
`Backfilled briefingNoteParticipants: ${notes.length} notes processed, ${participantRowsWritten} participant rows written`
|
|
283
|
+
);
|
|
284
|
+
return { notesProcessed: notes.length, participantRowsWritten };
|
|
285
|
+
},
|
|
286
|
+
});
|