feat(router): tune Layer 1 — глаголы + keyword>classification приоритет (stage 3 task 5b)

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 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-24 10:50:38 +03:00
parent 7ed72a09f7
commit 112591a0da
3 changed files with 79 additions and 49 deletions
+17 -20
View File
@@ -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}
-7
View File
@@ -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 |
+62 -22
View File
@@ -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