@homenshum/convex-mcp-nodebench 0.9.4 → 0.9.5

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 CHANGED
@@ -79,7 +79,7 @@ for (const tool of ALL_TOOLS) {
79
79
  // ── Server setup ────────────────────────────────────────────────────
80
80
  const server = new Server({
81
81
  name: "convex-mcp-nodebench",
82
- version: "0.9.4",
82
+ version: "0.9.5",
83
83
  }, {
84
84
  capabilities: {
85
85
  tools: {},
@@ -72,7 +72,10 @@ function auditActions(convexDir) {
72
72
  }
73
73
  const body = lines.slice(startLine, endLine).join("\n");
74
74
  // Check 1: ctx.db access in action (FATAL — not allowed)
75
- if (/ctx\.db\.(get|query|insert|patch|replace|delete)\s*\(/.test(body)) {
75
+ // BUT: skip if ctx.db is inside an inline ctx.runMutation/ctx.runQuery callback
76
+ // e.g. ctx.runMutation(async (ctx) => { ctx.db.patch(...) }) — the inner ctx is a mutation context
77
+ const hasInlineCallback = /ctx\.run(Mutation|Query)\s*\(\s*async\s*\(/.test(body);
78
+ if (/ctx\.db\.(get|query|insert|patch|replace|delete)\s*\(/.test(body) && !hasInlineCallback) {
76
79
  actionsWithDbAccess++;
77
80
  issues.push({
78
81
  severity: "critical",
@@ -81,7 +81,8 @@ function auditAuthorization(convexDir) {
81
81
  const identityAssign = body.match(/(?:const|let)\s+(\w+)\s*=\s*await\s+ctx\.auth\.getUserIdentity\s*\(\s*\)/);
82
82
  if (identityAssign) {
83
83
  const varName = identityAssign[1];
84
- const hasNullCheck = new RegExp(`if\\s*\\(\\s*!${varName}\\b|if\\s*\\(\\s*${varName}\\s*===?\\s*null|if\\s*\\(\\s*!${varName}\\s*\\)`).test(body);
84
+ // Recognize: if (!var), if (var === null), if (var), var &&, var ?, throw on !var
85
+ const hasNullCheck = new RegExp(`if\\s*\\(\\s*!${varName}\\b|if\\s*\\(\\s*${varName}\\s*===?\\s*null|if\\s*\\(\\s*${varName}\\s*\\)|${varName}\\s*&&|${varName}\\s*\\?|throw.*!${varName}|${varName}\\s*!==?\\s*null`).test(body);
85
86
  if (!hasNullCheck) {
86
87
  uncheckedIdentity++;
87
88
  issues.push({
@@ -97,7 +98,8 @@ function auditAuthorization(convexDir) {
97
98
  const authUserAssign = body.match(/(?:const|let)\s+(\w+)\s*=\s*await\s+getAuthUserId\s*\(/);
98
99
  if (authUserAssign) {
99
100
  const varName = authUserAssign[1];
100
- const hasNullCheck = new RegExp(`if\\s*\\(\\s*!${varName}\\b|if\\s*\\(\\s*${varName}\\s*===?\\s*null|throw.*!${varName}`).test(body);
101
+ // Recognize: if (!var), if (var === null), if (var), var &&, var ?, throw on !var
102
+ const hasNullCheck = new RegExp(`if\\s*\\(\\s*!${varName}\\b|if\\s*\\(\\s*${varName}\\s*===?\\s*null|if\\s*\\(\\s*${varName}\\s*\\)|${varName}\\s*&&|${varName}\\s*\\?|throw.*!${varName}|${varName}\\s*!==?\\s*null`).test(body);
101
103
  if (!hasNullCheck) {
102
104
  uncheckedIdentity++;
103
105
  issues.push({
@@ -77,17 +77,18 @@ function runQualityGate(projectDir, thresholds) {
77
77
  }
78
78
  catch { /* skip */ }
79
79
  }
80
- // Check 4: Type safety (as any casts)
80
+ // Check 4: Type safety (as any casts) — exclude test/eval files
81
81
  const typeSafety = getLatest("type_safety");
82
82
  if (typeSafety) {
83
83
  try {
84
84
  const issues = JSON.parse(typeSafety.issues_json);
85
- const asAnyIssues = Array.isArray(issues)
86
- ? issues.filter((i) => i.message?.includes("as any")).length
87
- : 0;
85
+ // Exclude test, eval, benchmark, and fixture files from production quality gate
86
+ const testEvalPattern = /test|__tests__|spec|\.test\.|\.spec\.|fixtures|mocks|evaluation|liveEval|liveApiSmoke|benchmark|calibration|harness|quickTest|comprehensiveTest|comprehensiveEval/i;
88
87
  // Each as-any issue represents a FILE, count from message for actual number
89
88
  const actualCasts = Array.isArray(issues)
90
89
  ? issues.reduce((sum, i) => {
90
+ if (testEvalPattern.test(i.location ?? ""))
91
+ return sum; // skip test/eval files
91
92
  const countMatch = i.message?.match(/(\d+)\s+`as any`/);
92
93
  return sum + (countMatch ? parseInt(countMatch[1], 10) : 0);
93
94
  }, 0)
@@ -102,13 +103,13 @@ function runQualityGate(projectDir, thresholds) {
102
103
  }
103
104
  catch { /* skip */ }
104
105
  }
105
- // Check 5: Unbounded collects
106
+ // Check 5: Unbounded collects (only warning-level — indexed collects are downgraded to info)
106
107
  const queryEfficiency = getLatest("query_efficiency");
107
108
  if (queryEfficiency) {
108
109
  try {
109
110
  const issues = JSON.parse(queryEfficiency.issues_json);
110
111
  const unbounded = Array.isArray(issues)
111
- ? issues.filter((i) => i.message?.includes(".collect()")).length
112
+ ? issues.filter((i) => i.severity === "warning" && i.message?.includes(".collect()")).length
112
113
  : 0;
113
114
  checks.push({
114
115
  metric: "unbounded_collects",
@@ -63,13 +63,16 @@ function auditQueryEfficiency(convexDir) {
63
63
  const chain = lines.slice(chainStart, i + 1).join("\n");
64
64
  if (!/\.take\s*\(/.test(chain) && !/\.paginate\s*\(/.test(chain)) {
65
65
  collectWithoutLimit++;
66
- // Check if it's a bounded table or could be large
67
66
  const tableMatch = chain.match(/\.query\s*\(\s*["'](\w+)["']\s*\)/);
67
+ // Indexed queries are bounded by the index range — lower severity
68
+ const hasIndex = /\.withIndex\s*\(/.test(chain);
68
69
  issues.push({
69
- severity: "warning",
70
+ severity: hasIndex ? "info" : "warning",
70
71
  location: `${relativePath}:${i + 1}`,
71
72
  message: `.collect() without .take() limit${tableMatch ? ` on table "${tableMatch[1]}"` : ""}. Could return entire table.`,
72
- fix: "Add .take(N) before .collect(), or use .paginate() for large result sets",
73
+ fix: hasIndex
74
+ ? "Consider adding .take(N) for predictable result sizes, even with index filtering"
75
+ : "Add .take(N) before .collect(), or use .paginate() for large result sets",
73
76
  });
74
77
  }
75
78
  }
@@ -75,7 +75,7 @@ function buildSarif(projectDir, auditTypes, limit) {
75
75
  tool: {
76
76
  driver: {
77
77
  name: "convex-mcp-nodebench",
78
- version: "0.9.4",
78
+ version: "0.9.5",
79
79
  informationUri: "https://www.npmjs.com/package/@homenshum/convex-mcp-nodebench",
80
80
  rules: [...rulesMap.values()],
81
81
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homenshum/convex-mcp-nodebench",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "description": "Convex-specific MCP server applying NodeBench self-instruct diligence patterns to Convex development. Schema audit, function compliance, deployment gates, persistent gotcha DB, and methodology guidance. Complements Context7 (raw docs) and official Convex MCP (deployment introspection) with structured verification workflows.",
5
5
  "type": "module",
6
6
  "bin": {