@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,8 @@
1
+ export default {
2
+ providers: [
3
+ {
4
+ domain: process.env.CLERK_JWT_ISSUER_DOMAIN,
5
+ applicationID: "convex",
6
+ },
7
+ ],
8
+ };
@@ -0,0 +1,321 @@
1
+ import { v } from "convex/values";
2
+ import { ConvexError } from "convex/values";
3
+ import { mutation, query } from "./_generated/server";
4
+ import type { Doc } from "./_generated/dataModel";
5
+ import { creatorValidator } from "./schema";
6
+ import { withOrgScope, filterByOrgScope, requireScope } from "./lib/auth";
7
+ import { requireId } from "./lib/ids";
8
+
9
+ // ─────────────────────────────────────────────────────────────────────────────
10
+ // create — insert a new briefing note
11
+ // ─────────────────────────────────────────────────────────────────────────────
12
+
13
+ export const create = mutation({
14
+ args: {
15
+ title: v.string(),
16
+ topic: v.string(),
17
+ participants: v.array(v.string()),
18
+ content: v.string(),
19
+ decisions: v.optional(v.array(v.string())),
20
+ linkedMemoryIds: v.optional(v.array(v.id("memories"))),
21
+ createdBy: creatorValidator,
22
+ },
23
+ returns: v.id("briefingNotes"),
24
+ handler: async (ctx, args) => {
25
+ return await ctx.db.insert("briefingNotes", {
26
+ ...args,
27
+ createdAt: Date.now(),
28
+ });
29
+ },
30
+ });
31
+
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+ // get — fetch a single briefing note by ID
34
+ // ─────────────────────────────────────────────────────────────────────────────
35
+
36
+ export const get = query({
37
+ // Accept a raw string, not `v.id("briefingNotes")`: the v.id() validator
38
+ // runs BEFORE the handler, so a wrong-table ID is rejected with a message
39
+ // Convex redacts in prod (`Server Error`, `error.data` undefined —
40
+ // measured). Narrowing inside the handler via requireId() throws a
41
+ // ConvexError whose payload survives redaction. Same contract as PR #1072
42
+ // (tasks.getById).
43
+ args: { noteId: v.string() },
44
+ returns: v.union(
45
+ v.object({
46
+ _id: v.id("briefingNotes"),
47
+ _creationTime: v.number(),
48
+ title: v.string(),
49
+ topic: v.string(),
50
+ participants: v.array(v.string()),
51
+ content: v.string(),
52
+ decisions: v.optional(v.array(v.string())),
53
+ linkedMemoryIds: v.optional(v.array(v.id("memories"))),
54
+ createdBy: creatorValidator,
55
+ createdAt: v.number(),
56
+ updatedAt: v.optional(v.number()),
57
+ updatedBy: v.optional(creatorValidator),
58
+ // PR #360 — Beta multi-tenant scope field. Optional so pre-PR #360 docs pass.
59
+ orgId: v.optional(v.string()),
60
+ }),
61
+ v.null(),
62
+ ),
63
+ handler: async (ctx, args) => {
64
+ const noteId = requireId(
65
+ ctx,
66
+ "briefingNotes",
67
+ args.noteId,
68
+ "noteId",
69
+ "Use the full 32-char noteId returned by list_briefing_notes or create_briefing_note.",
70
+ );
71
+ return await ctx.db.get(noteId);
72
+ },
73
+ });
74
+
75
+ // ─────────────────────────────────────────────────────────────────────────────
76
+ // Lite projection helper
77
+ // ─────────────────────────────────────────────────────────────────────────────
78
+
79
+ type BriefingNoteLite = {
80
+ _id: string;
81
+ _creationTime: number;
82
+ topic: string;
83
+ title: string;
84
+ participants: string[];
85
+ createdBy: string;
86
+ };
87
+
88
+ function projectBriefingNoteLite(doc: Doc<"briefingNotes">): BriefingNoteLite {
89
+ return {
90
+ _id: doc._id,
91
+ _creationTime: doc._creationTime,
92
+ topic: doc.topic,
93
+ title: doc.title,
94
+ participants: doc.participants,
95
+ createdBy: doc.createdBy,
96
+ };
97
+ }
98
+
99
+ // ─────────────────────────────────────────────────────────────────────────────
100
+ // list — list briefing notes, optional topic filter, ordered by createdAt desc
101
+ //
102
+ // New in v1.1:
103
+ // fields="lite" — compact projection: {_id,_creationTime,topic,title,participants,createdBy}
104
+ // fields="full" (default) — full doc (backward-compatible)
105
+ // ─────────────────────────────────────────────────────────────────────────────
106
+
107
+ // updatedSince widened-scan fix (same defect class as #1110 on billing, and
108
+ // convex/tasks.ts `list`/`listByMission`, convex/missions.ts `list`): the
109
+ // filter used to run in-memory after a `.take(limit)` that had already
110
+ // bounded the page in creation-descending order — a note updated recently
111
+ // but created outside that page was invisible while the response looked
112
+ // complete. When updatedSince is supplied, the fetch is widened to
113
+ // BRIEFING_NOTES_LIST_SCAN_CAP + 1 rows before the filter runs, then
114
+ // re-sliced to `limit`. If the widened scan itself hits its cap, we refuse
115
+ // to return a silently-incomplete page.
116
+ export const BRIEFING_NOTES_LIST_SCAN_CAP = 2000;
117
+
118
+ export const list = query({
119
+ args: {
120
+ topic: v.optional(v.string()),
121
+ limit: v.optional(v.number()),
122
+ fields: v.optional(v.union(v.literal("lite"), v.literal("full"))),
123
+ updatedSince: v.optional(v.number()),
124
+ // S3.3 B8 — cursor paging anchor (forward pagination, newest-first).
125
+ createdBefore: v.optional(v.number()),
126
+ },
127
+ // Returns validator omitted because union of full+lite produces overly strict types vs Doc<"briefingNotes"> optionality
128
+ handler: async (ctx, args) => {
129
+ const lite = args.fields === "lite";
130
+ // v2.3.3 — auto-clamp limit when fields=full + no explicit limit
131
+ const explicitLimit = args.limit !== undefined;
132
+ let limit = args.limit ?? 20;
133
+ if (!explicitLimit && !lite) {
134
+ limit = 15;
135
+ console.warn(
136
+ `[briefingNotes.list] auto-clamp: limit=15 applied (fields=full, no explicit limit).`,
137
+ );
138
+ }
139
+ const needsWideScan = args.updatedSince !== undefined;
140
+ const fetchCap = needsWideScan ? BRIEFING_NOTES_LIST_SCAN_CAP + 1 : limit;
141
+
142
+ let rows: Doc<"briefingNotes">[];
143
+
144
+ if (args.topic !== undefined) {
145
+ rows = await ctx.db
146
+ .query("briefingNotes")
147
+ .withIndex("by_topic", (q) => q.eq("topic", args.topic as string))
148
+ .order("desc")
149
+ .take(fetchCap);
150
+ } else {
151
+ rows = await ctx.db.query("briefingNotes").order("desc").take(fetchCap);
152
+ }
153
+
154
+ // Refuse to return a silently-incomplete page: if the widened scan
155
+ // itself hit its cap, there may be matching rows we never looked at.
156
+ // "I couldn't measure" must never render identically to "complete".
157
+ // No branch here was measured to exceed the cap in production (unlike
158
+ // tasks.list's assignedTo branches), so no index was added and the
159
+ // fetch is still a fixed-size widened scan — "shrink the updatedSince
160
+ // window" would be a false remedy and is left out of the message.
161
+ if (needsWideScan && rows.length > BRIEFING_NOTES_LIST_SCAN_CAP) {
162
+ throw new ConvexError(
163
+ `briefingNotes.list: SCAN_CAP_EXCEEDED — widened scan for updatedSince hit the cap of ${BRIEFING_NOTES_LIST_SCAN_CAP} candidate rows before the filter ran. The result would be incomplete and indistinguishable from a full match. Narrow with topic.`,
164
+ );
165
+ }
166
+
167
+ // v2.3.3 — updatedSince filter on updatedAt (fallback to _creationTime if missing)
168
+ if (args.updatedSince !== undefined) {
169
+ const since = args.updatedSince;
170
+ rows = rows.filter(
171
+ (r) => (r.updatedAt ?? r._creationTime) >= since,
172
+ );
173
+ }
174
+ // Re-bound to the requested page size now that the filter has run over
175
+ // the widened superset (no-op when a wide scan wasn't needed).
176
+ rows = rows.slice(0, limit);
177
+ // S3.3 B8 — cursor paging anchor: drop rows newer-or-equal to before.
178
+ if (args.createdBefore !== undefined) {
179
+ const before = args.createdBefore;
180
+ rows = rows.filter((r) => r._creationTime < before);
181
+ }
182
+
183
+ if (lite) return rows.map(projectBriefingNoteLite);
184
+ return rows;
185
+ },
186
+ });
187
+
188
+ // ─────────────────────────────────────────────────────────────────────────────
189
+ // deleteBriefingNote — hard delete a briefing note by ID
190
+ // RBAC: callerOrchestrator must match note.createdBy or be "system"
191
+ // Pass callerOrchestrator=undefined to bypass (server-to-server / admin use).
192
+ // ─────────────────────────────────────────────────────────────────────────────
193
+
194
+ export const deleteBriefingNote = mutation({
195
+ args: {
196
+ noteId: v.id("briefingNotes"),
197
+ callerOrchestrator: v.optional(creatorValidator),
198
+ },
199
+ returns: v.object({ deleted: v.boolean() }),
200
+ handler: async (ctx, args) => {
201
+ const note = await ctx.db.get(args.noteId);
202
+ if (!note) throw new Error("Briefing note not found");
203
+
204
+ if (args.callerOrchestrator === undefined) {
205
+ throw new Error(
206
+ "Unauthorized: callerOrchestrator is required to delete a briefing note — omitting it is refused, not exempted",
207
+ );
208
+ }
209
+ if (
210
+ args.callerOrchestrator !== "system" &&
211
+ note.createdBy !== args.callerOrchestrator
212
+ ) {
213
+ throw new Error(
214
+ `Unauthorized: only ${note.createdBy} (creator) or system can delete this briefing note`,
215
+ );
216
+ }
217
+
218
+ await ctx.db.delete(args.noteId);
219
+ return { deleted: true };
220
+ },
221
+ });
222
+
223
+ // ─────────────────────────────────────────────────────────────────────────────
224
+ // update — partial update of any mutable briefing note field
225
+ // RBAC deny-by-default: callerOrchestrator MUST be createdBy or "system"
226
+ // ─────────────────────────────────────────────────────────────────────────────
227
+
228
+ export const update = mutation({
229
+ args: {
230
+ noteId: v.id("briefingNotes"),
231
+ callerOrchestrator: creatorValidator, // REQUIRED — deny-by-default per memory j573cwcs3znp0xsvtg34x435jh84b0eg
232
+ title: v.optional(v.string()),
233
+ topic: v.optional(v.string()),
234
+ participants: v.optional(v.array(v.string())),
235
+ content: v.optional(v.string()),
236
+ decisions: v.optional(v.array(v.string())),
237
+ linkedMemoryIds: v.optional(v.array(v.id("memories"))),
238
+ },
239
+ returns: v.null(),
240
+ handler: async (ctx, args) => {
241
+ const { noteId, callerOrchestrator, ...fields } = args;
242
+ const note = await ctx.db.get(noteId);
243
+ if (note === null) {
244
+ throw new Error(`BriefingNote ${noteId} not found`);
245
+ }
246
+ const isAuthorized =
247
+ note.createdBy === callerOrchestrator || callerOrchestrator === "system";
248
+ if (!isAuthorized) {
249
+ throw new Error(
250
+ `Unauthorized: ${callerOrchestrator} is not creator of this briefing note`,
251
+ );
252
+ }
253
+ const patch: Record<string, unknown> = {
254
+ updatedAt: Date.now(),
255
+ updatedBy: callerOrchestrator,
256
+ };
257
+ for (const [key, value] of Object.entries(fields)) {
258
+ if (value !== undefined) {
259
+ patch[key] = value;
260
+ }
261
+ }
262
+ await ctx.db.patch(noteId, patch);
263
+ return null;
264
+ },
265
+ });
266
+
267
+ // ─────────────────────────────────────────────────────────────────────────────
268
+ // Day 102 v2.11.0 — CRUD baseline PR-C-bis option B (mission k575kc1r).
269
+ // BM25 keyword search over briefing note content via Convex native .searchIndex().
270
+ //
271
+ // Backed by the `search_content` searchIndex declared in schema.ts.
272
+ // Filter axes: topic, createdBy.
273
+ // ─────────────────────────────────────────────────────────────────────────────
274
+
275
+ export const searchBriefingNotesByKeyword = query({
276
+ args: {
277
+ query: v.string(),
278
+ topic: v.optional(v.string()),
279
+ createdBy: v.optional(creatorValidator),
280
+ limit: v.optional(v.number()),
281
+ fields: v.optional(v.union(v.literal("lite"), v.literal("full"))),
282
+ },
283
+ handler: async (ctx, args) => {
284
+ const scope = await withOrgScope(ctx);
285
+ requireScope(scope, "view-own-tasks");
286
+
287
+ const limit = Math.min(Math.max(args.limit ?? 20, 1), 200);
288
+ const lite = args.fields === "lite";
289
+
290
+ const results = await ctx.db
291
+ .query("briefingNotes")
292
+ .withSearchIndex("search_content", (q) => {
293
+ let qb = q.search("content", args.query);
294
+ if (args.topic !== undefined) qb = qb.eq("topic", args.topic);
295
+ if (args.createdBy !== undefined)
296
+ qb = qb.eq("createdBy", args.createdBy);
297
+ if (!scope.isMaster && scope.orgSlug !== null) {
298
+ qb = qb.eq("orgId", scope.orgSlug);
299
+ }
300
+ return qb;
301
+ })
302
+ .take(limit);
303
+
304
+ // Defense-in-depth: briefingNotes have no pilot/assignedTo so
305
+ // filterByOrgScope() does not fit. Enforce orgId match inline for
306
+ // non-master scopes — the index .eq("orgId", scope.orgSlug) above
307
+ // is the primary isolation; this is the belt-and-suspenders pass.
308
+ const filtered = scope.isMaster
309
+ ? results
310
+ : results.filter((r) => r.orgId === scope.orgSlug);
311
+
312
+ if (!lite) return filtered;
313
+ return filtered.map((b) => ({
314
+ _id: b._id,
315
+ title: b.title,
316
+ topic: b.topic,
317
+ createdBy: b.createdBy,
318
+ createdAt: b.createdAt,
319
+ }));
320
+ },
321
+ });
@@ -0,0 +1,14 @@
1
+ import { defineApp } from "convex/server";
2
+ import rag from "@convex-dev/rag/convex.config.js";
3
+ // Durable long-task engine (I1 — long-task survival). Verified real (not
4
+ // the empty-shell alpha.1 dist-tag): dist/component/engine/durableJob.js
5
+ // exports start/getStatus/cancel/getJobInternal/runStepInternal. Pinned
6
+ // exactly at 0.1.0-alpha.2 in package.json — do NOT loosen to `^`/`@alpha`,
7
+ // `latest` still resolves to the empty shell.
8
+ import agentEngine from "@vantageos/agent-engine/convex.config";
9
+
10
+ const app = defineApp();
11
+ app.use(rag);
12
+ app.use(agentEngine); // registered under the default name "agentEngine"
13
+
14
+ export default app;