From 317e7f4d3d8752ac2ca17c0e891bcf512df2e385 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Thu, 2 Jul 2026 14:34:17 -0500 Subject: [PATCH] fix(prompt-hook): make the structural-question gate multilingual (#1126) (#1134) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(prompt-hook): fire the structural gate for Latin-script, Cyrillic, and JA/KO prompts (#1126) The prompt-hook's keyword gate only knew English and simplified-Chinese keywords, so a structural question in French (or Spanish, German, Italian, Portuguese, Russian, Japanese, Korean, traditional Chinese) silently no-op'd unless it happened to contain an identifier-shaped code token — the #994 symptom, resurfaced for every other language. Root causes fixed: - JS \b is ASCII-only: a keyword whose first/last char is accented or non-Latin (où, qué, Cyrillic, kana) can never match \bkeyword\b — the same mechanism behind #994. Keyword matching now uses Unicode lookaround boundaries ((? * fix(prompt-hook): extend the gate to tier-2 languages (VI/TR/ID/PL/UA/NL/CS/RO/HU/EL/Nordics/FI/HI/AR/FA/HE/TH) The first pass covered the 10 largest languages; this closes the rest of the major-developer-population set (~29 total). Notable per-language mechanics the curation had to respect: - Agglutinative languages (Turkish, Finnish, Hungarian) need stems, not exact words — suffixes attach to everything (akışı, riippuu, működik). - Indonesian me-/di-/ber- prefixes block leading-boundary stems, so affixed forms are listed explicitly (memanggil, dipanggil, berfungsi). - Arabic/Farsi/Hebrew are spaced but proclitics attach to the word (وكيف = and-how), so they join the substring class with Thai. - Ukrainian і/и spellings diverge from Russian (архітектур ≠ архитектур). - Excluded terms that collide with English or code words: NL "pad", SV "var", CS "tok", Catalan "com" (matches every .com domain) — with regression tests pinning the exclusions. Vietnamese was the sharpest gap: spaced Latin with heavy diacritics — exactly the ASCII-\b failure class #1126 reports. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- CHANGELOG.md | 1 + __tests__/frontload-hook.test.ts | 96 +++++++++++++- src/bin/codegraph.ts | 4 +- src/directory.ts | 213 ++++++++++++++++++++++++++++--- 4 files changed, 292 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5362b5..7651104 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- The automatic context hook for Claude Code now fires for structural questions asked in nearly thirty languages — French, Spanish, Portuguese, German, Italian, Dutch, Polish, Czech, Romanian, Hungarian, Greek, Swedish, Danish, Norwegian, Finnish, Russian, Ukrainian, Turkish, Indonesian, Vietnamese, Thai, Hindi, Arabic, Farsi, Hebrew, Japanese, Korean, and both simplified and traditional Chinese — instead of just English and simplified Chinese. Previously a natural question like "comment marche la state machine des commandes ?" injected nothing unless it happened to contain a code-shaped symbol name, making the hook look broken for non-English teams. English questions phrased with derived word forms ("explain the architecture…", "what are the dependencies…") now fire too, and prompts in any other language still fire when they name a symbol from the index. Thanks @anthonyle-roy-lgtm for the report. (#1126) - Lua and Luau method calls with capitalized names (`obj:Method()` — the standard Roblox convention) now link to the right method. Because Lua's method-call syntax looks identical to a Luau type annotation, a capitalized call like `lg:Log()` was misread as declaring the variable's type, so whenever two or more classes shared a method name (`Init`, `Update`, `Destroy`, …) the call was silently dropped from callers, impact/blast-radius, and flow traces. Lowercase method names were unaffected. Thanks @inth3shadows for the precise root-cause analysis and repro. (#1124) - Removed dead code left behind by the discontinued managed-reasoning feature. Its `codegraph login` flow was unplugged before ever shipping in a release, but the unused module still shipped inside the platform bundles, and a security review flagged its Windows browser-open step (it routed the login URL through `cmd`, which would have been unsafe had the flow ever been wired back up). The leftover module and its tests are now fully deleted. Thanks @inth3shadows for the report. (#1114) diff --git a/__tests__/frontload-hook.test.ts b/__tests__/frontload-hook.test.ts index bf54790..d4a0699 100644 --- a/__tests__/frontload-hook.test.ts +++ b/__tests__/frontload-hook.test.ts @@ -130,7 +130,7 @@ describe('findIndexedSubprojectRoots', () => { }); describe('hasStructuralKeyword — keyword signal fires the hook directly (#994)', () => { - it('English keywords match, with `\\b` so "flow" ≠ "flower"', () => { + it('English keywords match with word boundaries 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); @@ -152,6 +152,100 @@ describe('hasStructuralKeyword — keyword signal fires the hook directly (#994) }); }); +describe('hasStructuralKeyword — Latin-script languages, Cyrillic, JA/KO (#1126)', () => { + it('French structural prompts fire — including the prompts from the report', () => { + expect(hasStructuralKeyword('comment marche la state machine des commandes ?')).toBe(true); + expect(hasStructuralKeyword("explique l'architecture du module de stock")).toBe(true); + expect(hasStructuralKeyword('qui appelle cette fonction de parsing ?')).toBe(true); + expect(hasStructuralKeyword('de quoi dépend le module de paiement ?')).toBe(true); + }); + + it('accented keyword edges match — ASCII `\\b` could never bound "où"', () => { + expect(hasStructuralKeyword('où est validé le token ?')).toBe(true); + expect(hasStructuralKeyword("d'où vient cette valeur ?")).toBe(true); + }); + + it('Spanish / Portuguese / German / Italian fire', () => { + expect(hasStructuralKeyword('¿cómo funciona la máquina de estados de pedidos?')).toBe(true); + expect(hasStructuralKeyword('¿qué rompe este cambio?')).toBe(true); + expect(hasStructuralKeyword('como funciona a máquina de estados dos pedidos?')).toBe(true); + expect(hasStructuralKeyword('qual é a arquitetura do módulo de estoque?')).toBe(true); + expect(hasStructuralKeyword('wie funktioniert die Zustandsmaschine für Bestellungen?')).toBe(true); + expect(hasStructuralKeyword('wovon hängt das Zahlungsmodul ab?')).toBe(true); + expect(hasStructuralKeyword('come funziona la macchina a stati degli ordini?')).toBe(true); + expect(hasStructuralKeyword('spiegami la struttura del modulo ordini')).toBe(true); + }); + + it('Russian / Japanese / Korean / traditional Chinese fire', () => { + expect(hasStructuralKeyword('как работает конечный автомат заказов?')).toBe(true); + expect(hasStructuralKeyword('от чего зависит модуль оплаты?')).toBe(true); + expect(hasStructuralKeyword('注文のステートマシンの仕組みを説明して')).toBe(true); + expect(hasStructuralKeyword('この関数の呼び出しの流れは?')).toBe(true); + expect(hasStructuralKeyword('주문 상태 머신은 어떻게 작동하나요?')).toBe(true); + expect(hasStructuralKeyword('訂單狀態機的架構是怎麼實現的?')).toBe(true); + }); + + it('English derived forms fire — "architecture"/"dependencies" failed the old exact-word list', () => { + expect(hasStructuralKeyword('explain the architecture of the stock module')).toBe(true); + expect(hasStructuralKeyword('what are the dependencies of the parser?')).toBe(true); + }); + + it('second-tier languages fire — VI/TR/ID/PL/UA/NL/CS/RO/HU/EL/SV/NO/FI/HI', () => { + expect(hasStructuralKeyword('state machine của đơn hàng hoạt động thế nào?')).toBe(true); // Vietnamese + expect(hasStructuralKeyword('sipariş durum makinesi nasıl çalışıyor?')).toBe(true); // Turkish + expect(hasStructuralKeyword('bu fonksiyonun bağımlılıkları neler?')).toBe(true); // Turkish (stem) + expect(hasStructuralKeyword('bagaimana cara kerja mesin status pesanan?')).toBe(true); // Indonesian + expect(hasStructuralKeyword('jak działa maszyna stanów zamówień?')).toBe(true); // Polish + expect(hasStructuralKeyword('co wywołuje tę funkcję?')).toBe(true); // Polish (stem) + expect(hasStructuralKeyword('як працює кінцевий автомат замовлень?')).toBe(true); // Ukrainian + expect(hasStructuralKeyword('від чого залежить модуль оплати?')).toBe(true); // Ukrainian (stem) + expect(hasStructuralKeyword('hoe werkt de state machine van bestellingen?')).toBe(true); // Dutch + expect(hasStructuralKeyword('jak funguje stavový automat objednávek?')).toBe(true); // Czech + expect(hasStructuralKeyword('cum funcționează mașina de stări a comenzilor?')).toBe(true); // Romanian + expect(hasStructuralKeyword('hogyan működik a rendelések állapotgépe?')).toBe(true); // Hungarian + expect(hasStructuralKeyword('πώς λειτουργεί η μηχανή καταστάσεων παραγγελιών;')).toBe(true); // Greek + expect(hasStructuralKeyword('hur fungerar orderns tillståndsmaskin?')).toBe(true); // Swedish + expect(hasStructuralKeyword('hvordan fungerer ordrenes tilstandsmaskin?')).toBe(true); // Norwegian/Danish + expect(hasStructuralKeyword('miten tilausten tilakone toimii?')).toBe(true); // Finnish + expect(hasStructuralKeyword('ऑर्डर स्टेट मशीन कैसे काम करती है?')).toBe(true); // Hindi + }); + + it('RTL scripts and Thai fire — proclitics/unsegmented text uses substring matching', () => { + expect(hasStructuralKeyword('كيف تعمل آلة حالات الطلبات؟')).toBe(true); // Arabic + expect(hasStructuralKeyword('وكيف يعتمد هذا على قاعدة البيانات؟')).toBe(true); // Arabic, proclitic و attached + expect(hasStructuralKeyword('ماشین وضعیت سفارش‌ها چگونه کار می‌کند؟')).toBe(true); // Farsi + expect(hasStructuralKeyword('איך עובדת מכונת המצבים של ההזמנות?')).toBe(true); // Hebrew + expect(hasStructuralKeyword('สถาปัตยกรรมของระบบทำงานอย่างไร')).toBe(true); // Thai + }); + + it('terms that collide with English or code words are deliberately excluded', () => { + expect(hasStructuralKeyword('pad the buffer with zeros')).toBe(false); // NL pad=path skipped + expect(hasStructuralKeyword('declare a var for the count')).toBe(false); // SV var=where skipped + expect(hasStructuralKeyword('refresh the token')).toBe(false); // CS tok=flow skipped + expect(hasStructuralKeyword('run the llama model locally')).toBe(false); // ES bare llama skipped + expect(hasStructuralKeyword('come back to this later')).toBe(false); // IT bare come skipped + }); + + it('stems match only at word start — no mid-word false positives', () => { + expect(hasStructuralKeyword('restructure this paragraph')).toBe(false); // "structur" mid-word + expect(hasStructuralKeyword('an independent module')).toBe(false); // "depend" mid-word + expect(hasStructuralKeyword('water the flower')).toBe(false); // unchanged guarantee + }); + + it('non-structural prose stays a no-op in every covered language', () => { + expect(hasStructuralKeyword('corrige cette faute de frappe')).toBe(false); // FR "fix this typo" + expect(hasStructuralKeyword('arregla este error tipográfico')).toBe(false); // ES + expect(hasStructuralKeyword('behebe diesen Tippfehler')).toBe(false); // DE + expect(hasStructuralKeyword('исправь эту опечатку')).toBe(false); // RU + expect(hasStructuralKeyword('このタイプミスを直して')).toBe(false); // JA + expect(hasStructuralKeyword('이 오타를 수정해줘')).toBe(false); // KO + expect(hasStructuralKeyword('sửa lỗi chính tả này')).toBe(false); // VI + expect(hasStructuralKeyword('bu yazım hatasını düzelt')).toBe(false); // TR + expect(hasStructuralKeyword('popraw tę literówkę')).toBe(false); // PL + expect(hasStructuralKeyword('صحح هذا الخطأ الإملائي')).toBe(false); // AR + }); +}); + describe('extractCodeTokens — candidate symbols the hook verifies against the graph', () => { it('pulls camelCase / PascalCase / snake_case / call / member tokens', () => { expect(extractCodeTokens('prepareArticlePublish 的调用链')).toContain('prepareArticlePublish'); diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index acef88d..092dca8 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -1072,8 +1072,8 @@ program // 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 + // (multilingual keywords, plus code-shaped tokens) so it fires for non-English + // prompts too (#994, #1126). 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); diff --git a/src/directory.ts b/src/directory.ts index da5b6e9..86830a5 100644 --- a/src/directory.ts +++ b/src/directory.ts @@ -234,36 +234,211 @@ export function findIndexedSubprojectRoots( } /** - * English structural keywords, matched with `\b` word boundaries so a keyword - * inside a longer word doesn't false-positive ("flow" in "flower"). + * Unicode-aware word-boundary emulation for the keyword lists below. JS's `\b` + * is ASCII-only — it fires only at `[A-Za-z0-9_]` edges — so it can never bound + * a keyword whose first or last character is accented or non-Latin: `/\boù\b/` + * NEVER matches "où est …" (ù isn't an ASCII word char, so no boundary exists + * next to it). That is the #994 CJK mechanism resurfaced for Latin scripts and + * Cyrillic (#1126). A lookaround — "not flanked by a letter, digit, or + * underscore" — is the script-independent equivalent. */ -const STRUCTURAL_EN = /\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; +const NOT_WORD_BEFORE = /(?