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:
co-authored by
Claude Opus 4.8
parent
703629edc3
commit
b45f309a1b
@@ -12,7 +12,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { planFrontload, findIndexedSubprojectRoots } from '../src/directory';
|
||||
import { planFrontload, findIndexedSubprojectRoots, isStructuralPrompt, hasStructuralKeyword, extractCodeTokens } from '../src/directory';
|
||||
|
||||
/** Make `dir` look indexed (isInitialized needs `.codegraph/codegraph.db`). */
|
||||
function mkIndexed(dir: string): string {
|
||||
@@ -128,3 +128,76 @@ describe('findIndexedSubprojectRoots', () => {
|
||||
expect(findIndexedSubprojectRoots(tmp, { maxDepth: 2 })).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasStructuralKeyword — keyword signal fires the hook directly (#994)', () => {
|
||||
it('English keywords match, with `\\b` so "flow" ≠ "flower"', () => {
|
||||
expect(hasStructuralKeyword('how does article publish work')).toBe(true);
|
||||
expect(hasStructuralKeyword('where is the token validated')).toBe(true);
|
||||
expect(hasStructuralKeyword('trace the request flow')).toBe(true);
|
||||
expect(hasStructuralKeyword('what calls parseToken')).toBe(true);
|
||||
expect(hasStructuralKeyword('water the flower')).toBe(false); // "flow" in "flower"
|
||||
});
|
||||
|
||||
it('Chinese keywords match WITHOUT `\\b` — the #994 fix (were silently dropped)', () => {
|
||||
expect(hasStructuralKeyword('介绍文章发布流程')).toBe(true); // introduce / flow
|
||||
expect(hasStructuralKeyword('登录是如何实现的')).toBe(true); // how / implement
|
||||
expect(hasStructuralKeyword('这个函数的调用链')).toBe(true); // call (chain)
|
||||
expect(hasStructuralKeyword('支付模块依赖哪些服务')).toBe(true); // depend
|
||||
expect(hasStructuralKeyword('修复这个拼写错误')).toBe(false); // "fix this typo"
|
||||
});
|
||||
|
||||
it('a bare code-token is NOT a keyword — it needs graph verification', () => {
|
||||
expect(hasStructuralKeyword('看看 get_user 这段逻辑')).toBe(false);
|
||||
expect(hasStructuralKeyword('I really love JavaScript')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractCodeTokens — candidate symbols the hook verifies against the graph', () => {
|
||||
it('pulls camelCase / PascalCase / snake_case / call / member tokens', () => {
|
||||
expect(extractCodeTokens('prepareArticlePublish 的调用链')).toContain('prepareArticlePublish');
|
||||
expect(extractCodeTokens('看看 get_user 这段逻辑')).toContain('get_user'); // snake_case
|
||||
expect(extractCodeTokens('render() 在哪触发')).toContain('render'); // call form
|
||||
expect(extractCodeTokens('user.login 做了什么').sort()).toEqual(['login', 'user']); // member access
|
||||
expect(extractCodeTokens('看看 UserService')).toContain('UserService'); // PascalCase class kept
|
||||
});
|
||||
|
||||
it('a tech brand is extracted as a CANDIDATE — the hook’s graph check is what rejects it', () => {
|
||||
// This is the #994 follow-up: "JavaScript" is identifier-shaped, so it surfaces
|
||||
// here as a candidate; the hook only fires if it's a real symbol in the index.
|
||||
expect(extractCodeTokens('I really love JavaScript')).toEqual(['JavaScript']);
|
||||
expect(extractCodeTokens('thoughts on GitHub vs GitLab').sort()).toEqual(['GitHub', 'GitLab']);
|
||||
});
|
||||
|
||||
it('ordinary prose and doc/data filenames yield no tokens', () => {
|
||||
expect(extractCodeTokens('fix typo in readme')).toEqual([]);
|
||||
expect(extractCodeTokens('fix the typo in README.md')).toEqual([]); // doc filename excluded
|
||||
expect(extractCodeTokens('bump the version in package.json')).toEqual([]);
|
||||
expect(extractCodeTokens('water the flower')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isStructuralPrompt — cheap candidate gate (keyword OR code-token)', () => {
|
||||
it('fires on a keyword prompt in any language', () => {
|
||||
expect(isStructuralPrompt('how does article publish work')).toBe(true);
|
||||
expect(isStructuralPrompt('介绍文章发布流程')).toBe(true);
|
||||
});
|
||||
|
||||
it('fires on a code-token prompt with no keyword', () => {
|
||||
expect(isStructuralPrompt('看看 get_user 这段逻辑')).toBe(true);
|
||||
expect(isStructuralPrompt('where is prepareArticlePublish 定义')).toBe(true);
|
||||
expect(isStructuralPrompt('user.login 做了什么')).toBe(true);
|
||||
});
|
||||
|
||||
it('a tech brand passes the CHEAP gate as a candidate — the hook then graph-verifies it', () => {
|
||||
// Layering, not a bug: isStructuralPrompt is shape-only, so a token-shaped brand
|
||||
// is a candidate here; the hook rejects it as a non-symbol (proven by the CLI e2e).
|
||||
expect(isStructuralPrompt('I really love JavaScript')).toBe(true);
|
||||
});
|
||||
|
||||
it('non-structural prose stays a no-op — in either language', () => {
|
||||
expect(isStructuralPrompt('fix typo in readme')).toBe(false);
|
||||
expect(isStructuralPrompt('修复这个拼写错误')).toBe(false);
|
||||
expect(isStructuralPrompt('water the flower')).toBe(false);
|
||||
expect(isStructuralPrompt('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user