@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,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
+ });