@homenshum/convex-mcp-nodebench 0.4.0 → 0.7.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 (34) hide show
  1. package/dist/index.js +52 -2
  2. package/dist/tools/actionAuditTools.d.ts +2 -0
  3. package/dist/tools/actionAuditTools.js +180 -0
  4. package/dist/tools/authorizationTools.d.ts +2 -0
  5. package/dist/tools/authorizationTools.js +201 -0
  6. package/dist/tools/critterTools.js +76 -5
  7. package/dist/tools/dataModelingTools.d.ts +2 -0
  8. package/dist/tools/dataModelingTools.js +168 -0
  9. package/dist/tools/deploymentTools.js +42 -2
  10. package/dist/tools/devSetupTools.d.ts +2 -0
  11. package/dist/tools/devSetupTools.js +170 -0
  12. package/dist/tools/embeddingProvider.d.ts +6 -0
  13. package/dist/tools/embeddingProvider.js +3 -0
  14. package/dist/tools/functionTools.js +24 -1
  15. package/dist/tools/httpTools.js +128 -48
  16. package/dist/tools/integrationBridgeTools.js +4 -0
  17. package/dist/tools/methodologyTools.js +8 -1
  18. package/dist/tools/migrationTools.d.ts +2 -0
  19. package/dist/tools/migrationTools.js +133 -0
  20. package/dist/tools/paginationTools.d.ts +2 -0
  21. package/dist/tools/paginationTools.js +125 -0
  22. package/dist/tools/queryEfficiencyTools.d.ts +2 -0
  23. package/dist/tools/queryEfficiencyTools.js +191 -0
  24. package/dist/tools/schemaTools.js +95 -1
  25. package/dist/tools/storageAuditTools.d.ts +2 -0
  26. package/dist/tools/storageAuditTools.js +148 -0
  27. package/dist/tools/toolRegistry.d.ts +9 -2
  28. package/dist/tools/toolRegistry.js +205 -16
  29. package/dist/tools/transactionSafetyTools.d.ts +2 -0
  30. package/dist/tools/transactionSafetyTools.js +166 -0
  31. package/dist/tools/typeSafetyTools.d.ts +2 -0
  32. package/dist/tools/typeSafetyTools.js +146 -0
  33. package/dist/types.d.ts +6 -1
  34. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -26,6 +26,16 @@ import { cronTools } from "./tools/cronTools.js";
26
26
  import { componentTools } from "./tools/componentTools.js";
27
27
  import { httpTools } from "./tools/httpTools.js";
28
28
  import { critterTools } from "./tools/critterTools.js";
29
+ import { authorizationTools } from "./tools/authorizationTools.js";
30
+ import { queryEfficiencyTools } from "./tools/queryEfficiencyTools.js";
31
+ import { actionAuditTools } from "./tools/actionAuditTools.js";
32
+ import { typeSafetyTools } from "./tools/typeSafetyTools.js";
33
+ import { transactionSafetyTools } from "./tools/transactionSafetyTools.js";
34
+ import { storageAuditTools } from "./tools/storageAuditTools.js";
35
+ import { paginationTools } from "./tools/paginationTools.js";
36
+ import { dataModelingTools } from "./tools/dataModelingTools.js";
37
+ import { devSetupTools } from "./tools/devSetupTools.js";
38
+ import { migrationTools } from "./tools/migrationTools.js";
29
39
  import { CONVEX_GOTCHAS } from "./gotchaSeed.js";
30
40
  import { REGISTRY } from "./tools/toolRegistry.js";
31
41
  import { initEmbeddingIndex } from "./tools/embeddingProvider.js";
@@ -41,6 +51,16 @@ const ALL_TOOLS = [
41
51
  ...componentTools,
42
52
  ...httpTools,
43
53
  ...critterTools,
54
+ ...authorizationTools,
55
+ ...queryEfficiencyTools,
56
+ ...actionAuditTools,
57
+ ...typeSafetyTools,
58
+ ...transactionSafetyTools,
59
+ ...storageAuditTools,
60
+ ...paginationTools,
61
+ ...dataModelingTools,
62
+ ...devSetupTools,
63
+ ...migrationTools,
44
64
  ];
