From 677e4fb6daf8744a299f2a60546c42b31a5338f3 Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Tue, 3 Mar 2026 19:44:32 -0700 Subject: [PATCH] fix: scope scoring bug in prompt_score + add 11 unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Export scorePrompt for testability - Fix: long prompts no longer get full scope marks (25→20) without actual scope-bounding keywords like 'only', 'just', 'specific' - Add tests/tools/prompt-score.test.ts with 11 tests covering all scoring dimensions, grade boundaries, and edge cases - Total test count: 43→54 --- package-lock.json | 2 +- src/tools/prompt-score.ts | 8 +++- tests/tools/prompt-score.test.ts | 75 ++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 tests/tools/prompt-score.test.ts diff --git a/package-lock.json b/package-lock.json index 89ef280..4e5a169 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "vitest": "^4.0.18" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/@esbuild/aix-ppc64": { diff --git a/src/tools/prompt-score.ts b/src/tools/prompt-score.ts index 1cecf01..e73e203 100644 --- a/src/tools/prompt-score.ts +++ b/src/tools/prompt-score.ts @@ -40,7 +40,7 @@ interface ScoreResult { feedback: string[]; } -function scorePrompt(text: string): ScoreResult { +export function scorePrompt(text: string): ScoreResult { const feedback: string[] = []; let specificity: number; let scope: number; @@ -59,8 +59,12 @@ function scorePrompt(text: string): ScoreResult { } // Scope: bounded task - if (/\b(only|just|single|one|specific|this)\b/i.test(text) || text.length > 100) { + const hasScopeKeyword = /\b(only|just|single|one|specific|this)\b/i.test(text); + if (hasScopeKeyword) { scope = 25; + } else if (text.length > 100) { + // Long prompts suggest detail but don't guarantee bounded scope + scope = 20; } else if (/\b(all|every|entire|whole)\b/i.test(text)) { scope = 10; feedback.push("🎯 'All/every' is broad — can you narrow the scope?"); diff --git a/tests/tools/prompt-score.test.ts b/tests/tools/prompt-score.test.ts new file mode 100644 index 0000000..1097bc9 --- /dev/null +++ b/tests/tools/prompt-score.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from "vitest"; +import { scorePrompt } from "../../src/tools/prompt-score.js"; + +describe("scorePrompt", () => { + it("gives high score for a well-formed prompt", () => { + const result = scorePrompt( + "Rename the `handleSubmit` function in `src/components/Form.tsx` to `onFormSubmit`. Only this one function. It should still pass the existing tests." + ); + expect(result.total).toBeGreaterThanOrEqual(80); + expect(result.grade).toMatch(/^[AB]/); + }); + + it("gives low score for a vague prompt", () => { + const result = scorePrompt("make it better"); + expect(result.total).toBeLessThanOrEqual(40); + expect(result.grade).toMatch(/^[DF]/); + expect(result.feedback.length).toBeGreaterThan(0); + }); + + it("rewards file paths for specificity", () => { + const withPath = scorePrompt("fix src/lib/utils.ts"); + const without = scorePrompt("fix the utility code"); + expect(withPath.specificity).toBeGreaterThan(without.specificity); + }); + + it("rewards scope-bounding keywords", () => { + const bounded = scorePrompt("only update the header component"); + const unbounded = scorePrompt("update components"); + expect(bounded.scope).toBeGreaterThan(unbounded.scope); + }); + + it("penalizes broad scope words", () => { + const result = scorePrompt("refactor all the files"); + expect(result.scope).toBeLessThanOrEqual(10); + expect(result.feedback.some(f => f.includes("broad"))).toBe(true); + }); + + it("rewards action verbs", () => { + const specific = scorePrompt("extract the validation logic into a helper"); + const vague = scorePrompt("clean up the validation stuff"); + expect(specific.actionability).toBeGreaterThan(vague.actionability); + }); + + it("rewards done conditions", () => { + const withDone = scorePrompt("add a test that should return 404 for missing users"); + const without = scorePrompt("add a test for missing users"); + expect(withDone.doneCondition).toBeGreaterThan(without.doneCondition); + }); + + it("treats questions as having implicit done condition", () => { + const result = scorePrompt("Why does the login page crash on mobile?"); + expect(result.doneCondition).toBe(20); + }); + + it("long prompts get partial scope credit, not full", () => { + const longPrompt = "do something with " + "a".repeat(200); + const result = scorePrompt(longPrompt); + // Should get 20 (partial) not 25 (full) since no scope keywords + expect(result.scope).toBe(20); + }); + + it("returns grade F for minimal input", () => { + const result = scorePrompt("hi"); + expect(result.grade).toBe("F"); + }); + + it("returns congratulatory feedback for perfect prompts", () => { + const result = scorePrompt( + "In `src/auth/login.ts`, rename only the `validateToken` function to `verifyAuthToken`. The existing test in `tests/auth.test.ts` should still pass." + ); + if (result.total >= 90) { + expect(result.feedback.some(f => f.includes("Excellent"))).toBe(true); + } + }); +});