@homenshum/convex-mcp-nodebench 0.4.1 → 0.8.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/dist/index.js +293 -4
- package/dist/tools/actionAuditTools.d.ts +2 -0
- package/dist/tools/actionAuditTools.js +180 -0
- package/dist/tools/authorizationTools.d.ts +2 -0
- package/dist/tools/authorizationTools.js +201 -0
- package/dist/tools/dataModelingTools.d.ts +2 -0
- package/dist/tools/dataModelingTools.js +168 -0
- package/dist/tools/deploymentTools.js +42 -2
- package/dist/tools/devSetupTools.d.ts +2 -0
- package/dist/tools/devSetupTools.js +170 -0
- package/dist/tools/embeddingProvider.d.ts +6 -0
- package/dist/tools/embeddingProvider.js +3 -0
- package/dist/tools/functionTools.js +24 -1
- package/dist/tools/httpTools.js +128 -48
- package/dist/tools/migrationTools.d.ts +2 -0
- package/dist/tools/migrationTools.js +133 -0
- package/dist/tools/paginationTools.d.ts +2 -0
- package/dist/tools/paginationTools.js +125 -0
- package/dist/tools/qualityGateTools.d.ts +2 -0
- package/dist/tools/qualityGateTools.js +204 -0
- package/dist/tools/queryEfficiencyTools.d.ts +2 -0
- package/dist/tools/queryEfficiencyTools.js +191 -0
- package/dist/tools/reportingTools.d.ts +2 -0
- package/dist/tools/reportingTools.js +240 -0
- package/dist/tools/schedulerTools.d.ts +2 -0
- package/dist/tools/schedulerTools.js +197 -0
- package/dist/tools/schemaTools.js +95 -1
- package/dist/tools/storageAuditTools.d.ts +2 -0
- package/dist/tools/storageAuditTools.js +148 -0
- package/dist/tools/toolRegistry.d.ts +4 -0
- package/dist/tools/toolRegistry.js +274 -11
- package/dist/tools/transactionSafetyTools.d.ts +2 -0
- package/dist/tools/transactionSafetyTools.js +166 -0
- package/dist/tools/typeSafetyTools.d.ts +2 -0
- package/dist/tools/typeSafetyTools.js +146 -0
- package/dist/tools/vectorSearchTools.d.ts +2 -0
- package/dist/tools/vectorSearchTools.js +192 -0
- package/dist/types.d.ts +6 -1
- package/package.json +1 -1
|
@@ -261,6 +261,230 @@ export const REGISTRY = [
|
|
|
261
261
|
phase: "meta",
|
|
262
262
|
complexity: "low",
|
|
263
263
|
},
|
|
264
|
+
// ── Authorization Tools ──────────────────
|
|
265
|
+
{
|
|
266
|
+
name: "convex_audit_authorization",
|
|
267
|
+
category: "function",
|
|
268
|
+
tags: ["auth", "authorization", "security", "getUserIdentity", "public", "mutation", "permission"],
|
|
269
|
+
quickRef: {
|
|
270
|
+
nextAction: "Fix critical auth issues: add getUserIdentity() checks to public mutations that write data",
|
|
271
|
+
nextTools: ["convex_audit_functions", "convex_audit_actions"],
|
|
272
|
+
methodology: "convex_function_compliance",
|
|
273
|
+
relatedGotchas: ["internal_for_private"],
|
|
274
|
+
confidence: "high",
|
|
275
|
+
},
|
|
276
|
+
phase: "audit",
|
|
277
|
+
complexity: "high",
|
|
278
|
+
},
|
|
279
|
+
// ── Query Efficiency Tools ────────────────
|
|
280
|
+
{
|
|
281
|
+
name: "convex_audit_query_efficiency",
|
|
282
|
+
category: "schema",
|
|
283
|
+
tags: ["query", "performance", "collect", "filter", "index", "pagination", "efficiency", "table-scan"],
|
|
284
|
+
quickRef: {
|
|
285
|
+
nextAction: "Add .take() limits to unbounded .collect() calls and indexes for .filter() patterns",
|
|
286
|
+
nextTools: ["convex_suggest_indexes", "convex_audit_pagination"],
|
|
287
|
+
methodology: "convex_index_optimization",
|
|
288
|
+
relatedGotchas: ["index_field_order"],
|
|
289
|
+
confidence: "high",
|
|
290
|
+
},
|
|
291
|
+
phase: "audit",
|
|
292
|
+
complexity: "medium",
|
|
293
|
+
},
|
|
294
|
+
// ── Action Audit Tools ────────────────────
|
|
295
|
+
{
|
|
296
|
+
name: "convex_audit_actions",
|
|
297
|
+
category: "function",
|
|
298
|
+
tags: ["action", "ctx.db", "use-node", "fetch", "error-handling", "external-api", "runtime"],
|
|
299
|
+
quickRef: {
|
|
300
|
+
nextAction: "Fix critical action issues: remove ctx.db access, add 'use node' directives, wrap external calls in try/catch",
|
|
301
|
+
nextTools: ["convex_audit_functions", "convex_audit_transaction_safety"],
|
|
302
|
+
methodology: "convex_function_compliance",
|
|
303
|
+
relatedGotchas: ["action_from_action"],
|
|
304
|
+
confidence: "high",
|
|
305
|
+
},
|
|
306
|
+
phase: "audit",
|
|
307
|
+
complexity: "medium",
|
|
308
|
+
},
|
|
309
|
+
// ── Type Safety Tools ─────────────────────
|
|
310
|
+
{
|
|
311
|
+
name: "convex_check_type_safety",
|
|
312
|
+
category: "function",
|
|
313
|
+
tags: ["type", "safety", "as-any", "undefined", "null", "id", "generated", "typescript"],
|
|
314
|
+
quickRef: {
|
|
315
|
+
nextAction: "Replace `as any` casts with proper types and use v.id() instead of v.string() for ID fields",
|
|
316
|
+
nextTools: ["convex_check_validator_coverage", "convex_audit_functions"],
|
|
317
|
+
methodology: "convex_function_compliance",
|
|
318
|
+
relatedGotchas: [],
|
|
319
|
+
confidence: "high",
|
|
320
|
+
},
|
|
321
|
+
phase: "audit",
|
|
322
|
+
complexity: "medium",
|
|
323
|
+
},
|
|
324
|
+
// ── Transaction Safety Tools ──────────────
|
|
325
|
+
{
|
|
326
|
+
name: "convex_audit_transaction_safety",
|
|
327
|
+
category: "function",
|
|
328
|
+
tags: ["transaction", "atomicity", "race-condition", "TOCTOU", "runMutation", "consistency"],
|
|
329
|
+
quickRef: {
|
|
330
|
+
nextAction: "Combine multiple runMutation calls into single atomic mutation to avoid partial failures",
|
|
331
|
+
nextTools: ["convex_audit_actions", "convex_audit_functions"],
|
|
332
|
+
methodology: "convex_function_compliance",
|
|
333
|
+
relatedGotchas: [],
|
|
334
|
+
confidence: "medium",
|
|
335
|
+
},
|
|
336
|
+
phase: "audit",
|
|
337
|
+
complexity: "high",
|
|
338
|
+
},
|
|
339
|
+
// ── Storage Audit Tools ───────────────────
|
|
340
|
+
{
|
|
341
|
+
name: "convex_audit_storage_usage",
|
|
342
|
+
category: "function",
|
|
343
|
+
tags: ["storage", "file", "upload", "blob", "getUrl", "orphan", "null-check"],
|
|
344
|
+
quickRef: {
|
|
345
|
+
nextAction: "Add null checks for storage.get()/getUrl() and implement file cleanup on record deletion",
|
|
346
|
+
nextTools: ["convex_audit_functions", "convex_check_type_safety"],
|
|
347
|
+
methodology: "convex_function_compliance",
|
|
348
|
+
relatedGotchas: [],
|
|
349
|
+
confidence: "high",
|
|
350
|
+
},
|
|
351
|
+
phase: "audit",
|
|
352
|
+
complexity: "low",
|
|
353
|
+
},
|
|
354
|
+
// ── Pagination Tools ──────────────────────
|
|
355
|
+
{
|
|
356
|
+
name: "convex_audit_pagination",
|
|
357
|
+
category: "schema",
|
|
358
|
+
tags: ["pagination", "paginate", "cursor", "numItems", "paginationOptsValidator", "limit"],
|
|
359
|
+
quickRef: {
|
|
360
|
+
nextAction: "Add paginationOptsValidator to paginated queries and bound numItems",
|
|
361
|
+
nextTools: ["convex_audit_query_efficiency", "convex_suggest_indexes"],
|
|
362
|
+
methodology: "convex_index_optimization",
|
|
363
|
+
relatedGotchas: [],
|
|
364
|
+
confidence: "high",
|
|
365
|
+
},
|
|
366
|
+
phase: "audit",
|
|
367
|
+
complexity: "low",
|
|
368
|
+
},
|
|
369
|
+
// ── Data Modeling Tools ───────────────────
|
|
370
|
+
{
|
|
371
|
+
name: "convex_audit_data_modeling",
|
|
372
|
+
category: "schema",
|
|
373
|
+
tags: ["modeling", "schema", "nesting", "array", "v.id", "referential-integrity", "normalization"],
|
|
374
|
+
quickRef: {
|
|
375
|
+
nextAction: "Fix dangling v.id() references and consider normalizing deeply nested tables",
|
|
376
|
+
nextTools: ["convex_audit_schema", "convex_suggest_indexes"],
|
|
377
|
+
methodology: "convex_schema_audit",
|
|
378
|
+
relatedGotchas: ["system_fields_auto"],
|
|
379
|
+
confidence: "medium",
|
|
380
|
+
},
|
|
381
|
+
phase: "audit",
|
|
382
|
+
complexity: "medium",
|
|
383
|
+
},
|
|
384
|
+
// ── Dev Setup Tools ───────────────────────
|
|
385
|
+
{
|
|
386
|
+
name: "convex_audit_dev_setup",
|
|
387
|
+
category: "deployment",
|
|
388
|
+
tags: ["setup", "gitignore", "env", "tsconfig", "initialize", "onboarding", "convex-json"],
|
|
389
|
+
quickRef: {
|
|
390
|
+
nextAction: "Fix setup issues: update .gitignore, create .env.example, run npx convex dev",
|
|
391
|
+
nextTools: ["convex_bootstrap_project", "convex_pre_deploy_gate"],
|
|
392
|
+
methodology: "convex_deploy_verification",
|
|
393
|
+
relatedGotchas: [],
|
|
394
|
+
confidence: "high",
|
|
395
|
+
},
|
|
396
|
+
phase: "deploy",
|
|
397
|
+
complexity: "low",
|
|
398
|
+
},
|
|
399
|
+
// ── Migration Tools ───────────────────────
|
|
400
|
+
{
|
|
401
|
+
name: "convex_schema_migration_plan",
|
|
402
|
+
category: "schema",
|
|
403
|
+
tags: ["migration", "diff", "snapshot", "deploy", "risk", "breaking-change", "backup"],
|
|
404
|
+
quickRef: {
|
|
405
|
+
nextAction: "Review migration steps and back up data before deploying if risk is high",
|
|
406
|
+
nextTools: ["convex_snapshot_schema", "convex_pre_deploy_gate"],
|
|
407
|
+
methodology: "convex_deploy_verification",
|
|
408
|
+
relatedGotchas: [],
|
|
409
|
+
confidence: "high",
|
|
410
|
+
},
|
|
411
|
+
phase: "deploy",
|
|
412
|
+
complexity: "medium",
|
|
413
|
+
},
|
|
414
|
+
// ── Reporting Tools ─────────────────────
|
|
415
|
+
{
|
|
416
|
+
name: "convex_export_sarif",
|
|
417
|
+
category: "integration",
|
|
418
|
+
tags: ["sarif", "export", "report", "github", "code-scanning", "ci", "static-analysis"],
|
|
419
|
+
quickRef: {
|
|
420
|
+
nextAction: "Upload the SARIF file to GitHub Code Scanning or open in VS Code SARIF Viewer",
|
|
421
|
+
nextTools: ["convex_audit_diff", "convex_quality_gate"],
|
|
422
|
+
methodology: "convex_deploy_verification",
|
|
423
|
+
relatedGotchas: [],
|
|
424
|
+
confidence: "high",
|
|
425
|
+
},
|
|
426
|
+
phase: "deploy",
|
|
427
|
+
complexity: "low",
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
name: "convex_audit_diff",
|
|
431
|
+
category: "deployment",
|
|
432
|
+
tags: ["diff", "baseline", "trend", "new-issues", "fixed", "improving", "degrading", "comparison"],
|
|
433
|
+
quickRef: {
|
|
434
|
+
nextAction: "Focus on fixing new issues first, then tackle existing ones",
|
|
435
|
+
nextTools: ["convex_export_sarif", "convex_quality_gate"],
|
|
436
|
+
methodology: "convex_deploy_verification",
|
|
437
|
+
relatedGotchas: [],
|
|
438
|
+
confidence: "high",
|
|
439
|
+
},
|
|
440
|
+
phase: "deploy",
|
|
441
|
+
complexity: "medium",
|
|
442
|
+
},
|
|
443
|
+
// ── Vector Search Tools ─────────────────
|
|
444
|
+
{
|
|
445
|
+
name: "convex_audit_vector_search",
|
|
446
|
+
category: "schema",
|
|
447
|
+
tags: ["vector", "search", "embedding", "dimension", "similarity", "vectorIndex", "float64", "AI", "RAG"],
|
|
448
|
+
quickRef: {
|
|
449
|
+
nextAction: "Fix dimension mismatches and add filterFields to vector indexes for better performance",
|
|
450
|
+
nextTools: ["convex_audit_schema", "convex_suggest_indexes"],
|
|
451
|
+
methodology: "convex_schema_audit",
|
|
452
|
+
relatedGotchas: [],
|
|
453
|
+
confidence: "high",
|
|
454
|
+
},
|
|
455
|
+
phase: "audit",
|
|
456
|
+
complexity: "medium",
|
|
457
|
+
},
|
|
458
|
+
// ── Scheduler Tools ─────────────────────
|
|
459
|
+
{
|
|
460
|
+
name: "convex_audit_schedulers",
|
|
461
|
+
category: "function",
|
|
462
|
+
tags: ["scheduler", "runAfter", "runAt", "schedule", "cron", "infinite-loop", "backoff", "retry", "delayed"],
|
|
463
|
+
quickRef: {
|
|
464
|
+
nextAction: "Fix self-scheduling loops (add termination conditions) and implement exponential backoff",
|
|
465
|
+
nextTools: ["convex_check_crons", "convex_audit_actions"],
|
|
466
|
+
methodology: "convex_function_compliance",
|
|
467
|
+
relatedGotchas: [],
|
|
468
|
+
confidence: "high",
|
|
469
|
+
},
|
|
470
|
+
phase: "audit",
|
|
471
|
+
complexity: "medium",
|
|
472
|
+
},
|
|
473
|
+
// ── Quality Gate Tools ──────────────────
|
|
474
|
+
{
|
|
475
|
+
name: "convex_quality_gate",
|
|
476
|
+
category: "deployment",
|
|
477
|
+
tags: ["quality", "gate", "score", "grade", "threshold", "sonarqube", "metrics", "pass-fail", "A-F"],
|
|
478
|
+
quickRef: {
|
|
479
|
+
nextAction: "Fix blockers to raise your grade, then run again to verify improvement",
|
|
480
|
+
nextTools: ["convex_audit_diff", "convex_export_sarif", "convex_pre_deploy_gate"],
|
|
481
|
+
methodology: "convex_deploy_verification",
|
|
482
|
+
relatedGotchas: [],
|
|
483
|
+
confidence: "high",
|
|
484
|
+
},
|
|
485
|
+
phase: "deploy",
|
|
486
|
+
complexity: "high",
|
|
487
|
+
},
|
|
264
488
|
];
|
|
265
489
|
export function getQuickRef(toolName) {
|
|
266
490
|
const entry = REGISTRY.find((e) => e.name === toolName);
|
|
@@ -338,6 +562,10 @@ export function findTools(query) {
|
|
|
338
562
|
/**
|
|
339
563
|
* Async wrapper around findTools that fuses BM25 results with embedding RRF
|
|
340
564
|
* when a neural embedding provider is available. Falls back to plain findTools otherwise.
|
|
565
|
+
*
|
|
566
|
+
* Uses Agent-as-a-Graph bipartite RRF (arxiv:2511.18194):
|
|
567
|
+
* - Tool nodes get direct wRRF with α_T = 1.0
|
|
568
|
+
* - Domain nodes get stronger wRRF with α_D = 1.5 (paper-optimal, lifts sibling tools in that category)
|
|
341
569
|
*/
|
|
342
570
|
export async function findToolsWithEmbedding(query) {
|
|
343
571
|
const bm25Results = findTools(query);
|
|
@@ -346,22 +574,57 @@ export async function findToolsWithEmbedding(query) {
|
|
|
346
574
|
const queryVec = await embedQuery(query);
|
|
347
575
|
if (!queryVec)
|
|
348
576
|
return bm25Results;
|
|
349
|
-
const vecResults = embeddingSearch(queryVec,
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
577
|
+
const vecResults = embeddingSearch(queryVec, 30);
|
|
578
|
+
// Split embedding results by node type
|
|
579
|
+
const toolRanks = new Map();
|
|
580
|
+
const domainRanks = new Map();
|
|
581
|
+
let toolIdx = 0, domainIdx = 0;
|
|
582
|
+
for (const r of vecResults) {
|
|
583
|
+
if (r.nodeType === "domain") {
|
|
584
|
+
domainIdx++;
|
|
585
|
+
domainRanks.set(r.name.replace("domain:", ""), domainIdx);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
toolIdx++;
|
|
589
|
+
toolRanks.set(r.name, toolIdx);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// Type-specific wRRF: α_T for direct tool matches, α_D for domain matches
|
|
593
|
+
// Paper-optimal (arxiv:2511.18194): α_A=1.5, α_T=1.0, K=60
|
|
594
|
+
// Validated via 6-config ablation grid in mcp-local tools.test.ts
|
|
595
|
+
const ALPHA_T = 1.0; // tool weight
|
|
596
|
+
const ALPHA_D = 1.5; // domain weight (paper-optimal — upward traversal boost)
|
|
597
|
+
const K = 60; // RRF k parameter (paper-optimal)
|
|
598
|
+
// RRF fusion: combine BM25 rank with type-specific embedding ranks
|
|
353
599
|
const fusedScores = new Map();
|
|
354
600
|
bm25Results.forEach((entry, i) => {
|
|
355
|
-
const bm25Rrf = 1000 / (
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
|
|
601
|
+
const bm25Rrf = 1000 / (K + i + 1);
|
|
602
|
+
// Direct tool embedding match
|
|
603
|
+
const tRank = toolRanks.get(entry.name);
|
|
604
|
+
const toolRrf = tRank ? ALPHA_T * 1000 / (K + tRank) : 0;
|
|
605
|
+
// Domain-level embedding match (upward traversal: tool → category → domain node)
|
|
606
|
+
const dRank = domainRanks.get(entry.category);
|
|
607
|
+
const domainRrf = dRank ? ALPHA_D * 1000 / (K + dRank) : 0;
|
|
608
|
+
fusedScores.set(entry.name, bm25Rrf + toolRrf + domainRrf);
|
|
359
609
|
});
|
|
360
610
|
// Also include embedding-only hits not in BM25 results
|
|
361
|
-
for (const [name, rank] of
|
|
611
|
+
for (const [name, rank] of toolRanks) {
|
|
362
612
|
if (!fusedScores.has(name)) {
|
|
363
|
-
const
|
|
364
|
-
|
|
613
|
+
const toolRrf = ALPHA_T * 1000 / (K + rank);
|
|
614
|
+
// Look up domain boost for this tool
|
|
615
|
+
const entry = REGISTRY.find((e) => e.name === name);
|
|
616
|
+
const dRank = entry ? domainRanks.get(entry.category) : undefined;
|
|
617
|
+
const domainRrf = dRank ? ALPHA_D * 1000 / (K + dRank) : 0;
|
|
618
|
+
fusedScores.set(name, toolRrf + domainRrf);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
// Domain-only hits: boost all tools in a matched domain even without direct tool hit
|
|
622
|
+
for (const [category, dRank] of domainRanks) {
|
|
623
|
+
const domainRrf = ALPHA_D * 1000 / (K + dRank);
|
|
624
|
+
for (const entry of REGISTRY) {
|
|
625
|
+
if (entry.category === category && !fusedScores.has(entry.name)) {
|
|
626
|
+
fusedScores.set(entry.name, domainRrf);
|
|
627
|
+
}
|
|
365
628
|
}
|
|
366
629
|
}
|
|
367
630
|
// Re-sort by fused score
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { getDb, genId } from "../db.js";
|
|
4
|
+
import { getQuickRef } from "./toolRegistry.js";
|
|
5
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
6
|
+
function findConvexDir(projectDir) {
|
|
7
|
+
const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex")];
|
|
8
|
+
for (const c of candidates) {
|
|
9
|
+
if (existsSync(c))
|
|
10
|
+
return c;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
function collectTsFiles(dir) {
|
|
15
|
+
const results = [];
|
|
16
|
+
if (!existsSync(dir))
|
|
17
|
+
return results;
|
|
18
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const full = join(dir, entry.name);
|
|
21
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "_generated") {
|
|
22
|
+
results.push(...collectTsFiles(full));
|
|
23
|
+
}
|
|
24
|
+
else if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
25
|
+
results.push(full);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
30
|
+
function auditTransactionSafety(convexDir) {
|
|
31
|
+
const files = collectTsFiles(convexDir);
|
|
32
|
+
const issues = [];
|
|
33
|
+
let totalMutations = 0;
|
|
34
|
+
let readModifyWrite = 0;
|
|
35
|
+
let multipleRunMutation = 0;
|
|
36
|
+
let checkThenAct = 0;
|
|
37
|
+
for (const filePath of files) {
|
|
38
|
+
const content = readFileSync(filePath, "utf-8");
|
|
39
|
+
const relativePath = filePath.replace(convexDir, "").replace(/^[\\/]/, "");
|
|
40
|
+
const lines = content.split("\n");
|
|
41
|
+
// Find action/mutation bodies
|
|
42
|
+
const funcPattern = /export\s+(?:const\s+(\w+)\s*=|default)\s+(action|internalAction|mutation|internalMutation)\s*\(/g;
|
|
43
|
+
let m;
|
|
44
|
+
while ((m = funcPattern.exec(content)) !== null) {
|
|
45
|
+
const funcName = m[1] || "default";
|
|
46
|
+
const funcType = m[2];
|
|
47
|
+
if (funcType === "mutation" || funcType === "internalMutation")
|
|
48
|
+
totalMutations++;
|
|
49
|
+
const startLine = content.slice(0, m.index).split("\n").length - 1;
|
|
50
|
+
// Extract body
|
|
51
|
+
let depth = 0;
|
|
52
|
+
let foundOpen = false;
|
|
53
|
+
let endLine = Math.min(startLine + 100, lines.length);
|
|
54
|
+
for (let j = startLine; j < lines.length; j++) {
|
|
55
|
+
for (const ch of lines[j]) {
|
|
56
|
+
if (ch === "{") {
|
|
57
|
+
depth++;
|
|
58
|
+
foundOpen = true;
|
|
59
|
+
}
|
|
60
|
+
if (ch === "}")
|
|
61
|
+
depth--;
|
|
62
|
+
}
|
|
63
|
+
if (foundOpen && depth <= 0) {
|
|
64
|
+
endLine = j + 1;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const body = lines.slice(startLine, endLine).join("\n");
|
|
69
|
+
// Check 1: Multiple ctx.runMutation calls in an action (separate transactions)
|
|
70
|
+
if (funcType === "action" || funcType === "internalAction") {
|
|
71
|
+
const runMutationMatches = body.match(/ctx\.runMutation\s*\(/g);
|
|
72
|
+
if (runMutationMatches && runMutationMatches.length >= 2) {
|
|
73
|
+
multipleRunMutation++;
|
|
74
|
+
issues.push({
|
|
75
|
+
severity: "warning",
|
|
76
|
+
location: `${relativePath}:${startLine + 1}`,
|
|
77
|
+
functionName: funcName,
|
|
78
|
+
message: `${funcType} "${funcName}" calls ctx.runMutation ${runMutationMatches.length} times. Each is a separate transaction — if the second fails, the first is NOT rolled back.`,
|
|
79
|
+
fix: "Combine into a single mutation that does both operations atomically, or add idempotency handling",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Check 2: Read-modify-write in action (TOCTOU race)
|
|
84
|
+
if (funcType === "action" || funcType === "internalAction") {
|
|
85
|
+
const hasRunQuery = /ctx\.runQuery\s*\(/.test(body);
|
|
86
|
+
const hasRunMutation = /ctx\.runMutation\s*\(/.test(body);
|
|
87
|
+
if (hasRunQuery && hasRunMutation) {
|
|
88
|
+
// Potential read-then-write pattern — check if they reference similar data
|
|
89
|
+
readModifyWrite++;
|
|
90
|
+
issues.push({
|
|
91
|
+
severity: "warning",
|
|
92
|
+
location: `${relativePath}:${startLine + 1}`,
|
|
93
|
+
functionName: funcName,
|
|
94
|
+
message: `${funcType} "${funcName}" reads via runQuery then writes via runMutation. Between these calls, another client may have changed the data (TOCTOU race).`,
|
|
95
|
+
fix: "Move the read-modify-write into a single mutation for atomicity, or add optimistic concurrency checks",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Check 3: Check-then-act in mutations (get → check → modify)
|
|
100
|
+
if (funcType === "mutation" || funcType === "internalMutation") {
|
|
101
|
+
// Pattern: const x = await ctx.db.get(...); if (!x) throw; ctx.db.patch(x._id, ...)
|
|
102
|
+
const hasGet = /ctx\.db\.get\s*\(/.test(body);
|
|
103
|
+
const hasPatch = /ctx\.db\.(patch|replace|delete)\s*\(/.test(body);
|
|
104
|
+
const hasConditional = /if\s*\(\s*!?\w+/.test(body);
|
|
105
|
+
if (hasGet && hasPatch && hasConditional) {
|
|
106
|
+
// This is actually safe in Convex mutations (they're serializable), but flag for review
|
|
107
|
+
// Only flag if there are multiple get/patch patterns suggesting complexity
|
|
108
|
+
const getCount = (body.match(/ctx\.db\.get\s*\(/g) || []).length;
|
|
109
|
+
const patchCount = (body.match(/ctx\.db\.(patch|replace|delete)\s*\(/g) || []).length;
|
|
110
|
+
if (getCount >= 2 && patchCount >= 2) {
|
|
111
|
+
checkThenAct++;
|
|
112
|
+
issues.push({
|
|
113
|
+
severity: "info",
|
|
114
|
+
location: `${relativePath}:${startLine + 1}`,
|
|
115
|
+
functionName: funcName,
|
|
116
|
+
message: `${funcType} "${funcName}" has complex read-check-modify pattern (${getCount} reads, ${patchCount} writes). Convex mutations are serializable so this is safe, but consider simplifying.`,
|
|
117
|
+
fix: "Consider breaking into smaller, focused mutations if the logic is complex",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
issues,
|
|
126
|
+
stats: { totalMutations, readModifyWrite, multipleRunMutation, checkThenAct },
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// ── Tool Definition ─────────────────────────────────────────────────
|
|
130
|
+
export const transactionSafetyTools = [
|
|
131
|
+
{
|
|
132
|
+
name: "convex_audit_transaction_safety",
|
|
133
|
+
description: "Audit Convex functions for transaction safety: multiple ctx.runMutation calls in actions (separate transactions — partial failure risk), read-then-write patterns across query/mutation boundaries (TOCTOU races), and complex check-then-act mutations.",
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
projectDir: {
|
|
138
|
+
type: "string",
|
|
139
|
+
description: "Absolute path to the project root containing a convex/ directory",
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
required: ["projectDir"],
|
|
143
|
+
},
|
|
144
|
+
handler: async (args) => {
|
|
145
|
+
const projectDir = resolve(args.projectDir);
|
|
146
|
+
const convexDir = findConvexDir(projectDir);
|
|
147
|
+
if (!convexDir) {
|
|
148
|
+
return { error: "No convex/ directory found" };
|
|
149
|
+
}
|
|
150
|
+
const { issues, stats } = auditTransactionSafety(convexDir);
|
|
151
|
+
const db = getDb();
|
|
152
|
+
db.prepare("INSERT INTO audit_results (id, project_dir, audit_type, issues_json, issue_count) VALUES (?, ?, ?, ?, ?)").run(genId("audit"), projectDir, "transaction_safety", JSON.stringify(issues), issues.length);
|
|
153
|
+
return {
|
|
154
|
+
summary: {
|
|
155
|
+
...stats,
|
|
156
|
+
totalIssues: issues.length,
|
|
157
|
+
critical: issues.filter((i) => i.severity === "critical").length,
|
|
158
|
+
warnings: issues.filter((i) => i.severity === "warning").length,
|
|
159
|
+
},
|
|
160
|
+
issues: issues.slice(0, 30),
|
|
161
|
+
quickRef: getQuickRef("convex_audit_transaction_safety"),
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
//# sourceMappingURL=transactionSafetyTools.js.map
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { getDb, genId } from "../db.js";
|
|
4
|
+
import { getQuickRef } from "./toolRegistry.js";
|
|
5
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
6
|
+
function findConvexDir(projectDir) {
|
|
7
|
+
const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex")];
|
|
8
|
+
for (const c of candidates) {
|
|
9
|
+
if (existsSync(c))
|
|
10
|
+
return c;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
function collectTsFiles(dir) {
|
|
15
|
+
const results = [];
|
|
16
|
+
if (!existsSync(dir))
|
|
17
|
+
return results;
|
|
18
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const full = join(dir, entry.name);
|
|
21
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "_generated") {
|
|
22
|
+
results.push(...collectTsFiles(full));
|
|
23
|
+
}
|
|
24
|
+
else if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
25
|
+
results.push(full);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
30
|
+
function auditTypeSafety(convexDir) {
|
|
31
|
+
const files = collectTsFiles(convexDir);
|
|
32
|
+
const issues = [];
|
|
33
|
+
let filesWithAsAny = 0;
|
|
34
|
+
let asAnyCastCount = 0;
|
|
35
|
+
let undefinedReturns = 0;
|
|
36
|
+
let looseIdTypes = 0;
|
|
37
|
+
for (const filePath of files) {
|
|
38
|
+
const content = readFileSync(filePath, "utf-8");
|
|
39
|
+
const relativePath = filePath.replace(convexDir, "").replace(/^[\\/]/, "");
|
|
40
|
+
const lines = content.split("\n");
|
|
41
|
+
let fileHasAsAny = false;
|
|
42
|
+
for (let i = 0; i < lines.length; i++) {
|
|
43
|
+
const line = lines[i];
|
|
44
|
+
if (line.trim().startsWith("//") || line.trim().startsWith("*"))
|
|
45
|
+
continue;
|
|
46
|
+
// Check 1: `as any` casts
|
|
47
|
+
const asAnyMatches = line.match(/as\s+any\b/g);
|
|
48
|
+
if (asAnyMatches) {
|
|
49
|
+
asAnyCastCount += asAnyMatches.length;
|
|
50
|
+
if (!fileHasAsAny) {
|
|
51
|
+
fileHasAsAny = true;
|
|
52
|
+
filesWithAsAny++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Check 2: return undefined (should be null in Convex)
|
|
56
|
+
if (/return\s+undefined\b/.test(line)) {
|
|
57
|
+
undefinedReturns++;
|
|
58
|
+
issues.push({
|
|
59
|
+
severity: "warning",
|
|
60
|
+
location: `${relativePath}:${i + 1}`,
|
|
61
|
+
message: "Returning undefined — Convex serializes undefined differently from null. Use null explicitly.",
|
|
62
|
+
fix: "Replace `return undefined` with `return null`",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// Check 3: Using string type where Id<'table'> should be
|
|
66
|
+
// Heuristic: args with names like *Id that use v.string() instead of v.id("table")
|
|
67
|
+
const idArgMatch = line.match(/(\w+Id)\s*:\s*v\.string\s*\(\s*\)/);
|
|
68
|
+
if (idArgMatch) {
|
|
69
|
+
looseIdTypes++;
|
|
70
|
+
issues.push({
|
|
71
|
+
severity: "warning",
|
|
72
|
+
location: `${relativePath}:${i + 1}`,
|
|
73
|
+
message: `Arg "${idArgMatch[1]}" uses v.string() but looks like an ID field. Use v.id("tableName") for type-safe ID references.`,
|
|
74
|
+
fix: `Change ${idArgMatch[1]}: v.string() to ${idArgMatch[1]}: v.id("tableName")`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// Check 4: Manual interface/type definitions that shadow generated types
|
|
78
|
+
if (/(?:interface|type)\s+(Doc|Id|DataModel|FunctionReference)\b/.test(line)) {
|
|
79
|
+
issues.push({
|
|
80
|
+
severity: "critical",
|
|
81
|
+
location: `${relativePath}:${i + 1}`,
|
|
82
|
+
message: "Manual type definition shadows Convex generated type. Import from _generated/dataModel instead.",
|
|
83
|
+
fix: 'Import from "_generated/dataModel" or "_generated/server" instead of defining manually',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Aggregate `as any` per file
|
|
88
|
+
if (fileHasAsAny) {
|
|
89
|
+
const count = (content.match(/as\s+any\b/g) || []).length;
|
|
90
|
+
issues.push({
|
|
91
|
+
severity: "warning",
|
|
92
|
+
location: relativePath,
|
|
93
|
+
message: `${count} \`as any\` cast(s) in this file. Each cast bypasses Convex's automatic type safety.`,
|
|
94
|
+
fix: "Replace `as any` with proper typing or use Convex generated types",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
issues,
|
|
100
|
+
stats: {
|
|
101
|
+
totalFiles: files.length,
|
|
102
|
+
filesWithAsAny,
|
|
103
|
+
asAnyCastCount,
|
|
104
|
+
undefinedReturns,
|
|
105
|
+
looseIdTypes,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// ── Tool Definition ─────────────────────────────────────────────────
|
|
110
|
+
export const typeSafetyTools = [
|
|
111
|
+
{
|
|
112
|
+
name: "convex_check_type_safety",
|
|
113
|
+
description: "Check Convex code for type safety issues: `as any` casts (bypass generated types), returning undefined instead of null, v.string() where v.id() should be used, and manual type definitions shadowing Convex generated types.",
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: {
|
|
117
|
+
projectDir: {
|
|
118
|
+
type: "string",
|
|
119
|
+
description: "Absolute path to the project root containing a convex/ directory",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
required: ["projectDir"],
|
|
123
|
+
},
|
|
124
|
+
handler: async (args) => {
|
|
125
|
+
const projectDir = resolve(args.projectDir);
|
|
126
|
+
const convexDir = findConvexDir(projectDir);
|
|
127
|
+
if (!convexDir) {
|
|
128
|
+
return { error: "No convex/ directory found" };
|
|
129
|
+
}
|
|
130
|
+
const { issues, stats } = auditTypeSafety(convexDir);
|
|
131
|
+
const db = getDb();
|
|
132
|
+
db.prepare("INSERT INTO audit_results (id, project_dir, audit_type, issues_json, issue_count) VALUES (?, ?, ?, ?, ?)").run(genId("audit"), projectDir, "type_safety", JSON.stringify(issues), issues.length);
|
|
133
|
+
return {
|
|
134
|
+
summary: {
|
|
135
|
+
...stats,
|
|
136
|
+
totalIssues: issues.length,
|
|
137
|
+
critical: issues.filter((i) => i.severity === "critical").length,
|
|
138
|
+
warnings: issues.filter((i) => i.severity === "warning").length,
|
|
139
|
+
},
|
|
140
|
+
issues: issues.slice(0, 30),
|
|
141
|
+
quickRef: getQuickRef("convex_check_type_safety"),
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
//# sourceMappingURL=typeSafetyTools.js.map
|