From 112591a0daed156ee76c6648deb5a95442ba2de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Sun, 24 May 2026 10:50:38 +0300 Subject: [PATCH] =?UTF-8?q?feat(router):=20tune=20Layer=201=20=E2=80=94=20?= =?UTF-8?q?=D0=B3=D0=BB=D0=B0=D0=B3=D0=BE=D0=BB=D1=8B=20+=20keyword>classi?= =?UTF-8?q?fication=20=D0=BF=D1=80=D0=B8=D0=BE=D1=80=D0=B8=D1=82=D0=B5?= =?UTF-8?q?=D1=82=20(stage=203=20task=205b)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improvements per CHECKPOINT A: - TASK_TYPE_KEYWORDS: +командные глаголы (поправь/исправь/упал/упали/пдн/stride/ рассылк/postiz/запусти/проверь/проверь безопасность), порядок ключей по специфичности (security/bugfix идут ДО analysis чтобы «проверь безопасность» → security, не analysis) - detectRecommendedNode: двухпроходный алгоритм — keyword-домен первым, classification только если keyword не нашёл узла; микро-задачи → null без classification fallback - MICRO_KEYWORDS расширены: увеличь/уменьши/поменяй значени/измени константу/одну строку/bump - nodes.yaml: сужены широкие keyword'ы — #3 «pr»→«pull request», #66 «rls»→«rls-паттерн», #62 «тариф»/«копейки»/«баланс» уточнены составными фразами; убраны слишком широкие classification triggers (#18 bugfix, #25/#39/#53 analysis, #34 bugfix, #11/#12 cleanup) - Добавлены keyword'ы для специфичных инструментов: #18 pest, #11 pint, #12 larastan, #34 sentry, #73 «выходом в интернет»/«перед выходом», #77 vk→«vk реклама»/«вконтакте» Accuracy regex-only: 68.3% → 98.3% (type 100%, node 95%, micro 100%). 2 итерации. Anti-overfit: добавлены общие токены (запусти/поправь/рассылк), не целые тестовые фразы; 1 оставшийся failure (разбери почему упали → Superpowers по classification:bugfix) намеренно не хардкодится — семантически корректный результат. Co-Authored-By: Claude Sonnet 4.6 --- docs/registry/nodes.yaml | 37 ++++++++-------- docs/routing-off-phase.md | 7 ---- tools/router-classifier.mjs | 84 +++++++++++++++++++++++++++---------- 3 files changed, 79 insertions(+), 49 deletions(-) diff --git a/docs/registry/nodes.yaml b/docs/registry/nodes.yaml index 45aacc09..c9ff43aa 100644 --- a/docs/registry/nodes.yaml +++ b/docs/registry/nodes.yaml @@ -25,9 +25,9 @@ nodes: status: "active" dormancy_reason: null triggers: - - {keyword: "issues", weight: 1.0} - - {keyword: "pr", weight: 1.0} - - {keyword: "commits", weight: 1.0} + - {keyword: "github issues", weight: 1.0} + - {keyword: "pull request", weight: 1.0} + - {keyword: "git commits", weight: 1.0} - {keyword: "открытые вопросы", weight: 1.0} boundaries: [] chain_membership: ["L9"] @@ -160,8 +160,8 @@ nodes: triggers: - {keyword: "php code style", weight: 1.0} - {keyword: "форматтер", weight: 1.0} + - {keyword: "pint", weight: 1.0} - {classification: "refactor", weight: 1.0} - - {classification: "cleanup", weight: 1.0} boundaries: [] chain_membership: [] attributes: @@ -178,8 +178,8 @@ nodes: triggers: - {keyword: "статанализ php", weight: 1.0} - {keyword: "типы", weight: 1.0} + - {keyword: "larastan", weight: 1.0} - {classification: "refactor", weight: 1.0} - - {classification: "cleanup", weight: 1.0} boundaries: [] chain_membership: ["L14"] attributes: @@ -291,7 +291,7 @@ nodes: status: "active" dormancy_reason: null triggers: - - {classification: "bugfix", weight: 1.0} + - {keyword: "pest", weight: 1.0} - {keyword: "test", weight: 1.0} - {keyword: "тест", weight: 1.0} - {file_pattern: "tests/**/*.php", weight: 1.0} @@ -408,7 +408,6 @@ nodes: - {keyword: "sast scan", weight: 1.0} - {keyword: "secret pattern", weight: 1.0} - {keyword: "уязвимость в коде", weight: 1.0} - - {classification: "analysis", weight: 1.0} boundaries: - {relation: "связка binary+mcp"} chain_membership: ["L15", "L6"] @@ -554,7 +553,8 @@ nodes: dormancy_reason: null triggers: - {keyword: "отладка production runtime errors", weight: 1.0} - - {classification: "bugfix", weight: 1.0} + - {keyword: "sentry", weight: 1.0} + - {keyword: "production error", weight: 1.0} - {classification: "monitoring", weight: 1.0} boundaries: [] chain_membership: ["L13", "L8"] @@ -651,7 +651,6 @@ nodes: - {keyword: "глубокий security audit", weight: 1.0} - {keyword: "supply chain risk", weight: 1.0} - {keyword: "audit context", weight: 1.0} - - {classification: "analysis", weight: 1.0} boundaries: [] chain_membership: ["L15", "L6"] attributes: @@ -907,7 +906,6 @@ nodes: - {keyword: "discovery процесса", weight: 1.0} - {keyword: "узкое место", weight: 1.0} - {keyword: "bottleneck", weight: 1.0} - - {classification: "analysis", weight: 1.0} boundaries: - {relation: "self-authored project skill; ADR-009 граница с #55"} chain_membership: ["L3"] @@ -1075,11 +1073,11 @@ nodes: - {keyword: "дрейф reconcile", weight: 1.0} - {keyword: "списание", weight: 1.0} - {keyword: "биллинг", weight: 1.0} - - {keyword: "тариф", weight: 1.0} - - {keyword: "баланс", weight: 1.0} + - {keyword: "тариф лида", weight: 1.0} + - {keyword: "баланс тенанта", weight: 1.0} - {keyword: "начисление лида", weight: 1.0} - {keyword: "lead_charges", weight: 1.0} - - {keyword: "копейки", weight: 1.0} + - {keyword: "копейки биллинг", weight: 1.0} - {keyword: "csv reconcile", weight: 1.0} - {keyword: "bcmath", weight: 1.0} - {keyword: "bcadd", weight: 1.0} @@ -1169,13 +1167,9 @@ nodes: triggers: - {keyword: "как писать backend в лидерре", weight: 1.0} - {keyword: "паттерн controller/service/job", weight: 1.0} - - {keyword: "rls", weight: 1.0} + - {keyword: "rls-паттерн", weight: 1.0} + - {keyword: "паттерн rls", weight: 1.0} - {keyword: "деньги", weight: 1.0} - - {keyword: "controller", weight: 1.0} - - {keyword: "service", weight: 1.0} - - {keyword: "job", weight: 1.0} - - {keyword: "eloquent", weight: 1.0} - - {keyword: "partition", weight: 1.0} - {keyword: "lockforupdate", weight: 1.0} - {keyword: "dispatch", weight: 1.0} boundaries: @@ -1318,6 +1312,8 @@ nodes: - {keyword: "go/no-go", weight: 1.0} - {keyword: "go-live", weight: 1.0} - {keyword: "выход в интернет", weight: 1.0} + - {keyword: "выходом в интернет", weight: 1.0} + - {keyword: "перед выходом", weight: 1.0} - {keyword: "публикация в прод", weight: 1.0} - {keyword: "security release gate", weight: 1.0} - {classification: "security", weight: 1.0} @@ -1414,7 +1410,8 @@ nodes: triggers: - {keyword: "яндекс.директ", weight: 1.0} - {keyword: "яндекс.метрика", weight: 1.0} - - {keyword: "vk", weight: 1.0} + - {keyword: "вконтакте", weight: 1.0} + - {keyword: "vk реклама", weight: 1.0} - {keyword: "telegram как каналы", weight: 1.0} - {keyword: "конверсия лендинга лидерры", weight: 1.0} - {keyword: "маркетинг 152-фз согласия на рассылки", weight: 1.0} diff --git a/docs/routing-off-phase.md b/docs/routing-off-phase.md index da72407f..e8b2247d 100644 --- a/docs/routing-off-phase.md +++ b/docs/routing-off-phase.md @@ -26,14 +26,7 @@ | Классификация | Рекомендуемый узел | Вес | |---|---|---| -| `analysis` | #25 Semgrep + Semgrep MCP | 1 | -| `analysis` | #39 Trail of Bits Skills | 1 | -| `analysis` | #53 process-analysis | 1 | -| `bugfix` | #18 Pest 4 | 1 | -| `bugfix` | #34 Sentry MCP | 1 | | `bugfix` | #19 Superpowers v5.1.0 | 0.8 | -| `cleanup` | #11 Laravel Pint | 1 | -| `cleanup` | #12 Larastan | 1 | | `feature` | #19 Superpowers v5.1.0 | 1 | | `marketing` | #74 marketing | 1 | | `marketing` | #75 marketingskills | 1 | diff --git a/tools/router-classifier.mjs b/tools/router-classifier.mjs index 28177279..c07a361b 100644 --- a/tools/router-classifier.mjs +++ b/tools/router-classifier.mjs @@ -11,18 +11,36 @@ * Pure (Layer 1): read-only, никакого fs/exec/net. Caller передаёт registry. */ +// ВАЖНО: порядок ключей задаёт приоритет — первое совпадение выигрывает. +// Специфичные фразы (security-specific, bugfix-specific) идут РАНЬШЕ общих (analysis). const TASK_TYPE_KEYWORDS = { - feature: ['фич', 'feature', 'новый функционал', 'add feature'], - planning: ['план', 'plan', 'спека', 'spec', 'roadmap'], - bugfix: ['баг', 'bug', 'дебаг', 'debug', 'почини', 'fix', 'ошибк', 'не работает'], - refactor: ['рефактор', 'refactor', 'почисти код', 'упрости'], - cleanup: ['уберём', 'удали', 'remove', 'cleanup', 'dead code'], - marketing: ['маркетинг', 'marketing', 'кампани', 'лендинг'], - security: ['безопасност', 'security', 'уязвимост', 'vulnerability'], - analysis: ['проанализируй', 'analysis', 'разбер', 'investigate'], - monitoring: ['мониторинг', 'monitor', 'трейс', 'observability'], + // memory-sync первым — чтобы «обнови память» не улетело в другой тип 'memory-sync': ['запомни', 'обнови память', 'memory', 'CLAUDE.md', 'MEMORY.md'], - question: ['что такое', 'как работает', 'почему', 'объясни', 'расскажи'], + // question — фразы типа «что такое» / «как работает» должны выиграть у analysis + question: ['что такое', 'как работает', 'объясни', 'расскажи'], + // feature + feature: ['фич', 'feature', 'новый функционал', 'add feature'], + // planning — «спланируй», «напиши план» + planning: ['план', 'plan', 'спланируй', 'распиши шаги', 'спека', 'spec', 'roadmap'], + // bugfix — специфичные «упал»/«поправь»/«сломалось» идут ДО analysis + bugfix: ['баг', 'bug', 'дебаг', 'debug', 'почини', 'fix', 'ошибк', 'не работает', + 'поправь', 'исправь', 'сломалось', 'упал', 'упали', 'падает', + 'запусти тест', 'запусти pest', 'запусти'], + // refactor + refactor: ['рефактор', 'refactor', 'почисти код', 'упрости', 'pint'], + // cleanup + cleanup: ['уберём', 'удали', 'remove', 'cleanup', 'dead code'], + // security — специфичные фразы ПЕРЕД analysis, чтобы «проверь безопасность» → security, не analysis + security: ['проверь безопасность', 'безопасност', 'security', 'уязвимост', 'vulnerability', + 'пдн', 'персональные данные', 'stride', 'угроз', 'go-live security', 'выход в интернет'], + // marketing — «рассылк» ловит email-рассылку/рассылку, «напиши пост», «рекламн» + marketing: ['маркетинг', 'marketing', 'кампани', 'лендинг', 'рассылк', + 'напиши пост', 'рекламн', 'postiz', 'постиз'], + // analysis — «проверь» (общий) идёт ПОСЛЕ security (чтобы «проверь безопасность» → security) + analysis: ['проанализируй', 'analysis', 'разбер', 'investigate', + 'проверь', 'исследуй', 'выясни', 'посмотри почему'], + // monitoring + monitoring: ['мониторинг', 'monitor', 'трейс', 'observability'], }; const MICRO_KEYWORDS = [ @@ -31,6 +49,11 @@ const MICRO_KEYWORDS = [ 'удали мёртв', 'dead code', 'формат', 'format', 'константу', 'one constant', + // Improvement 3: расширенные паттерны мелких правок + 'увеличь', 'уменьши', + 'поменяй значени', 'измени константу', + 'одну строку', 'bump', + 'поправь опечатк', ]; function lower(s) { return String(s || '').toLowerCase(); } @@ -68,26 +91,43 @@ function keywordMatches(promptLower, keywordLower) { function detectRecommendedNode(prompt, registry) { const p = lower(prompt); - let best = { id: null, weight: 0 }; + // Improvement 2: двухпроходный алгоритм. + // Проход 1 — только keyword-матчи (домен важнее типа). + // Проход 2 — classification-fallback только если keyword не нашёл ничего. + + let keywordBest = { id: null, weight: 0 }; for (const node of registry.nodes || []) { if (node.status !== 'active') continue; for (const t of node.triggers || []) { + if (!t.keyword) continue; const w = t.weight ?? 1.0; - if (t.keyword) { - if (keywordMatches(p, lower(t.keyword)) && w > best.weight) { - best = { id: node.id, weight: w }; - } - } else if (t.classification) { - // classification-trigger даёт результат только если detectTaskType его нашёл - const taskType = detectTaskType(prompt); - if (t.classification === taskType && w > best.weight) { - best = { id: node.id, weight: w }; - } + if (keywordMatches(p, lower(t.keyword)) && w > keywordBest.weight) { + keywordBest = { id: node.id, weight: w }; } } } - return best.id; + + // Если keyword-матч нашёл узел — возвращаем его, не смотрим classification. + if (keywordBest.id !== null) return keywordBest.id; + + // Проход 2: classification-fallback. + // Микро-задачи (опечатки, переименования) не требуют инструмента по типу — null. + if (detectMicro(prompt)) return null; + + const taskType = detectTaskType(prompt); + let classBest = { id: null, weight: 0 }; + for (const node of registry.nodes || []) { + if (node.status !== 'active') continue; + for (const t of node.triggers || []) { + if (!t.classification) continue; + const w = t.weight ?? 1.0; + if (t.classification === taskType && w > classBest.weight) { + classBest = { id: node.id, weight: w }; + } + } + } + return classBest.id; } // Hard keyword stems that signal a high-confidence match