fix(prompt-hook): fire front-load hook for non-English prompts (#994) (#1004)

The UserPromptSubmit hook's structural-prompt gate was English-only, so a
structural question written in Chinese — or any non-Latin script — silently
injected nothing: JS `\b` is ASCII-only and never matches between Han
characters, so the keyword regex couldn't fire (and couldn't be extended in
place). To the user the hook looked unwired, with no error to explain why.

Make the gate language-aware, split into tested helpers in directory.ts:
- hasStructuralKeyword: English (\b-guarded) + CJK structural keywords.
- extractCodeTokens: identifier-shaped tokens (camelCase / snake_case /
  name() / a.b) in any language — verified against the index via
  getNodesByName before firing, so a tech brand like `JavaScript` that looks
  like a symbol but isn't one here doesn't inject ~16KB of spurious context.
- isStructuralPrompt: the cheap candidate gate (keyword OR code-token).

Adds 21 unit tests for the gate (previously untested) covering the reporter's
verification table plus the false-positive guards.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-26 18:39:34 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 703629edc3
commit b45f309a1b
4 changed files with 174 additions and 7 deletions
+16 -6
View File
@@ -26,7 +26,7 @@
import { Command } from 'commander';
import * as path from 'path';
import * as fs from 'fs';
import { getCodeGraphDir, isInitialized, unsafeIndexRootReason, findNearestCodeGraphRoot, planFrontload } from '../directory';
import { getCodeGraphDir, isInitialized, unsafeIndexRootReason, findNearestCodeGraphRoot, planFrontload, hasStructuralKeyword, extractCodeTokens } from '../directory';
import { detectWorktreeIndexMismatch, worktreeMismatchWarning } from '../sync/worktree';
import { createShimmerProgress } from '../ui/shimmer-progress';
import { getGlyphs } from '../ui/glyphs';
@@ -1053,11 +1053,15 @@ program
try { input = JSON.parse(raw); } catch { return; }
const prompt = String(input.prompt || '');
// Gate: only structural / flow / impact / where-how prompts get context.
// A cheap regex keeps every other prompt ("fix this typo") a zero-cost
// no-op so we never add latency where there's no structural answer to give.
const STRUCTURAL = /\b(how|where|trace|flow|path|reach(?:es|ed)?|call(?:s|ed|er|ers|ee)?|depend|impact|affect|wired?|connect|implement|architect|structure|breaks?|what calls|why does)\b/i;
if (!prompt || !STRUCTURAL.test(prompt)) return;
// Gate: only structural / flow / impact / where-how prompts get context, so
// every other prompt ("fix this typo") stays a zero-cost no-op. Language-aware
// (English + CJK keywords, plus code-shaped tokens) so it fires for non-English
// prompts too (issue #994). A keyword fires on its own; a code-token is only a
// CANDIDATE — verified against the graph below, so a tech brand ("JavaScript")
// that looks like a symbol but isn't one here doesn't inject spurious context.
const keyworded = hasStructuralKeyword(prompt);
const codeTokens = keyworded ? [] : extractCodeTokens(prompt);
if (!keyworded && codeTokens.length === 0) return;
// Decide what to inject, shaped by WHERE the index(es) are: the nearest
// indexed ancestor of cwd, or — when cwd is an un-indexed workspace root
@@ -1079,6 +1083,12 @@ program
const { default: CodeGraph } = await loadCodeGraph();
const cg = await CodeGraph.open(plan.exploreRoot);
try {
// Code-token-only prompt: require that at least one token is a REAL symbol
// in THIS index before front-loading. Without it, a brand name or common
// word that merely looks like code ("JavaScript", "GitHub") would run
// explore and inject ~16KB of low-relevance context (issue #994 follow-up).
// A keyword-bearing prompt skips this — the keyword is signal enough.
if (!keyworded && !codeTokens.some((t) => cg.getNodesByName(t).length > 0)) return;
const { ToolHandler } = await import('../mcp/tools');
const handler = new ToolHandler(cg);
const result = await handler.execute('codegraph_explore', { query: prompt });