mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-29 06:46:49 +08:00
Everyday uses of como and wie were treated as sufficient structural evidence for HIGH. Remove these words from the unconditional keyword list so existing index verification determines HIGH, MEDIUM, or silence. Add real-index CLI regressions while preserving existing English and multilingual structural tests. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9a96b52991
commit
4fd3e6d86b
@@ -5,13 +5,14 @@
|
||||
* for the RIGHT project — including the monorepo case where the agent's cwd is
|
||||
* an un-indexed workspace root and the index lives in a sub-project. These test
|
||||
* `planFrontload` / `findIndexedSubprojectRoots` directly (the hook's decision
|
||||
* logic), since the end-to-end hook is validated by a live agent run, not a
|
||||
* unit test.
|
||||
* logic), plus the built CLI's confidence tiers against a real index.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { CodeGraph } from '../src';
|
||||
import { planFrontload, isTaskNotification, findIndexedSubprojectRoots, unsafeIndexRootReason, isStructuralPrompt, hasStructuralKeyword, extractCodeTokens, PROMPT_HOOK_INJECTION_MAX, CLAUDE_CODE_INLINE_HOOK_OUTPUT_LIMIT, capPromptHookInjection } from '../src/directory';
|
||||
|
||||
// Make the built-in exports configurable so HOME can point at a real temp
|
||||
@@ -302,6 +303,84 @@ describe('hasStructuralKeyword — Latin-script languages, Cyrillic, JA/KO (#112
|
||||
});
|
||||
});
|
||||
|
||||
describe('prompt-hook ambiguous words need corroboration (#1654)', () => {
|
||||
let tmp: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cg-hook-ambiguous-')));
|
||||
fs.writeFileSync(path.join(tmp, 'orders.ts'), `
|
||||
export class OrderStateMachine {
|
||||
submitOrder() { return true; }
|
||||
}
|
||||
`);
|
||||
const cg = await CodeGraph.init(tmp, { silent: true });
|
||||
try { await cg.indexAll(); } finally { cg.destroy(); }
|
||||
});
|
||||
afterEach(() => { fs.rmSync(tmp, { recursive: true, force: true }); });
|
||||
|
||||
function hook(prompt: string): string {
|
||||
const result = spawnSync(process.execPath, [path.resolve(__dirname, '../dist/bin/codegraph.js'), 'prompt-hook'], {
|
||||
cwd: tmp,
|
||||
input: JSON.stringify({ cwd: tmp, prompt }),
|
||||
encoding: 'utf8',
|
||||
timeout: 15_000,
|
||||
env: {
|
||||
...process.env,
|
||||
CODEGRAPH_TELEMETRY: '0', DO_NOT_TRACK: '1', CODEGRAPH_NO_DAEMON: '1',
|
||||
CODEGRAPH_NO_RELAUNCH: '1', CODEGRAPH_WASM_RELAUNCHED: '1',
|
||||
// Enable only the hook subprocess under test.
|
||||
CODEGRAPH_NO_PROMPT_HOOK: '0', CODEGRAPH_PROMPT_HOOK: '1',
|
||||
},
|
||||
});
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
it('stays silent for everyday Portuguese, Spanish and German without indexed evidence', () => {
|
||||
for (const prompt of [
|
||||
'eu como pizza toda sexta',
|
||||
'faz como a gente combinou ontem',
|
||||
'roda os testes como esta e me diz o resultado',
|
||||
'commita isso como fix, nao como feat',
|
||||
'hazlo como ayer',
|
||||
'mach es wie gestern',
|
||||
'COMO combinado ontem',
|
||||
'Wie gestern bitte',
|
||||
'como JavaScript?',
|
||||
'wie MissingService?',
|
||||
'run the tests and report back',
|
||||
]) {
|
||||
expect.soft(hasStructuralKeyword(prompt), prompt).toBe(false);
|
||||
expect.soft(hook(prompt), prompt).toBe('');
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps HIGH for verified identifiers or another structural keyword', () => {
|
||||
for (const prompt of [
|
||||
'como OrderStateMachine?',
|
||||
'como submitOrder()?',
|
||||
'wie OrderStateMachine?',
|
||||
'como funciona a máquina de estados?',
|
||||
'wie funktioniert die Zustandsmaschine?',
|
||||
'onde fica a lógica dos pedidos?',
|
||||
'¿cómo se procesan los pedidos?',
|
||||
'how does OrderStateMachine work?',
|
||||
]) {
|
||||
expect.soft(hook(prompt), prompt).toContain('Structural context from CodeGraph');
|
||||
}
|
||||
});
|
||||
|
||||
it('uses MEDIUM for indexed prose segments without a strong keyword or verified token', () => {
|
||||
for (const prompt of ['como state machine?', 'wie state machine?']) {
|
||||
const output = hook(prompt);
|
||||
expect(output, prompt).toContain('CodeGraph found indexed symbols matching this prompt');
|
||||
expect(output).toContain('OrderStateMachine');
|
||||
expect(output).not.toContain('Structural context from CodeGraph');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractCodeTokens — candidate symbols the hook verifies against the graph', () => {
|
||||
it('pulls camelCase / PascalCase / snake_case / call / member tokens', () => {
|
||||
expect(extractCodeTokens('prepareArticlePublish 的调用链')).toContain('prepareArticlePublish');
|
||||
|
||||
+8
-8
@@ -310,8 +310,9 @@ const NOT_WORD_AFTER = /(?![\p{L}\p{N}_])/u.source;
|
||||
* Structural keywords matched as EXACT words (boundary on both sides): short
|
||||
* or ambiguous tokens where prefix matching would false-positive ("flow" in
|
||||
* "flower", "path" in "pathological"). Grouped by language; a term appears once
|
||||
* even when several languages share it ("como" is Portuguese for how AND
|
||||
* unaccented-typed Spanish "cómo").
|
||||
* even when several languages share it. Ambiguous everyday words like PT/ES
|
||||
* "como" and DE "wie" are excluded: the hook instead requires another strong
|
||||
* keyword, a verified code token, or indexed prose segments (#1654).
|
||||
*/
|
||||
const STRUCTURAL_WORDS = [
|
||||
// English — the pre-#1126 list minus what moved to STRUCTURAL_STEMS: the
|
||||
@@ -320,17 +321,16 @@ const STRUCTURAL_WORDS = [
|
||||
'how', 'where', 'tracing', 'flows?', 'paths?', 'reach(?:es|ed)?', 'wired?', 'breaks?', 'why does',
|
||||
// French (où=where, flux=flow, chemin=path, casse=breaks)
|
||||
'comment', 'où', 'flux', 'chemins?', 'casse',
|
||||
// Spanish (cómo/como=how, dónde/donde=where, flujo=flow, ruta/camino=path,
|
||||
// Spanish (cómo=how, dónde/donde=where, flujo=flow, ruta/camino=path,
|
||||
// rompe=breaks, llaman / quién llama = call(s) — bare "llama" is excluded:
|
||||
// it's also the animal/model name in English prompts)
|
||||
'cómo', 'dónde', 'donde', 'flujos?', 'rutas?', 'caminos?', 'rompe', 'llaman', 'quién llama', 'quien llama',
|
||||
// Portuguese (como=how — also covers unaccented Spanish; onde=where,
|
||||
// fluxo=flow, caminho=path)
|
||||
'como', 'onde', 'fluxos?', 'caminhos?',
|
||||
// German (wie=how, wo/woher/wohin=where, Pfad=path, Fluss/Ablauf=flow,
|
||||
// Portuguese (onde=where, fluxo=flow, caminho=path)
|
||||
'onde', 'fluxos?', 'caminhos?',
|
||||
// German (wo/woher/wohin=where, Pfad=path, Fluss/Ablauf=flow,
|
||||
// bricht/kaputt=breaks, ruft=calls, hängt=depends — "hängt … von X ab"
|
||||
// splits the separable verb "abhängen", so the "abhäng" stem can't catch it)
|
||||
'wie', 'wo', 'woher', 'wohin', 'pfade?', 'fluss', 'ablauf', 'bricht', 'kaputt', 'ruft', 'hängt',
|
||||
'wo', 'woher', 'wohin', 'pfade?', 'fluss', 'ablauf', 'bricht', 'kaputt', 'ruft', 'hängt',
|
||||
// Italian (dove=where, flusso=flow, percorso/i=path)
|
||||
'dove', 'flusso', 'percors[oi]',
|
||||
// Russian (как=how, где=where, путь/пути=path, работает=works)
|
||||
|
||||
Reference in New Issue
Block a user