|
| 1 | +/** |
| 2 | + * @file Shared error-message-quality classifier. The single source for "is this |
| 3 | + * a vague-only error message" — consumed by both |
| 4 | + * `error-message-quality-reminder` (Stop hook, grades code blocks the |
| 5 | + * assistant wrote) and the `error-messages-are-thorough` check (commit-time, |
| 6 | + * grades `throw new …Error("…")` across the committed tree). Extracted so the |
| 7 | + * pattern list + grading bar live in ONE place; a tweak to either lands for |
| 8 | + * both surfaces at once. |
| 9 | + * |
| 10 | + * The bar (per CLAUDE.md "Error messages"): a message is vague when it is a |
| 11 | + * short static string carrying ONLY a vague verb/noun — no "what" rule, no |
| 12 | + * field/location, no saw-vs-wanted value. A message with a colon (field-path |
| 13 | + * prefix), an embedded quote (a shown value), or length > 40 is presumed to |
| 14 | + * carry specifics and is NOT graded. |
| 15 | + */ |
| 16 | + |
| 17 | +// Match any Error-suffixed class plus the legacy TemporalError name. |
| 18 | +export const ERROR_CLASS_RE = /(?:Error|TemporalError)$/ |
| 19 | + |
| 20 | +export interface VaguePattern { |
| 21 | + readonly label: string |
| 22 | + readonly regex: RegExp |
| 23 | + readonly hint: string |
| 24 | +} |
| 25 | + |
| 26 | +export const VAGUE_MESSAGE_PATTERNS: readonly VaguePattern[] = [ |
| 27 | + { |
| 28 | + label: 'bare "invalid"', |
| 29 | + regex: |
| 30 | + /^(?:invalid|invalid value|invalid input|invalid argument|invalid format)\.?$/i, |
| 31 | + hint: '"Invalid" describes the fallout, not the rule. Say what shape was expected: "must be lowercase", "must match /^[a-z]+$/", "must be one of X / Y / Z".', |
| 32 | + }, |
| 33 | + { |
| 34 | + label: 'bare "failed"', |
| 35 | + regex: |
| 36 | + /^(?:failed|failure|operation failed|request failed|action failed)\.?$/i, |
| 37 | + hint: '"Failed" describes the symptom. Name what was attempted and what blocked it: "could not write <path>: ENOENT", "fetch <url> returned 503".', |
| 38 | + }, |
| 39 | + { |
| 40 | + label: 'bare "error occurred"', |
| 41 | + regex: /^(?:an? )?error(?:\s+occurred)?\.?$/i, |
| 42 | + hint: 'The message says nothing the reader can act on. State the rule, the location, the bad value.', |
| 43 | + }, |
| 44 | + { |
| 45 | + label: 'bare "something went wrong"', |
| 46 | + regex: /^something went wrong\.?$/i, |
| 47 | + hint: 'Pure filler. CLAUDE.md "Error messages": the reader should fix the problem from the message alone.', |
| 48 | + }, |
| 49 | + { |
| 50 | + label: 'bare "unable to X" / "could not X" (verb-only)', |
| 51 | + regex: /^(?:unable to|could not|cannot|can'?t)\s+\w+\.?$/i, |
| 52 | + hint: 'No object / no reason. "Unable to read" → "could not read <path>: <errno>".', |
| 53 | + }, |
| 54 | + { |
| 55 | + label: 'bare "not found"', |
| 56 | + regex: /^(?:not found|not\s+exist|does not exist|missing)\.?$/i, |
| 57 | + hint: 'Missing what? Where? Say "config file not found: <path>" with the specific path.', |
| 58 | + }, |
| 59 | + { |
| 60 | + label: 'bare "bad" / "wrong" / "incorrect"', |
| 61 | + regex: |
| 62 | + /^(?:bad|wrong|incorrect|invalid format)(?:\s+(?:argument|data|format|input|value))?\.?$/i, |
| 63 | + hint: 'Same as "invalid" — describe the rule the value violated, not how you feel about it.', |
| 64 | + }, |
| 65 | +] |
| 66 | + |
| 67 | +export interface MessageGrade { |
| 68 | + readonly label: string |
| 69 | + readonly hint: string |
| 70 | +} |
| 71 | + |
| 72 | +/** |
| 73 | + * Grade a single thrown-error message string. Returns the matched vague pattern, |
| 74 | + * or undefined when the message clears the bar (carries a colon / quoted value, |
| 75 | + * is longer than 40 chars, or matches no vague-only pattern). A non-string |
| 76 | + * message (template literal with interpolation, an identifier) is out of scope — |
| 77 | + * pass an empty string and it returns undefined. |
| 78 | + */ |
| 79 | +export function gradeMessage(message: string): MessageGrade | undefined { |
| 80 | + const trimmed = message.trim() |
| 81 | + if (trimmed.length === 0) { |
| 82 | + return undefined |
| 83 | + } |
| 84 | + // A colon suggests a field-path prefix; an embedded quote/backtick suggests a |
| 85 | + // shown "saw vs. wanted" value. Either way, presumed specific. |
| 86 | + if ( |
| 87 | + trimmed.includes(':') || |
| 88 | + trimmed.includes('"') || |
| 89 | + trimmed.includes('`') |
| 90 | + ) { |
| 91 | + return undefined |
| 92 | + } |
| 93 | + if (trimmed.length > 40) { |
| 94 | + return undefined |
| 95 | + } |
| 96 | + for (let i = 0, { length } = VAGUE_MESSAGE_PATTERNS; i < length; i += 1) { |
| 97 | + const pattern = VAGUE_MESSAGE_PATTERNS[i]! |
| 98 | + if (pattern.regex.test(trimmed)) { |
| 99 | + return { label: pattern.label, hint: pattern.hint } |
| 100 | + } |
| 101 | + } |
| 102 | + return undefined |
| 103 | +} |
0 commit comments