45
65
  const toolMap = new Map();
46
66
  for (const tool of ALL_TOOLS) {
@@ -59,10 +79,40 @@ const server = new Server({
59
79
  getDb();
60
80
  seedGotchasIfEmpty(CONVEX_GOTCHAS);
61
81
  // ── Background: initialize embedding index for semantic search ───────
62
- const embeddingCorpus = REGISTRY.map((entry) => ({
82
+ // Uses Agent-as-a-Graph bipartite corpus: tool nodes + domain nodes for graph-aware retrieval
83
+ const descMap = new Map(ALL_TOOLS.map((t) => [t.name, t.description]));
84
+ // Tool nodes: individual tools with full metadata text
85
+ const toolCorpus = REGISTRY.map((entry) => ({
63
86
  name: entry.name,
64
- text: `${entry.name} ${entry.tags.join(" ")} ${entry.category} ${entry.phase}`,
87
+ text: `${entry.name} ${entry.tags.join(" ")} ${entry.category} ${entry.phase} ${descMap.get(entry.name) ?? ""}`,
88
+ nodeType: "tool",
65
89
  }));
90
+ // Domain nodes: aggregate category descriptions for upward traversal
91
+ // When a domain matches, all tools in that domain get a sibling boost
92
+ const categoryTools = new Map();
93
+ for (const entry of REGISTRY) {
94
+ const list = categoryTools.get(entry.category) ?? [];
95
+ list.push(entry.name);
96
+ categoryTools.set(entry.category, list);
97
+ }
98
+ const domainCorpus = [...categoryTools.entries()].map(([category, toolNames]) => {
99
+ const allTags = new Set();
100
+ const descs = [];
101
+ for (const tn of toolNames) {
102
+ const e = REGISTRY.find((r) => r.name === tn);
103
+ if (e)
104
+ e.tags.forEach((t) => allTags.add(t));
105
+ const d = descMap.get(tn);
106
+ if (d)
107
+ descs.push(d);
108
+ }
109
+ return {
110
+ name: `domain:${category}`,
111
+ text: `${category} domain: ${toolNames.join(" ")} ${[...allTags].join(" ")} ${descs.map(d => d.slice(0, 80)).join(" ")}`,
112
+ nodeType: "domain",
113
+ };
114
+ });
115
+ const embeddingCorpus = [...toolCorpus, ...domainCorpus];
66
116
  initEmbeddingIndex(embeddingCorpus).catch(() => {
67
117
  /* Embedding init failed — semantic search stays disabled */
68
118
  });
@@ -0,0 +1,2 @@
1
+ import type { McpTool } from "../types.js";
2
+ export declare const actionAuditTools: McpTool[];
@@ -0,0 +1,180 @@
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 auditActions(convexDir) {
31
+ const files = collectTsFiles(convexDir);
32
+ const issues = [];
33
+ let totalActions = 0;
34
+ let actionsWithDbAccess = 0;
35
+ let actionsWithoutNodeDirective = 0;
36
+ let actionsWithoutErrorHandling = 0;
37
+ let actionCallingAction = 0;
38
+ // Node APIs that require "use node" directive
39
+ const nodeApis = /\b(require|__dirname|__filename|Buffer\.|process\.env|fs\.|path\.|crypto\.|child_process|net\.|http\.|https\.)\b/;
40
+ for (const filePath of files) {
41
+ const content = readFileSync(filePath, "utf-8");
42
+ const relativePath = filePath.replace(convexDir, "").replace(/^[\\/]/, "");
43
+ const lines = content.split("\n");
44
+ const hasUseNode = /["']use node["']/.test(content);
45
+ const actionPattern = /export\s+(?:const\s+(\w+)\s*=|default)\s+(action|internalAction)\s*\(/g;
46
+ let m;
47
+ while ((m = actionPattern.exec(content)) !== null) {
48
+ const funcName = m[1] || "default";
49
+ const funcType = m[2];
50
+ totalActions++;
51
+ const startLine = content.slice(0, m.index).split("\n").length - 1;
52
+ // Extract body using brace tracking
53
+ let depth = 0;
54
+ let foundOpen = false;
55
+ let endLine = Math.min(startLine + 100, lines.length);
56
+ for (let j = startLine; j < lines.length; j++) {
57
+ for (const ch of lines[j]) {
58
+ if (ch === "{") {
59
+ depth++;
60
+ foundOpen = true;
61
+ }
62
+ if (ch === "}")
63
+ depth--;
64
+ }
65
+ if (foundOpen && depth <= 0) {
66
+ endLine = j + 1;
67
+ break;
68
+ }
69
+ }
70
+ const body = lines.slice(startLine, endLine).join("\n");
71
+ // Check 1: ctx.db access in action (FATAL — not allowed)
72
+ if (/ctx\.db\.(get|query|insert|patch|replace|delete)\s*\(/.test(body)) {
73
+ actionsWithDbAccess++;
74
+ issues.push({
75
+ severity: "critical",
76
+ location: `${relativePath}:${startLine + 1}`,
77
+ functionName: funcName,
78
+ message: `${funcType} "${funcName}" accesses ctx.db directly. Actions cannot access the database — use ctx.runQuery/ctx.runMutation instead.`,
79
+ fix: "Move DB operations into a query or mutation, then call via ctx.runQuery(internal.file.func, args) or ctx.runMutation(...)",
80
+ });
81
+ }
82
+ // Check 2: Node API usage without "use node"
83
+ if (!hasUseNode && nodeApis.test(body)) {
84
+ actionsWithoutNodeDirective++;
85
+ issues.push({
86
+ severity: "critical",
87
+ location: `${relativePath}:${startLine + 1}`,
88
+ functionName: funcName,
89
+ message: `${funcType} "${funcName}" uses Node.js APIs but file lacks "use node" directive. Will fail in Convex runtime.`,
90
+ fix: `Add "use node"; at the top of ${relativePath}`,
91
+ });
92
+ }
93
+ // Check 3: External API calls without try/catch
94
+ const hasFetch = /\bfetch\s*\(/.test(body);
95
+ const hasAxios = /\baxios\b/.test(body);
96
+ const hasExternalCall = hasFetch || hasAxios;
97
+ const hasTryCatch = /try\s*\{/.test(body);
98
+ if (hasExternalCall && !hasTryCatch) {
99
+ actionsWithoutErrorHandling++;
100
+ issues.push({
101
+ severity: "warning",
102
+ location: `${relativePath}:${startLine + 1}`,
103
+ functionName: funcName,
104
+ message: `${funcType} "${funcName}" makes external API calls without try/catch. Network failures will crash the action.`,
105
+ fix: "Wrap fetch/axios calls in try/catch and handle errors gracefully",
106
+ });
107
+ }
108
+ // Check 4: Action calling another action
109
+ if (/ctx\.runAction\s*\(/.test(body)) {
110
+ actionCallingAction++;
111
+ issues.push({
112
+ severity: "warning",
113
+ location: `${relativePath}:${startLine + 1}`,
114
+ functionName: funcName,
115
+ message: `${funcType} "${funcName}" calls ctx.runAction(). Only call action from action when crossing runtimes (V8 ↔ Node). Otherwise extract shared logic into a helper function.`,
116
+ fix: "Extract shared logic into an async helper, or use ctx.runMutation/ctx.runQuery as intermediary",
117
+ });
118
+ }
119
+ // Check 5: Very long action body (likely doing too much)
120
+ const bodyLines = endLine - startLine;
121
+ if (bodyLines > 80) {
122
+ issues.push({
123
+ severity: "info",
124
+ location: `${relativePath}:${startLine + 1}`,
125
+ functionName: funcName,
126
+ message: `${funcType} "${funcName}" is ${bodyLines} lines long. Consider splitting into smaller actions or extracting helpers.`,
127
+ fix: "Break large actions into smaller, focused functions",
128
+ });
129
+ }
130
+ }
131
+ }
132
+ return {
133
+ issues,
134
+ stats: {
135
+ totalActions,
136
+ actionsWithDbAccess,
137
+ actionsWithoutNodeDirective,
138
+ actionsWithoutErrorHandling,
139
+ actionCallingAction,
140
+ },
141
+ };
142
+ }
143
+ // ── Tool Definition ─────────────────────────────────────────────────
144
+ export const actionAuditTools = [
145
+ {
146
+ name: "convex_audit_actions",
147
+ description: 'Audit Convex actions for: ctx.db access (fatal — actions cannot access DB directly), missing "use node" directive for Node APIs, external API calls without error handling, and action-calling-action anti-patterns.',
148
+ inputSchema: {
149
+ type: "object",
150
+ properties: {
151
+ projectDir: {
152
+ type: "string",
153
+ description: "Absolute path to the project root containing a convex/ directory",
154
+ },
155
+ },
156
+ required: ["projectDir"],
157
+ },
158
+ handler: async (args) => {
159
+ const projectDir = resolve(args.projectDir);
160
+ const convexDir = findConvexDir(projectDir);
161
+ if (!convexDir) {
162
+ return { error: "No convex/ directory found" };
163
+ }
164
+ const { issues, stats } = auditActions(convexDir);
165
+ const db = getDb();
166
+ db.prepare("INSERT INTO audit_results (id, project_dir, audit_type, issues_json, issue_count) VALUES (?, ?, ?, ?, ?)").run(genId("audit"), projectDir, "action_audit", JSON.stringify(issues), issues.length);
167
+ return {
168
+ summary: {
169
+ ...stats,
170
+ totalIssues: issues.length,
171
+ critical: issues.filter((i) => i.severity === "critical").length,
172
+ warnings: issues.filter((i) => i.severity === "warning").length,
173
+ },
174
+ issues: issues.slice(0, 30),
175
+ quickRef: getQuickRef("convex_audit_actions"),
176
+ };
177
+ },
178
+ },
179
+ ];
180
+ //# sourceMappingURL=actionAuditTools.js.map
@@ -0,0 +1,2 @@
1
+ import type { McpTool } from "../types.js";
2
+ export declare const authorizationTools: McpTool[];
@@ -0,0 +1,201 @@
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 auditAuthorization(convexDir) {
31
+ const files = collectTsFiles(convexDir);
32
+ const issues = [];
33
+ let totalPublic = 0;
34
+ let withAuth = 0;
35
+ let withoutAuth = 0;
36
+ let uncheckedIdentity = 0;
37
+ const funcTypes = ["query", "mutation", "action"];
38
+ const writeSensitive = /admin|delete|purge|remove|destroy|drop|reset|wipe|erase|revoke/i;
39
+ const dbWriteOps = /ctx\.db\.(insert|patch|replace|delete)\s*\(/;
40
+ for (const filePath of files) {
41
+ const content = readFileSync(filePath, "utf-8");
42
+ const relativePath = filePath.replace(convexDir, "").replace(/^[\\/]/, "");
43
+ const lines = content.split("\n");
44
+ for (let i = 0; i < lines.length; i++) {
45
+ const line = lines[i];
46
+ for (const ft of funcTypes) {
47
+ const exportPattern = new RegExp(`export\\s+(?:const\\s+(\\w+)\\s*=|default)\\s+${ft}\\s*\\(`);
48
+ const match = line.match(exportPattern);
49
+ if (!match)
50
+ continue;
51
+ const funcName = match[1] || "default";
52
+ totalPublic++;
53
+ // Extract function body using brace tracking
54
+ let depth = 0;
55
+ let foundOpen = false;
56
+ let endLine = Math.min(i + 100, lines.length);
57
+ for (let j = i; j < lines.length; j++) {
58
+ for (const ch of lines[j]) {
59
+ if (ch === "{") {
60
+ depth++;
61
+ foundOpen = true;
62
+ }
63
+ if (ch === "}")
64
+ depth--;
65
+ }
66
+ if (foundOpen && depth <= 0) {
67
+ endLine = j + 1;
68
+ break;
69
+ }
70
+ }
71
+ const body = lines.slice(i, endLine).join("\n");
72
+ const hasAuthCheck = /ctx\.auth\.getUserIdentity\s*\(\s*\)/.test(body) ||
73
+ /getUserIdentity/.test(body);
74
+ const hasDbWrite = dbWriteOps.test(body);
75
+ const isSensitiveName = writeSensitive.test(funcName);
76
+ if (hasAuthCheck) {
77
+ withAuth++;
78
+ // Check: getUserIdentity called but return not null-checked
79
+ const identityAssign = body.match(/(?:const|let)\s+(\w+)\s*=\s*await\s+ctx\.auth\.getUserIdentity\s*\(\s*\)/);
80
+ if (identityAssign) {
81
+ const varName = identityAssign[1];
82
+ // Look for null check: if (!var), if (var === null), if (var == null), if (!var)
83
+ const hasNullCheck = new RegExp(`if\\s*\\(\\s*!${varName}\\b|if\\s*\\(\\s*${varName}\\s*===?\\s*null|if\\s*\\(\\s*!${varName}\\s*\\)`).test(body);
84
+ if (!hasNullCheck) {
85
+ uncheckedIdentity++;
86
+ issues.push({
87
+ severity: "critical",
88
+ location: `${relativePath}:${i + 1}`,
89
+ functionName: funcName,
90
+ message: `${ft} "${funcName}" calls getUserIdentity() but doesn't check for null. Unauthenticated users will get undefined identity.`,
91
+ fix: `Add: if (!${varName}) { throw new Error("Not authenticated"); }`,
92
+ });
93
+ }
94
+ }
95
+ }
96
+ else {
97
+ withoutAuth++;
98
+ // Critical: public mutation/action with DB writes but no auth
99
+ if ((ft === "mutation" || ft === "action") && hasDbWrite) {
100
+ issues.push({
101
+ severity: "critical",
102
+ location: `${relativePath}:${i + 1}`,
103
+ functionName: funcName,
104
+ message: `Public ${ft} "${funcName}" writes to DB without auth check. Any client can call this.`,
105
+ fix: `Add: const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new Error("Not authenticated");`,
106
+ });
107
+ }
108
+ // Critical: sensitive-named function without auth
109
+ if (isSensitiveName) {
110
+ issues.push({
111
+ severity: "critical",
112
+ location: `${relativePath}:${i + 1}`,
113
+ functionName: funcName,
114
+ message: `Public ${ft} "${funcName}" has a sensitive name but no auth check. Consider making it internal or adding auth.`,
115
+ fix: `Either change to internal${ft.charAt(0).toUpperCase() + ft.slice(1)} or add auth check`,
116
+ });
117
+ }
118
+ // Warning: public query reading user-specific data without auth
119
+ if (ft === "query" && /ctx\.db\.get|ctx\.db\.query/.test(body) && /user|profile|account|email/i.test(body)) {
120
+ issues.push({
121
+ severity: "warning",
122
+ location: `${relativePath}:${i + 1}`,
123
+ functionName: funcName,
124
+ message: `Public query "${funcName}" accesses user-related data without auth. Consider if unauthenticated access is intended.`,
125
+ fix: "Add auth check if this data should be protected",
126
+ });
127
+ }
128
+ }
129
+ }
130
+ }
131
+ }
132
+ return {
133
+ issues,
134
+ stats: {
135
+ totalPublicFunctions: totalPublic,
136
+ publicWithAuth: withAuth,
137
+ publicWithoutAuth: withoutAuth,
138
+ uncheckedIdentity,
139
+ },
140
+ };
141
+ }
142
+ // ── Tool Definition ─────────────────────────────────────────────────
143
+ export const authorizationTools = [
144
+ {
145
+ name: "convex_audit_authorization",
146
+ description: "Audit Convex functions for authorization issues: public mutations/actions writing to DB without auth checks, getUserIdentity() without null checks, sensitive-named functions without auth, and unprotected user data queries.",
147
+ inputSchema: {
148
+ type: "object",
149
+ properties: {
150
+ projectDir: {
151
+ type: "string",
152
+ description: "Absolute path to the project root containing a convex/ directory",
153
+ },
154
+ },
155
+ required: ["projectDir"],
156
+ },
157
+ handler: async (args) => {
158
+ const projectDir = resolve(args.projectDir);
159
+ const convexDir = findConvexDir(projectDir);
160
+ if (!convexDir) {
161
+ return { error: "No convex/ directory found" };
162
+ }
163
+ const { issues, stats } = auditAuthorization(convexDir);
164
+ // Store audit result
165
+ const db = getDb();
166
+ db.prepare("INSERT INTO audit_results (id, project_dir, audit_type, issues_json, issue_count) VALUES (?, ?, ?, ?, ?)").run(genId("audit"), projectDir, "authorization", JSON.stringify(issues), issues.length);
167
+ const critical = issues.filter((i) => i.severity === "critical");
168
+ const warnings = issues.filter((i) => i.severity === "warning");
169
+ // Group by issue type
170
+ const byType = {};
171
+ for (const issue of issues) {
172
+ const type = issue.message.includes("without auth check") ? "no_auth_on_write" :
173
+ issue.message.includes("null") ? "unchecked_identity" :
174
+ issue.message.includes("sensitive name") ? "sensitive_no_auth" :
175
+ issue.message.includes("user-related") ? "unprotected_user_data" :
176
+ "other";
177
+ if (!byType[type])
178
+ byType[type] = { count: 0, examples: [] };
179
+ byType[type].count++;
180
+ if (byType[type].examples.length < 3)
181
+ byType[type].examples.push(issue);
182
+ }
183
+ return {
184
+ summary: {
185
+ ...stats,
186
+ totalIssues: issues.length,
187
+ critical: critical.length,
188
+ warnings: warnings.length,
189
+ authCoverage: stats.totalPublicFunctions > 0
190
+ ? `${stats.publicWithAuth}/${stats.totalPublicFunctions} (${Math.round(100 * stats.publicWithAuth / stats.totalPublicFunctions)}%)`
191
+ : "0/0",
192
+ },
193
+ issuesByType: Object.entries(byType)
194
+ .sort(([, a], [, b]) => b.count - a.count)
195
+ .map(([type, data]) => ({ type, count: data.count, examples: data.examples })),
196
+ quickRef: getQuickRef("convex_audit_authorization"),
197
+ };
198
+ },
199
+ },
200
+ ];
201
+ //# sourceMappingURL=authorizationTools.js.map
@@ -32,22 +32,26 @@ function scoreCritterCheck(input) {
32
32
  const taskLower = input.task.toLowerCase().trim();
33
33
  const whyLower = input.why.toLowerCase().trim();
34
34
  const whoLower = input.who.toLowerCase().trim();
35
- // Check 1: Circular reasoning
35
+ // Check 1: Circular reasoning (threshold 0.5)
36
36
  const taskWords = new Set(taskLower.split(/\s+/).filter((w) => w.length > 3));
37
37
  const whyWords = whyLower.split(/\s+/).filter((w) => w.length > 3);
38
38
  const overlap = whyWords.filter((w) => taskWords.has(w));
39
- if (whyWords.length > 0 && overlap.length / whyWords.length > 0.7) {
39
+ if (whyWords.length > 0 && overlap.length / whyWords.length > 0.5) {
40
40
  score -= 30;
41
41
  feedback.push("Circular: your 'why' mostly restates the task. What user outcome does this enable?");
42
42
  }
43
43
  // Check 2: Vague audience
44
- const vagueAudiences = ["users", "everyone", "people", "the team", "stakeholders", "clients"];
44
+ const vagueAudiences = ["users", "everyone", "people", "the team", "stakeholders", "clients", "customers", "developers"];
45
45
  if (vagueAudiences.includes(whoLower)) {
46
46
  score -= 20;
47
47
  feedback.push(`"${input.who}" is too broad. Which user role or API consumer specifically?`);
48
48
  }
49
- // Check 3: Too short
50
- if (whyLower.length < 10) {
49
+ // Check 3: Empty or too short
50
+ if (whyLower.length === 0) {
51
+ score -= 40;
52
+ feedback.push("Empty 'why': you haven't stated any purpose at all.");
53
+ }
54
+ else if (whyLower.length < 10) {
51
55
  score -= 25;
52
56
  feedback.push("The 'why' is too short. What problem does this solve?");
53
57
  }
@@ -61,6 +65,73 @@ function scoreCritterCheck(input) {
61
65
  score -= 15;
62
66
  feedback.push("Citing authority instead of understanding purpose. Why does this matter to the product?");
63
67
  }
68
+ // Check 5: Non-answer patterns (count matches, -20 each, cap -40)
69
+ const nonAnswerPatterns = [
70
+ "just because", "don't know", "not sure", "why not", "might need it",
71
+ "no reason", "no idea", "whatever", "idk", "tbd",
72
+ ];
73
+ const nonAnswerHits = nonAnswerPatterns.filter((p) => whyLower.includes(p)).length;
74
+ if (nonAnswerHits > 0) {
75
+ const nonAnswerPenalty = Math.min(nonAnswerHits * 20, 40);
76
+ score -= nonAnswerPenalty;
77
+ feedback.push("Non-answer: your 'why' signals unclear purpose. What specific problem does this solve?");
78
+ }
79
+ // Check 6: Repetitive padding (why + who)
80
+ const whyAllWords = whyLower.split(/\s+/).filter((w) => w.length > 2);
81
+ if (whyAllWords.length >= 5) {
82
+ const whyUniqueWords = new Set(whyAllWords);
83
+ if (whyUniqueWords.size / whyAllWords.length < 0.4) {
84
+ score -= 25;
85
+ feedback.push("Repetitive: your 'why' repeats the same words. Articulate distinct reasoning.");
86
+ }
87
+ }
88
+ const whoAllWords = whoLower.split(/\s+/).filter((w) => w.length > 2);
89
+ if (whoAllWords.length >= 5) {
90
+ const whoUniqueWords = new Set(whoAllWords);
91
+ if (whoUniqueWords.size / whoAllWords.length < 0.4) {
92
+ score -= 25;
93
+ feedback.push("Repetitive: your 'who' repeats the same words. Name a real audience.");
94
+ }
95
+ }
96
+ // Check 7: Buzzword-heavy corporate-speak (scans why + who)
97
+ const buzzwords = [
98
+ "leverage", "synergies", "synergy", "paradigm", "holistic", "alignment",
99
+ "transformation", "innovative", "disruptive", "best practices",
100
+ "streamline", "ecosystem", "actionable", "circle back",
101
+ ];
102
+ const allText = `${whyLower} ${whoLower}`;
103
+ const buzzCount = buzzwords.filter((b) => allText.includes(b)).length;
104
+ if (buzzCount >= 4) {
105
+ score -= 35;
106
+ feedback.push("Buzzword-heavy: corporate-speak without concrete meaning. What specific problem does this solve?");
107
+ }
108
+ else if (buzzCount >= 3) {
109
+ score -= 30;
110
+ feedback.push("Buzzword-heavy: corporate-speak without concrete meaning. What specific problem does this solve?");
111
+ }
112
+ else if (buzzCount >= 2) {
113
+ score -= 20;
114
+ feedback.push("Buzzword-heavy: corporate-speak without concrete meaning. What specific problem does this solve?");
115
+ }
116
+ // Check 8: Hedging language
117
+ const hedgeWords = ["could", "potentially", "maybe", "possibly", "might", "perhaps", "hopefully"];
118
+ const hedgeCount = hedgeWords.filter((h) => {
119
+ const regex = new RegExp(`\\b${h}\\b`, "i");
120
+ return regex.test(whyLower);
121
+ }).length;
122
+ if (hedgeCount >= 2) {
123
+ score -= 15;
124
+ feedback.push("Hedging: too many 'could/maybe/potentially' signals uncertain value. Be definitive.");
125
+ }
126
+ // Check 9: Task-word echo — same word from task repeated 3+ times in why
127
+ for (const tw of taskWords) {
128
+ const twCount = whyWords.filter((w) => w === tw).length;
129
+ if (twCount >= 3) {
130
+ score -= 20;
131
+ feedback.push(`Echo: "${tw}" appears ${twCount} times in your 'why' — this is filler, not reasoning.`);
132
+ break;
133
+ }
134
+ }
64
135
  // Bonus for specificity
65
136
  if (input.success_looks_like && input.success_looks_like.length > 20) {
66
137
  score += 10;
@@ -0,0 +1,2 @@
1
+ import type { McpTool } from "../types.js";
2
+ export declare const dataModelingTools: McpTool[];