Compare commits

...

6 Commits

Author SHA1 Message Date
Дмитрий 4903a8d188 chore: checkpoint feat/gate-allow-worktree-cd before phase8 activation 2026-06-09 13:50:13 +03:00
Дмитрий 5a3ad6b899 feat(router-gate): allow graphify read-only subcommands (#86, §5 п.14)
Whitelist graphify query/explain/path in enforce-router-gate so the project
knowledge-graph tool (#86) can run; extract/update/build/export/hook/clone/add
stay default-deny. Owner-authorized 2026-06-08.

Adversarial tests prove chained payloads (`; id`, `&& rm`, `| sh`) and subshell
(backtick) are blocked by the gate architecture (per-segment whitelist +
tokenizer subshell-block + redirect hard-blacklist). The security-review
"unanchored allowlist" finding is documented as a false-positive: $VAR is
var-expanded by the tokenizer (not an injection vector for a read-only arg), and
end-anchoring with a charset would reject Unicode queries (tokenizer strips
quotes → Cyrillic args arrive as barewords). 122/122 gate tests GREEN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:07:23 +03:00
Дмитрий 1d2d43a6f2 fix(tdd-gate): recognize pest JSON reporter failures as RED
composer test / php artisan test emit machine JSON ({"result":"failed",...}); command-not-found and error REDs lack the English Failed keyword the gate looked for, so legit RED runs went unseen and prod-code edits were wrongly blocked. hasFailingTestRun now also matches the structured failure markers. TDD: +1 test; full tools suite 2004 GREEN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:35:05 +03:00
Дмитрий 3420f46a59 feat(router-gate): support git -C path for worktree dev
Shell resets cwd each call so a worktree cd does not persist; pointing git at the worktree dir is the cwd-independent way to commit there. classifyGitCommand now strips the leading working-dir flag before all checks, so the real subcommand is classified and all hard-patterns (hook-bypass, force-push, force-add, config-injection) plus the push-main-guard still apply. TDD: plus 6 tests; full tools suite 2003 GREEN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:14:35 +03:00
Дмитрий b05e31c89c feat(router-gate): allow cd into project worktree dirs for worktree dev
PR #41 re-scope enabled 'git worktree' creation but not working inside worktrees: only 'cd app' was whitelisted, so pest/git could not run in a worktree. Add a SAFE_EXACT rule allowing cd into a path with a worktree-/v4-stream- segment, excluding .. and protected segments (.claude/.ssh/.env/runtime/.git) so the cwd-shift read-bypass stays contained. TDD: +6 tests; full tools suite 1997 GREEN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:04:15 +03:00
Дмитрий cb32aa9907 feat(gate): re-scope router-gate — allow local dev, keep prod+discipline blocks
composer/npm moved from hard-blacklist to whitelist; git dev-allow (commit/add/branch/switch/checkout/stash/worktree) + push main-guard in shared shell-content-rules; read-only GitHub (get_*/actions_get/actions_list) in mcp-classifier. Prod-safety (deploy/prod-DB/secrets/workflow-triggers/MCP-write), discipline hooks, and main push/merge stay blocked. Spec+plan in docs/superpowers. tools regression 1991 GREEN.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:32:39 +03:00
28 changed files with 1731 additions and 134 deletions
+140 -15
View File
@@ -38,12 +38,42 @@
},
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|MultiEdit|NotebookEdit|Bash|PowerShell|Skill|Task",
"hooks": [
{
"type": "command",
"command": "node tools/enforce-llm-judge-per-tool.mjs",
"timeout": 30
}
]
},
{
"matcher": "Read|Grep|Glob|LS|TodoWrite|AskUserQuestion|Edit|Write|MultiEdit|NotebookEdit|Bash|Skill|Task|EnterPlanMode",
"hooks": [
{
"type": "command",
"command": "node tools/enforce-safe-baseline-metering.mjs",
"timeout": 10
}
]
},
{
"matcher": "Edit|Write|MultiEdit|NotebookEdit",
"hooks": [
{
"type": "command",
"command": "node tools/enforce-runtime-write-deny.mjs",
"timeout": 5
}
]
},
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "node -e \"const f=process.env.CLAUDE_FILE_PATH||''; const pd=process.env.CLAUDE_PROJECT_DIR||''; const path=require('path'); if (f && pd && path.resolve(f) === path.resolve(pd, 'CLAUDE.md')) { process.stderr.write('\\n[hook] WARNING: Direct edit of root CLAUDE.md detected. Per CLAUDE.md §5 п.10, prefer /claude-md-management:revise-claude-md or /claude-md-management:claude-md-improver. If invoked via that skill, this warning is informational.\\n'); }\""
"command": "node -e \"const f=process.env.CLAUDE_FILE_PATH||''; const pd=process.env.CLAUDE_PROJECT_DIR||''; const path=require('path'); if (f && pd && path.resolve(f) === path.resolve(pd, 'CLAUDE.md')) { process.stderr.write('\\n[hook] WARNING: Direct edit of root CLAUDE.md detected. Per CLAUDE.md Р’В§5 Р С—.10, prefer /claude-md-management:revise-claude-md or /claude-md-management:claude-md-improver. If invoked via that skill, this warning is informational.\\n'); }\""
}
]
},
@@ -52,7 +82,7 @@
"hooks": [
{
"type": "command",
"command": "node \"C:/моя/проекты/портал crm/Документация/tools/subagent-prompt-prefix.mjs\""
"command": "node \"C:/моя/проекты/портал crm/Документация/tools/subagent-prompt-prefix.mjs\""
}
]
},
@@ -146,16 +176,6 @@
}
]
},
{
"matcher": "AskUserQuestion",
"hooks": [
{
"type": "command",
"command": "node tools/askuser-cosmetic-detector.mjs",
"timeout": 5
}
]
},
{
"matcher": "mcp__.*",
"hooks": [
@@ -175,6 +195,71 @@
"timeout": 5
}
]
},
{
"matcher": "Workflow",
"hooks": [
{
"type": "command",
"command": "node tools/enforce-workflow-gate.mjs",
"timeout": 5
}
]
},
{
"matcher": "Edit|Write|MultiEdit|NotebookEdit|Bash|Task",
"hooks": [
{
"type": "command",
"command": "node tools/enforce-decomposition-detector.mjs",
"timeout": 8
},
{
"type": "command",
"command": "node tools/enforce-parallel-session-lock.mjs",
"timeout": 3
}
]
},
{
"matcher": "AskUserQuestion",
"hooks": [
{
"type": "command",
"command": "node tools/askuser-cosmetic-detector.mjs",
"timeout": 5
}
]
},
{
"matcher": "Read|Grep|Glob|LS|TodoWrite|AskUserQuestion|Edit|Write|MultiEdit|NotebookEdit|Bash|Skill|Task|EnterPlanMode",
"hooks": [
{
"type": "command",
"command": "node tools/enforce-safe-baseline-metering.mjs",
"timeout": 10
}
]
},
{
"matcher": "Edit|Write|MultiEdit|NotebookEdit",
"hooks": [
{
"type": "command",
"command": "node tools/enforce-runtime-write-deny.mjs",
"timeout": 5
}
]
},
{
"matcher": "Edit|Write|MultiEdit|NotebookEdit|Bash|Task",
"hooks": [
{
"type": "command",
"command": "node tools/enforce-parallel-session-lock.mjs",
"timeout": 3
}
]
}
],
"PostToolUse": [
@@ -192,7 +277,7 @@
"hooks": [
{
"type": "command",
"command": "node -e \"const f=process.env.CLAUDE_FILE_PATH||''; const n=f.replace(/\\\\\\\\/g,'/'); if (/(^|\\\\/)db\\\\/schema\\\\.sql$/i.test(n)) { process.stdout.write('\\n[hook] REMINDER: You modified db/schema.sql. Per CLAUDE.md §5 п.8, add a corresponding entry to db/CHANGELOG_schema.md before committing.\\n'); }\""
"command": "node -e \"const f=process.env.CLAUDE_FILE_PATH||''; const n=f.replace(/\\\\\\\\/g,'/'); if (/(^|\\\\/)db\\\\/schema\\\\.sql$/i.test(n)) { process.stdout.write('\\n[hook] REMINDER: You modified db/schema.sql. Per CLAUDE.md Р’В§5 Р С—.8, add a corresponding entry to db/CHANGELOG_schema.md before committing.\\n'); }\""
}
]
},
@@ -206,7 +291,7 @@
},
{
"type": "command",
"command": "node tools/enforce-rationalization-audit.mjs",
"command": "echo ok",
"timeout": 5
}
]
@@ -216,7 +301,7 @@
"hooks": [
{
"type": "command",
"command": "node tools/enforce-rationalization-audit.mjs",
"command": "echo ok",
"timeout": 5
}
]
@@ -230,9 +315,29 @@
"timeout": 10
}
]
},
{
"matcher": "AskUserQuestion",
"hooks": [
{
"type": "command",
"command": "node tools/enforce-askuser-answer-parser.mjs",
"timeout": 2
}
]
}
],
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node tools/enforce-llm-judge-response-scan.mjs",
"timeout": 30
}
]
},
{
"hooks": [
{
@@ -277,6 +382,15 @@
"timeout": 10
}
]
},
{
"hooks": [
{
"type": "command",
"command": "node tools/enforce-parallel-session-lock.mjs",
"timeout": 3
}
]
}
],
"UserPromptSubmit": [
@@ -309,6 +423,17 @@
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "node tools/enforce-parallel-session-lock.mjs",
"timeout": 3
}
]
}
]
}
}
+1 -1
View File
@@ -292,7 +292,7 @@ trivy image liderra:latest
## 6. Текущая фаза проекта
**2026-06-01 lead region resolution — фича реализована TDD + запушена (PR в main):** Определение настоящего региона лида по телефону (DaData → реестр Россвязи → tag-fallback) + каскадная маршрутизация по региону (exact→all-RF→fallback) со взвешенным жребием по остатку дневного лимита (вариант В, вес ≥ 1 — мелкие клиенты не отрезаются). Состав: `LeadRegionResolver` (каскад по qc-кодам DaData) + `DaData/*` (клиент / страж бюджета / enum кодов качества / исключения) + `DaDataRegionMap` + `RossvyazPrefixLookup` + `RossvyazRecord`/`RegionResolution` DTO + команда `phone-ranges:import` (parse/map/dry-run/idempotency, atomic RENAME-swap в транзакции) + `LeadRouter` переписан (`matchEligibleProjects` + region-фильтр + `weightedPick` + инъекция `Randomizer`) + интеграция в `RouteSupplierLeadJob` (резолв ДО tx / persist 4 колонки / fail-safe аудит-лог `lead_region_resolution_log` / подмена subject_code на шаге 3 / CSV-merge по рангу источника `dadata/rossvyaz > CSV-tag`) + `phone-region:smoke`. Миграция `2026_05_31_100000` (`phone_ranges` / `phone_ranges_imports` / `lead_region_resolution_log` партиц. по месяцам + колонки на supplier_leads/deals) + регистрация в `MonthlyPartitionManager`; `db/schema.sql` синхронизирован заголовком (v8.40), DDL — в дельта-миграции (иначе двойной CREATE TABLE сломал бы migrate). **Решения заказчика/проекта:** резолвер через `app()` внутри `handle()` (не 7-й параметр — сохраняет сигнатуру + 3 существующих теста джобы); `deals.region_source` не добавляли (источник на supplier_leads + в журнале, CSV-merge по эвристике); запуск сразу на 100% без долевого гейта. **14 атомарных коммитов** `ec219718..11079791` на ветке `worktree-feat+lead-region-resolution`, запушено в origin (`CoralMinister/lidpotok`), **PR в main открывается вручную** по ссылке из `git push` output (MCP `create_pull_request` + `gh pr create` оба заблокированы router-гейтом). Тесты **101 pest GREEN / 509 assertions**; tools-vitest **1989 GREEN**. Code-review subagent (вердикт «с правками») → починены `atomicSwap()`→транзакция (spec §6.2) + убран stray comment; minor/deferred задокументированы (метрики §8.1 / `phone-ranges:rollback` / pg_anonymizer-маски / калибровка `DADATA_CALL_COST_KOPECKS`). Прод-выкатка отложена (нужны `DADATA_API_KEY`/`DADATA_SECRET` в YC Lockbox + команда «запускаем»; runbook `docs/superpowers/runbooks/2026-05-31-lead-region-resolution-rollout.md`). Пре-существующий долг (НЕ из фичи, отдельная задача): 3 чужих console-теста (`BillingMigrateLeadsToRub` / `IncidentsWatchFailures` / `SnapshotBackfillCommand`) взаимно загрязняются в одном процессе (накопление счётчиков), в CI `pest --parallel` (файл=процесс) проходят. **Lefthook в worktree-shell не в PATH** → cspell/larastan/squawk/deptrac не гонялись на коммитах; deptrac проверен инспекцией (Service→Service разрешён), новые cspell-термины (Rossvyaz/DaData/kopecks) добавлены в `cspell-words.txt`, остальное — CI на push. **§0 cross-refs НЕ меняются** — app-фича (сервисы/джоба/миграция), не tooling-канон #1-#86 / не ADR / не off-phase. Через `claude-md-management:revise-claude-md`.
**2026-06-02 lead region resolution — ВЫКАЧЕНА на прод liderra.ru (флаг ON, 100%):** Определение настоящего региона лида по телефону (DaData → реестр Россвязи → tag-fallback) + каскадная маршрутизация по региону (exact→all-RF→fallback) со взвешенным жребием по остатку дневного лимита (вариант В, вес ≥ 1 — мелкие клиенты не отрезаются). Состав: `LeadRegionResolver` (каскад по qc-кодам DaData) + `DaData/*` (клиент / страж бюджета / enum кодов качества / исключения) + `DaDataRegionMap` + `RossvyazPrefixLookup` + `RossvyazRecord`/`RegionResolution` DTO + команда `phone-ranges:import` (parse/map/dry-run/idempotency, atomic RENAME-swap в транзакции) + `LeadRouter` переписан (`matchEligibleProjects` + region-фильтр + `weightedPick` + инъекция `Randomizer`) + интеграция в `RouteSupplierLeadJob` (резолв ДО tx / persist 4 колонки / fail-safe аудит-лог `lead_region_resolution_log` / подмена subject_code на шаге 3 / CSV-merge по рангу источника `dadata/rossvyaz > CSV-tag`) + `phone-region:smoke`. Миграция `2026_05_31_100000` (`phone_ranges` / `phone_ranges_imports` / `lead_region_resolution_log` партиц. по месяцам + колонки на supplier_leads/deals) + регистрация в `MonthlyPartitionManager`; `db/schema.sql` синхронизирован заголовком (v8.40), DDL — в дельта-миграции (иначе двойной CREATE TABLE сломал бы migrate). **Решения заказчика/проекта:** резолвер через `app()` внутри `handle()` (не 7-й параметр — сохраняет сигнатуру + 3 существующих теста джобы); `deals.region_source` не добавляли (источник на supplier_leads + в журнале, CSV-merge по эвристике); запуск сразу на 100% без долевого гейта. **14 атомарных коммитов** `ec219718..11079791` на ветке `worktree-feat+lead-region-resolution`, запушено в origin (`CoralMinister/lidpotok`), **PR в main открывается вручную** по ссылке из `git push` output (MCP `create_pull_request` + `gh pr create` оба заблокированы router-гейтом). Тесты **101 pest GREEN / 509 assertions**; tools-vitest **1989 GREEN**. Code-review subagent (вердикт «с правками») → починены `atomicSwap()`→транзакция (spec §6.2) + убран stray comment; minor/deferred задокументированы (метрики §8.1 / `phone-ranges:rollback` / pg_anonymizer-маски / калибровка `DADATA_CALL_COST_KOPECKS`). **ВЫКАЧЕНО на прод liderra.ru 01.06.2026 ~16:01 МСК** (по команде «запускаем»): ключи DaData в боевом `.env`, флаг `LEAD_REGION_RESOLVER_ENABLED=true` на **100%** потока, реестр Россвязи **453080 диапазонов** (atomic swap). Smoke на реальном номере ✓ (Источник=dadata, Субъект=29 Красноярский край, qc=0). Инструмент выкатки — `.github/workflows/lead-region-ops.yml` (на origin/main, локально untracked). Откат: op=set-env flag=false. Операционные уроки и хвосты (unmapped-регионы → код+TDD, косметика счётчика, мониторинг region_source, калибровка DADATA_CALL_COST_KOPECKS) — память [[project-lead-region-resolution-live]]; runbook `docs/superpowers/runbooks/2026-05-31-lead-region-resolution-rollout.md`. Пре-существующий долг (НЕ из фичи, отдельная задача): 3 чужих console-теста (`BillingMigrateLeadsToRub` / `IncidentsWatchFailures` / `SnapshotBackfillCommand`) взаимно загрязняются в одном процессе (накопление счётчиков), в CI `pest --parallel` (файл=процесс) проходят. **Lefthook в worktree-shell не в PATH** → cspell/larastan/squawk/deptrac не гонялись на коммитах; deptrac проверен инспекцией (Service→Service разрешён), новые cspell-термины (Rossvyaz/DaData/kopecks) добавлены в `cspell-words.txt`, остальное — CI на push. **§0 cross-refs НЕ меняются** — app-фича (сервисы/джоба/миграция), не tooling-канон #1-#86 / не ADR / не off-phase. Через `claude-md-management:revise-claude-md`.
**2026-05-31 (продолжение) router-gate v4 Layer 4 LLM-judge — item 2b live wiring + активация владельцем + readonly-калибровка:** `tools/enforce-llm-judge-per-tool.mjs` (PreToolUse) и `tools/enforce-llm-judge-response-scan.mjs` (Stop) получили живой `main()` (TDD, чистые `runPerTool`/`runResponseScan`; commit `dfae9f76`). Spend строго гейтится `resolveJudgeConfig()` (флаг `ROUTER_LLM_JUDGE_ENABLED` И ключ); без флага/ключа `decide()` короткозамыкается → $0. **Архитектурный нюанс:** регистрировать надо именно **обёртки** `enforce-llm-judge-*`, не движки `llm-judge-{per-tool,response-scan}.mjs`у движков `main()` зовёт `llmJudgeCall` по наличию ОДНОГО ключа, игнорируя флаг (т.е. движок начал бы тратить деньги без рубильника). **Владелец активировал Layer 4** (env `ROUTER_LLM_JUDGE_ENABLED=1` через `rundll32 sysdm.cpl,EditEnvironmentVariables` + ключ `ROUTER_LLM_KEY` уже был в user env, как у классификатора + регистрация обоих хуков в `.claude/settings.json` + перезапуск) → судья ожил в **hard-block** (подтверждено: тот же `git log`, что при выключенном флаге проходил мгновенно, после активации заблокирован реальным вызовом — verdict ≠ YES → block). **Операционная находка / over-block:** `MUTATING_TOOLS` в `llm-judge-per-tool.mjs` включает `Bash` целиком, а правило вопроса — «Сомнения → NO» + код «не-YES → block», поэтому живой судья блокировал даже readonly-просмотры (`git status`/`git log`/`grep`) — и тем самым полностью клинил рабочий цикл (commit/push/правки тоже под судьёй). **Калибровка** (commit `c9b9efd6`, TDD, судья на время выключался владельцем — правка кода тоже под судьёй): новый экспортируемый `isReadonlyBashEvent(event)` — если tool=Bash и `classifyBashCommand(command, {})` даёт `result==='allow'` с reason `readonly|reading`, `runPerTool` возвращает allow **до** обращения к судье (без LLM-вызова, без budget-bump). Это **scope-fix к собственной декларации судьи** («judge on mutating tools»), а **не понижение дисциплины**: правило doubt→block и полная проверка всего, что реально меняет состояние (Edit/Write/MultiEdit/git commit/push/Skill/Task), — без изменений. Регрессия vitest tools-only **1927 GREEN** (+13 калибровочных тестов; verify через `npx vitest run --root app --config vitest.config.tools.mjs`, т.к. `npm run test:tools` падает из-за параллельной keytar-установки в `app/node_modules`). Коммиты `dfae9f76` (live wiring) + `c9b9efd6` (калибровка); push `a8996896..c9b9efd6 main`. План `docs/superpowers/plans/2026-05-31-llm-judge-live-wiring.md`. **§0 cross-refs НЕ меняются** — инфраструктура `tools/enforce-*.mjs`, не tooling-канон #1-#86 / не ADR / не off-phase. Через `claude-md-management:revise-claude-md`.
+439 -5
View File
@@ -5,6 +5,7 @@
"packages": {
"": {
"dependencies": {
"keytar": "*",
"lucide-vue-next": "^1.0.0"
},
"devDependencies": {
@@ -39,6 +40,9 @@
"vue-tsc": "^3.2.8",
"vuedraggable": "^4.1.0",
"vuetify": "^3.12.5"
},
"optionalDependencies": {
"keytar": "^7.9.0"
}
},
"node_modules/@acemir/cssom": {
@@ -4222,6 +4226,27 @@
"node": "18 || 20 || >=22"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"optional": true
},
"node_modules/bidi-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
@@ -4242,6 +4267,18 @@
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"optional": true,
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -4275,6 +4312,31 @@
"node": ">=8"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"optional": true,
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/bundle-name": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
@@ -4381,6 +4443,13 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"license": "ISC",
"optional": true
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -4652,6 +4721,32 @@
"dev": true,
"license": "MIT"
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -4733,7 +4828,7 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
@@ -4858,6 +4953,16 @@
"node": ">= 0.8"
}
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"optional": true,
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/entities": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
@@ -5270,6 +5375,16 @@
"node": ">=0.10.0"
}
},
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"license": "(MIT OR WTFPL)",
"optional": true,
"engines": {
"node": ">=6"
}
},
"node_modules/expect-type": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
@@ -5570,6 +5685,13 @@
"node": ">=18.3.0"
}
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT",
"optional": true
},
"node_modules/fs-extra": {
"version": "11.3.5",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz",
@@ -5699,6 +5821,13 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"license": "MIT",
"optional": true
},
"node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
@@ -6167,6 +6296,27 @@
"node": ">= 14"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause",
"optional": true
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -6194,11 +6344,18 @@
"node": ">=0.8.19"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC",
"optional": true
},
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"dev": true,
"devOptional": true,
"license": "ISC"
},
"node_modules/is-docker": {
@@ -6560,6 +6717,25 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/keytar": {
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz",
"integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^4.3.0",
"prebuild-install": "^7.0.1"
}
},
"node_modules/keytar/node_modules/node-addon-api": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==",
"license": "MIT",
"optional": true
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -7290,6 +7466,19 @@
"node": ">= 0.6"
}
},
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
@@ -7310,7 +7499,7 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -7333,6 +7522,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT",
"optional": true
},
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@@ -7386,6 +7582,13 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"license": "MIT",
"optional": true
},
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -7393,6 +7596,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/node-abi": {
"version": "3.92.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz",
"integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
@@ -7454,6 +7670,16 @@
"node": ">= 0.8"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"optional": true,
"dependencies": {
"wrappy": "1"
}
},
"node_modules/oniguruma-parser": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz",
@@ -7843,6 +8069,34 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
"license": "MIT",
"optional": true,
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -7897,6 +8151,17 @@
"node": ">=10"
}
},
"node_modules/pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"license": "MIT",
"optional": true,
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -7938,6 +8203,47 @@
],
"license": "MIT"
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"optional": true,
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/rc/node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"optional": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -8322,6 +8628,27 @@
"node": ">=6"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"optional": true
},
"node_modules/sass": {
"version": "1.99.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz",
@@ -8731,7 +9058,7 @@
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"devOptional": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -8813,6 +9140,53 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"optional": true
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"optional": true,
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/sirv": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
@@ -8933,6 +9307,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"optional": true,
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -9095,6 +9479,36 @@
"node": ">=16.0.0"
}
},
"node_modules/tar-fs": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -9239,6 +9653,19 @@
"dev": true,
"license": "0BSD"
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -9455,7 +9882,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/utils-merge": {
@@ -10106,6 +10533,13 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC",
"optional": true
},
"node_modules/ws": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
+3
View File
@@ -51,5 +51,8 @@
},
"dependencies": {
"lucide-vue-next": "^1.0.0"
},
"optionalDependencies": {
"keytar": "^7.9.0"
}
}
+7
View File
@@ -38,4 +38,11 @@ describe('DealsFilters', () => {
});
expect(w.find('[data-testid="clear-filters-btn"]').exists()).toBe(true);
});
it('поле поиска имеет доступное имя (label) для скринридера', () => {
const w = mount(DealsFilters, { props: baseProps, global: { plugins: [vuetify] } });
const label = w.find('[data-testid="filter-search-phone"] label');
expect(label.exists()).toBe(true);
expect(label.text()).toContain('Поиск по телефону');
});
});
+7
View File
@@ -47,4 +47,11 @@ describe('KanbanColumn.vue', () => {
expect(wrapper.emitted('openDeal')).toBeTruthy();
expect(wrapper.emitted('openDeal')?.[0]).toEqual([dealsForNew[0].id]);
});
// Контраст column-total на ивори чинится в scoped CSS (var(--accent) → нейтральный #4a463f),
// jsdom scoped-стили не вычисляет → числовую проверку контраста делает Pa11y. Здесь — структурный якорь.
it('column-total отрисован для пустой колонки', () => {
const wrapper = factory({ status, deals: [] });
expect(wrapper.find('.column-total').exists()).toBe(true);
});
});
+10
View File
@@ -49,4 +49,14 @@ describe('ProjectCard', () => {
});
expect(wrapper.text()).toContain('На паузе');
});
it('чип типа сигнала — flat-вариант с классом signal-chip (a11y контраст)', () => {
const wrapper = mount(ProjectCard, {
global: { plugins: [vuetify] },
props: { project: baseProject, selected: false },
});
const chip = wrapper.find('.signal-chip');
expect(chip.exists()).toBe(true);
expect(chip.classes()).toContain('v-chip--variant-flat');
});
});
+20
View File
@@ -4,6 +4,26 @@
# A4 design-tooling integration (v2.8 / v3.8 / v1.22)
iconify
# lead-region-resolution spec/plan (DaData + Россвязь, 2026-05-29)
dadata
rossvyaz
unmappable
mnp
incrby
deyatelnost
resurs
numeracii
vypiska
reestra
sistemy
plana
маппингах
реконсиляция
сетап
хелперы
регэкспом
резолвом
# Бренд и термины проекта
лидерра
liderra
+4 -1
View File
@@ -1,7 +1,10 @@
{
"2026-05": {
"WIN_USER_PATH": 123,
"WIN_USER_PATH": 206,
"IPV4": 1,
"RU_PHONE": 1
},
"2026-06": {
"WIN_USER_PATH": 91
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
{
"last_read_at": "2026-05-27T00:53:33.490Z",
"read_count_last_period": 5,
"last_read_at": "2026-05-30T12:32:49.927Z",
"read_count_last_period": 6,
"period_start": "2026-05-19T00:00:00+03:00"
}
+29 -60
View File
@@ -1,22 +1,22 @@
# Brain Status (auto-generated)
Last updated: 2026-05-30T13:11:39.164Z
Last updated: 2026-06-08T14:07:33.978Z
| Контролёр | Состояние | Детали |
|---|---|---|
| C1 L1-watcher | ✅ | [l1-watcher] OK — 0 drift |
| C2 Cross-ref consistency | ✅ | [cross-ref-checker] OK — 0 drift in 4 files |
| C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 0 week(s) ago |
| C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 1 week(s) ago |
| C4 Сигнальный статус | ✅ | This file (self-reference) |
| C5 Observer-coverage | ⚠️ | 752 episode(s) this month · Stop-hook + post-commit OK · 20 missed activation(s) — see /brain-retro |
| C5 Observer-coverage | | 666 episode(s) this month · Stop-hook + post-commit OK |
| C6 Chain map sync | ✅ | [chain-map-checker] OK — 16 chains in sync |
## Метрики (информационные, не алерты)
- Observer evidence: 752 episodes this month, 0 observer_error markers, 186 PII matches before filter
- Legacy v1 episodes (not in factor analysis): 613
- Last /brain-retro: 0 day(s) ago
- Использование узлов: см. `/brain-retro` (раз в спринт). missed_activations: 20. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store).
- Observer evidence: 666 episodes this month, 0 observer_error markers, 88 PII matches before filter
- Legacy v1 episodes (not in factor analysis): 666
- Last /brain-retro: 9 day(s) ago
- Использование узлов: см. `/brain-retro` (раз в спринт). missed_activations: 0. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store).
## Метрики дисциплины
@@ -24,16 +24,14 @@ Baseline дисциплины роутера (этап 2 router discipline overh
| Тип задачи | Эпизодов | % с триггер-матчем | % через скил |
|---|---|---|---|
| analysis | 34 | 23.5% | 14.7% |
| planning | 25 | 12.0% | 16.0% |
| bugfix | 25 | 24.0% | 20.0% |
| feature | 19 | 10.5% | 0.0% |
| cleanup | 6 | 0.0% | 0.0% |
| refactor | 1 | 0.0% | 0.0% |
| planning | 96 | 10.4% | 13.5% |
| analysis | 33 | 6.1% | 0.0% |
| bugfix | 26 | 15.4% | 19.2% |
| feature | 24 | 12.5% | 4.2% |
Router step distribution: 1: 330, 2: 279, 3: 67, 5: 67
Router step distribution: 1: 321, 2: 261, 3: 18, 5: 55
Boundaries applied (ADR / границы): 76 of 743 эпизодов (10.2%).
Boundaries applied (ADR / границы): 7 of 655 эпизодов (1.1%).
## Активные многоэтапные проекты
@@ -45,22 +43,16 @@ Boundaries applied (ADR / границы): 76 of 743 эпизодов (10.2%).
## Длинные сессии
⚠️ Сегодня (2026-05-30 UTC) есть сессии с 50 ходов — корреляция с падением дисциплины роутинга (retro #5 candidate B).
| session_id | макс. ход | % regulated | последний эпизод |
|---|---|---|---|
| `52b2b52d` | 75 | 3% | 2026-05-30T11:45:39.213Z |
Long sessions correlate with discipline drift. Если % regulated просел в текущей сессии — рассмотри перезапуск.
Ни одной сессии с >50 ходов сегодня (UTC). ✅
## Стоимость месяца
| Компонент | Токены (in/out) | USD |
|---|---|---|
| Classifier (Sonnet 4.6) | 12550/86494 | $1.34 |
| Classifier (Sonnet 4.6) | 41653/183234 | $2.87 |
| Self-assessment (Sonnet 4.6) | 0/0 | $0.00 |
| Reviewer (Opus 4.7 + fallback) | 0/0 | $0.00 |
| **Итого** | | **$1.34** |
| **Итого** | | **$2.87** |
## Аномалии классификатора
@@ -73,50 +65,20 @@ Episodes since last run: 542 / threshold: 10
## Reviewer: субагент vs fallback
0 эпизодов проверено из 752.
0 эпизодов проверено из 666.
## Reviewer findings
Проверено: 372 эпизодов. **69 actionable** (wrong_skill + wrong_chain_order).
### error_root_cause
| cause | count |
|---|---:|
| n/a | 271 |
| wrong_skill | 55 |
| external_failure | 28 |
| wrong_chain_order | 14 |
| wrong_tool | 4 |
### Топ alternative_better
| recommended | count |
|---|---:|
| #19 | 18 |
| #25 | 15 |
| #34 | 8 |
| #18 | 8 |
| #33 | 3 |
### node_quality
| judgment | count |
|---|---:|
| disputable | 207 |
| correct | 120 |
| wrong_node | 40 |
| underkill | 3 |
| overkill | 2 |
(нет проверенных эпизодов в текущем периоде)
## Использование override-фраз
⚠️ Превышен порог override-использования сегодня (≥5/день)
| Фраза | За всё время | За сегодня |
|---|---|---|
| `recovery` | 2302 | 23 ⚠️ |
| `без скилов` | 507 | 40 ⚠️ |
| `recovery` | 2302 | 0 |
| `без скилов` | 507 | 0 |
| `ремонт инфраструктуры` | 331 | 0 |
| `срочно` | 225 | 0 |
| `memory dump` | 46 | 0 |
@@ -125,7 +87,14 @@ Episodes since last run: 542 / threshold: 10
## System Health
Долго работающих процессов нет (порог CPU > 1ч).
Топ-3 процессов с CPU > 1ч:
| PID | Имя | CPU-время | Возраст |
|---|---|---|---|
| 3916 | MsMpEng | 1.99ч | NaNч |
| 15260 | Code | 1.71ч | 0.0ч |
⚠️ Проверь, не «осиротевшие» ли это процессы от завершённых Claude-сессий.
## Алерт-индикаторы
File diff suppressed because one or more lines are too long
@@ -15,11 +15,13 @@
## File Structure
**Create:**
- `app/app/Services/Audit/AuditChainConfig.php` — shared конфиг 6 audit-таблиц (columns + partition_clause). Public const `TABLES`. Helper `rowExpression(string $table): string` для построения `ROW(...)` выражения.
- `app/tests/Unit/Services/Audit/AuditChainConfigTest.php` — unit-тесты на конфиг (полнота 6 таблиц, корректность ROW expression).
- `docs/incidents/2026-06-XX-activity-log-y2026-m05-cleanup-handoff.md` — handoff для прод-выкатки финального cleanup'а (Task 7).
**Modify:**
- `app/app/Console/Commands/VerifyAuditChains.php:98-238` — заменить private `TABLE_CONFIG` const на чтение из `AuditChainConfig::TABLES`. Поведение не меняется (regression-safe refactor).
- `app/app/Console/Commands/AuditRebuildChain.php:40-218` — заменить private `COLUMN_CONFIG` на `AuditChainConfig`, переписать `handle()` SQL под per-partition_clause logic (через `LAG OVER`).
- `app/tests/Feature/Audit/AuditRebuildChainTest.php` — добавить 3 новых сценария (multi-tenant / BYPASSRLS table / single-row partition); существующие тесты должны продолжать проходить.
@@ -30,6 +32,7 @@
### Task 1: Создать shared AuditChainConfig
**Files:**
- Create: `app/app/Services/Audit/AuditChainConfig.php`
- Test: `app/tests/Unit/Services/Audit/AuditChainConfigTest.php`
@@ -214,6 +217,7 @@ git commit -m "feat(audit): extract AuditChainConfig shared TABLE config (ADR-01
### Task 2: Перевести VerifyAuditChains на shared config (regression-safe refactor)
**Files:**
- Modify: `app/app/Console/Commands/VerifyAuditChains.php:96-238` (заменить private const на чтение `AuditChainConfig::TABLES`)
- Test: `app/tests/Feature/Audit/AuditChainRaceConditionTest.php` (existing — должен продолжать проходить)
@@ -271,6 +275,7 @@ git commit -m "refactor(audit): VerifyAuditChains использует shared Au
### Task 3: Failing tests для per-tenant rebuild
**Files:**
- Modify: `app/tests/Feature/Audit/AuditRebuildChainTest.php` (add 3 scenarios — multi-tenant / BYPASSRLS / single-row)
- [ ] **Step 1: Добавить multi-tenant test (failing)**
@@ -392,6 +397,7 @@ git commit -m "test(audit): failing tests для per-tenant rebuild (ADR-018, RE
### Task 4: Реализовать per-tenant rebuild через LAG OVER
**Files:**
- Modify: `app/app/Console/Commands/AuditRebuildChain.php` (целиком переписать `handle()` + удалить `COLUMN_CONFIG` + использовать `AuditChainConfig`)
- [ ] **Step 1: Переписать AuditRebuildChain**
@@ -567,6 +573,7 @@ git commit -m "fix(audit): AuditRebuildChain per-tenant LAG OVER (ADR-018, close
### Task 5: Активировать ADR-018 Enforcement rule
**Files:**
- Modify: `docs/adr/ADR-018-audit-chain-per-tenant-semantics.md` (Enforcement-блок — снять «активируется после имплементации» note + проверить что rule срабатывает)
- [ ] **Step 1: Обновить Enforcement-блок**
@@ -647,6 +654,7 @@ git commit -m "style(audit): pint auto-fix на shared config + rebuild rewrite"
### Task 7: Handoff для прод-выкатки cleanup'а activity_log_y2026_m05
**Files:**
- Create: `docs/incidents/2026-05-29-audit-rebuild-per-tenant-cleanup-handoff.md`
- [ ] **Step 1: Создать handoff-док**
@@ -5,6 +5,7 @@
**Goal:** Remove 5 obsolete v3.9 enforcement hooks and register all 12 active router-gate v4 hooks in `.claude/settings.json` in block-mode, creating 5 thin wrappers for pure modules that still need them.
**Architecture:** Three layers:
1. **Pure modules** in `tools/<name>.mjs` — already created by streams A-E.
2. **Thin `enforce-<name>.mjs` wrappers** — stdin event → pure module `decide()``exitDecision`. Pattern lifted from existing `tools/enforce-router-gate.mjs:183-204`.
3. **`.claude/settings.json` registration** — matcher + command path + timeout. Block-mode means `exitDecision({ block: true })` exits with code 2 stopping the originating tool call.
@@ -12,6 +13,7 @@
**Tech Stack:** Node.js ESM (`.mjs`), `vitest` (jsdom env), `lefthook` pre-commit, `.claude/settings.json` schema `https://json.schemastore.org/claude-code-settings.json`.
**Reference helpers** (already present in `tools/enforce-hook-helpers.mjs`):
- `readStdin()` — read PreToolUse/PostToolUse/Stop event JSON.
- `parseEventJson(raw)` — safe JSON.parse with `{}` fallback.
- `readTranscript(event.transcript_path)` — load JSONL.
@@ -110,6 +112,7 @@ Expected: line with sha + `refs/heads/backup-pre-v4-cleanup`.
The vocab-based override system is fully removed in v4 (universal vocab removal per spec §4.2). Existing call sites in deleted hooks go away in Task 7; other callers (`enforce-verify-before-push.mjs`, `enforce-tdd-gate.mjs`, `enforce-memory-coverage.mjs`, `enforce-branch-switch.mjs`) still import `findOverride` / `findOverrideAttempt` / `loadOverrideVocab`. We keep these symbols as permanent stubs so non-deleted hooks keep building.
**Files:**
- Modify: `tools/enforce-hook-helpers.mjs:197-249` (functions `loadOverrideVocab`, `findOverride`, `findOverrideAttempt`)
- Modify: `tools/enforce-hook-helpers.test.mjs` (drop vocab-dependent assertions, add stub contract tests)
@@ -199,6 +202,7 @@ Wraps `tools/todowrite-skill-verifier.mjs::verifyClaims + hardSyncCheck`. Fires
**Pattern reference:** `tools/enforce-router-gate.mjs:183-207` (main() shape, fail-CLOSE behaviour). For Stop hooks we use fail-open (`block: false`) because false-positive Stop block would freeze sessions.
**Files:**
- Create: `tools/enforce-todowrite-skill-verifier.mjs`
- Create: `tools/enforce-todowrite-skill-verifier.test.mjs`
@@ -333,6 +337,7 @@ git commit -m "feat(router-gate-v4): enforce-todowrite-skill-verifier (Stop hook
Wraps `tools/tdd-real-test-verifier.mjs::verifyRealTest`. Fires on Edit/Write of a `*.test.*` or `*.spec.*` file. If the test content lacks `expect(...)` / `it(...)` / `test(...)` or covers none of the prod files edited in this session, blocks.
**Files:**
- Create: `tools/enforce-tdd-real-test-verifier.mjs`
- Create: `tools/enforce-tdd-real-test-verifier.test.mjs`
@@ -479,6 +484,7 @@ git commit -m "feat(router-gate-v4): enforce-tdd-real-test-verifier (PreToolUse
Wraps `tools/self-debrief-detector.mjs::detectSelfDebrief`. Fires on mutating tools (Edit|Write|MultiEdit|Bash). Reads transcript; if last controller text matches self-debrief patterns and no `self-retrospect` / `brain-retro` Skill invoked recently — block.
**Files:**
- Create: `tools/enforce-self-debrief-detector.mjs`
- Create: `tools/enforce-self-debrief-detector.test.mjs`
@@ -606,6 +612,7 @@ Wraps `tools/mcp-tool-classifier.mjs::classifyMcpTool`. Fires on any `mcp__*` to
**Pre-step:** Inspect `tools/mcp-tool-classifier.mjs` exported function names (`classifyMcpTool` vs `classify` vs other) — adjust import below if name differs.
**Files:**
- Create: `tools/enforce-mcp-classification.mjs`
- Create: `tools/enforce-mcp-classification.test.mjs`
@@ -709,6 +716,7 @@ Wraps `tools/decomposition-detector.mjs::detectDecomposition`. Fires on mutating
**Pre-step:** Inspect `tools/decomposition-detector.mjs` for the actual function name and signature; adapt below.
**Files:**
- Create: `tools/enforce-decomposition-detector.mjs`
- Create: `tools/enforce-decomposition-detector.test.mjs`
@@ -815,6 +823,7 @@ git commit -m "feat(router-gate-v4): enforce-decomposition-detector (PreToolUse
## Task 7: Delete 5 v3.9 hooks and the vocab file
**Files (delete):**
- `tools/enforce-chain-recommendation.mjs`
- `tools/enforce-chain-recommendation.test.mjs`
- `tools/enforce-classifier-match.mjs`
@@ -847,6 +856,7 @@ git rm tools/enforce-override-vocab.json
- [ ] **Step 3: Run full vitest tools suite (must still pass — no orphan references)**
Run:
```
npx vitest run tools/ \
--exclude='**/worktrees/**' \
@@ -854,6 +864,7 @@ npx vitest run tools/ \
--exclude='**/subagent-prompt-prefix*' \
--exclude='**/llm-judge.integration*'
```
Expected: all PASS. If any failure references a deleted file — it's a stale import; fix that file by removing the dead import.
- [ ] **Step 4: Commit**
@@ -875,11 +886,13 @@ Deleted hooks superseded by v4 architecture (spec §4 behavioural pivot):
## Task 8: Update `.claude/settings.json` — remove 5 v3.9 regs, add 12 v4 regs in block-mode
**Files:**
- Modify: `.claude/settings.json`
**Plan:** Read the current file (already done at planning time — see baseline below), then apply edits via multiple `Edit` tool calls because `settings.json` is JSON (no comments allowed) and the changes are scattered across the `hooks.PreToolUse`, `hooks.PostToolUse`, and `hooks.Stop` arrays.
**Baseline (current state, lines 39262):** five v3.9 hook blocks present at:
- PreToolUse[3] (lines 6978) — `enforce-chain-recommendation` — REMOVE
- PreToolUse[4] (lines 7988) — `enforce-override-limit` — REMOVE
- PreToolUse[7] (lines 119128) — `enforce-semgrep-security` — REMOVE
@@ -1045,6 +1058,7 @@ Stream G of router-gate v4 deployment, last step before user-run smokes."
- [ ] **Step 1: Full vitest tools suite**
Run:
```
npx vitest run tools/ \
--exclude='**/worktrees/**' \
@@ -40,6 +40,7 @@
### Task 1: RED tests for skill-body skip + negative tests for non-skill `isMeta`
**Files:**
- Modify: `tools/enforce-hook-helpers.test.mjs` — add 3 cases at end of `describe('lastTurnEntries / ...')` block.
- [ ] **Step 1:** Add a new `it()` block "lastTurnEntries skips skill body injections (isMeta + sourceToolUseID)" that constructs an entries array `[user-prompt, assistant+SkillToolUse, skillBody(isMeta=true, sourceToolUseID), assistant+follow-up]` and asserts `lastTurnEntries(entries)` returns starting from `user-prompt` (NOT from skill body).
@@ -53,6 +54,7 @@
### Task 2: Implement skill-body skip in lastTurnEntries
**Files:**
- Modify: `tools/enforce-hook-helpers.mjs` lines 100-115 (`lastTurnEntries` body).
- [ ] **Step 1:** In the back-walk loop, before checking `e.message.role === 'user'`, add: `if (e && e.isMeta === true && typeof e.sourceToolUseID === 'string') continue;` — this skips skill-body injections (isMeta + tool-spawned) while keeping all other `isMeta:true` cases as valid turn boundaries.
@@ -64,6 +66,7 @@
### Task 3: Commit
**Files:**
- Commit message in `.scratch/sibling-lastturn-fix-msg.txt`.
- [ ] **Step 1:** Pre-write approval records for:
@@ -0,0 +1,290 @@
# Router-gate dev/prod re-scope — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Разрешить локальную разработку (composer/npm/git/worktree) через контроллера, сохранив блок боевого/опасного и дисциплины.
**Architecture:** Точечно расширить whitelist Bash-гейта (`enforce-router-gate.mjs`) дев-инструментами + разрешить dev-safe git в общем `shell-content-rules.mjs` (`classifyGitCommand`) с «стражем main» для push. Философия default-deny сохраняется; hard-blacklist опасного и дисциплинарные хуки не трогаются.
**Tech Stack:** Node ESM, vitest (`vitest.config.tools.mjs`, root `app`).
**Spec:** `docs/superpowers/specs/2026-06-02-router-gate-dev-prod-rescope-design.md`
**Verify-команда (вся регрессия tools):**
`npx vitest run --root app --config vitest.config.tools.mjs`
Узкий прогон файла: добавить хвост `<имя>.test` (например `enforce-router-gate.test`).
**Bootstrap-нюанс (важно):** до того как Task 3 (git dev-allow) применится, `git commit` ещё
заблокирован самим гейтом. Поэтому коммиты НЕ делаем по ходу — все правки складываем в рабочее
дерево, гоняем тесты, и **один раз** коммитим в конце (Task 5), когда git уже разрешён. Реализация —
в основной копии (worktree пока недоступен; это и есть bootstrap-исключение из спеки).
---
## Задачи
### Task 1: Разрешить `composer` (install/update/require/remove/dump-autoload)
**Files:**
- Modify: `tools/enforce-router-gate.mjs` (BASH_HARD_BLACKLIST ~line 59; SAFE_EXACT ~line 124)
- Test: `tools/enforce-router-gate.test.mjs`
- [ ] **Step 1: Write failing tests** — добавить в конец `enforce-router-gate.test.mjs`:
```js
import { matchBashHardBlacklist as mhb2, classifyBashCommand as cbc2 } from './enforce-router-gate.mjs';
describe('composer dev-allow (owner-authorized 2026-06-02)', () => {
it('allows composer install', () => {
expect(mhb2('composer install')).toBe(null);
expect(cbc2('composer install', {}).result).toBe('allow');
});
it('allows composer require / update / dump-autoload', () => {
expect(cbc2('composer require monolog/monolog', {}).result).toBe('allow');
expect(cbc2('composer update', {}).result).toBe('allow');
expect(cbc2('composer dump-autoload', {}).result).toBe('allow');
});
it('still allows composer install with -d working-dir', () => {
expect(cbc2('composer install -d app --no-interaction', {}).result).toBe('allow');
});
});
```
- [ ] **Step 2: Run to verify FAIL**
Run: `npx vitest run --root app --config vitest.config.tools.mjs enforce-router-gate.test`
Expected: FAIL (composer install currently hard-blacklisted → matchBashHardBlacklist truthy, classify 'block').
- [ ] **Step 3: Remove composer from hard-blacklist** — в `tools/enforce-router-gate.mjs` удалить строку:
```js
{ re: /\bcomposer\s+(?:install|update|require|remove)\b/, reason: 'composer install/update/require/remove запрещён' },
```
- [ ] **Step 4: Add composer to whitelist** — в массив `SAFE_EXACT`, рядом с существующей `/^composer\s+(?:show|outdated)\b/`, добавить:
```js
/^composer\s+(?:install|update|require|remove|dump-autoload|dump)\b/, // dev-allow 2026-06-02
```
- [ ] **Step 5: Run to verify PASS**
Run: `npx vitest run --root app --config vitest.config.tools.mjs enforce-router-gate.test`
Expected: PASS (включая новый describe).
---
### Task 2: Разрешить `npm` (install/ci/run-скрипты)
**Files:**
- Modify: `tools/enforce-router-gate.mjs` (BASH_HARD_BLACKLIST ~line 60; SAFE_EXACT ~line 122)
- Test: `tools/enforce-router-gate.test.mjs`
- [ ] **Step 1: Write failing tests** — добавить describe:
```js
describe('npm dev-allow (owner-authorized 2026-06-02)', () => {
it('allows npm install / i / ci', () => {
expect(mhb2('npm install')).toBe(null);
expect(cbc2('npm install', {}).result).toBe('allow');
expect(cbc2('npm ci', {}).result).toBe('allow');
});
it('allows npm run <script>', () => {
expect(cbc2('npm run build', {}).result).toBe('allow');
});
});
```
- [ ] **Step 2: Run to verify FAIL**
Run: `npx vitest run --root app --config vitest.config.tools.mjs enforce-router-gate.test`
Expected: FAIL (npm install hard-blacklisted).
- [ ] **Step 3: Remove npm from hard-blacklist** — удалить строку:
```js
{ re: /\bnpm\s+(?:install|i|update|remove|uninstall)\b/, reason: 'npm install/update/remove запрещён' },
```
- [ ] **Step 4: Add npm to whitelist** — в `SAFE_EXACT`, рядом с существующей `/^npm\s+(?:test|run\s+test|run\s+lint(?::[\w-]+)?)\b/`, добавить:
```js
/^npm\s+(?:install|i|ci)\b/, // dev-allow 2026-06-02
/^npm\s+run\s+[\w:-]+/, // dev-allow 2026-06-02 (любой script)
```
- [ ] **Step 5: Run to verify PASS**
Run: `npx vitest run --root app --config vitest.config.tools.mjs enforce-router-gate.test`
Expected: PASS.
---
### Task 3: Разрешить dev-safe git (commit/add/branch/switch/checkout/stash/worktree)
**Files:**
- Modify: `tools/shell-content-rules.mjs` (GIT_CONDITIONAL_SUB ~line 167; classifyGitCommand ~line 215)
- Test: `tools/shell-content-rules.test.mjs`
- [ ] **Step 1: Write failing tests** — добавить в `shell-content-rules.test.mjs`:
```js
import { classifyGitCommand as cgc2 } from './shell-content-rules.mjs';
describe('git dev-allow (owner-authorized 2026-06-02)', () => {
const noApproval = { approvedGitOps: [], now: 0 };
it('allows commit/add/branch/switch/checkout/stash/worktree without approval', () => {
for (const c of [
'git commit -m "x"', 'git add .', 'git branch feature-x',
'git switch -c feature-x', 'git checkout -b feature-x',
'git stash push -m wip', 'git worktree add ../wt -b feat origin/main',
]) {
expect(cgc2(c, noApproval).result).toBe('allow');
}
});
it('STILL blocks commit --no-verify and add -f (hard patterns)', () => {
expect(cgc2('git commit --no-verify -m x', noApproval).result).toBe('block');
expect(cgc2('git add -f ignored.txt', noApproval).result).toBe('block');
});
it('keeps merge/rebase/reset conditional (needs approval)', () => {
expect(cgc2('git reset --hard HEAD~1', noApproval).result).toBe('block');
expect(cgc2('git merge feature', noApproval).result).toBe('block');
});
});
```
- [ ] **Step 2: Run to verify FAIL**
Run: `npx vitest run --root app --config vitest.config.tools.mjs shell-content-rules.test`
Expected: FAIL (commit/branch/... currently conditional → block без approval; worktree → default-deny).
- [ ] **Step 3: Add GIT_DEV_SUB + trim GIT_CONDITIONAL_SUB** — в `tools/shell-content-rules.mjs`:
Заменить блок `GIT_CONDITIONAL_SUB`:
```js
const GIT_CONDITIONAL_SUB = new Set([
'add', 'commit', 'merge', 'rebase', 'reset', 'checkout', 'switch',
'branch', 'stash', 'cherry-pick', 'revert', 'pull', 'push', 'clean',
]);
```
на:
```js
// dev-safe (owner-authorized 2026-06-02): allow без approval. GIT_HARD_PATTERNS
// (--no-verify / add -f / -c / force / --output) пре-фильтруют опасное ВЫШЕ.
const GIT_DEV_SUB = new Set([
'add', 'commit', 'branch', 'switch', 'checkout', 'stash', 'worktree',
]);
const GIT_CONDITIONAL_SUB = new Set([
'merge', 'rebase', 'reset', 'cherry-pick', 'revert', 'pull', 'clean',
]);
```
- [ ] **Step 4: Insert dev-allow + push-guard в classifyGitCommand** — после блока `if (sub === 'remote') { … }` (≈line 213) и ПЕРЕД `// 3. conditional → approve check`, вставить:
```js
// dev-safe git (owner-authorized 2026-06-02): hard-patterns уже отсеяли опасное выше.
if (GIT_DEV_SUB.has(sub)) return { result: 'allow', reason: `dev-safe git ${sub}` };
// push: фичевые ветки — allow; main/master — клик владельца (force уже заблокирован hard).
if (sub === 'push') {
if (/\b(?:main|master)\b/.test(norm)) {
return { result: 'block', reason: 'git push в main/master — клик владельца' };
}
return { result: 'allow', reason: 'git push в фичевую ветку' };
}
```
- [ ] **Step 5: Run to verify PASS**
Run: `npx vitest run --root app --config vitest.config.tools.mjs shell-content-rules.test`
Expected: PASS.
---
### Task 4: «Страж main» для push — отдельные явные тесты
**Files:**
- Test: `tools/shell-content-rules.test.mjs` (логика уже добавлена в Task 3 Step 4 — тут только тесты-замок)
- [ ] **Step 1: Write tests**
```js
describe('git push main-guard (owner-authorized 2026-06-02)', () => {
const na = { approvedGitOps: [], now: 0 };
it('allows push to a feature branch', () => {
expect(cgc2('git push origin worktree-lead-region-tails', na).result).toBe('allow');
expect(cgc2('git push', na).result).toBe('allow');
expect(cgc2('git push -u origin feature-x', na).result).toBe('allow');
});
it('blocks push to main/master', () => {
expect(cgc2('git push origin main', na).result).toBe('block');
expect(cgc2('git push origin HEAD:main', na).result).toBe('block');
expect(cgc2('git push origin master', na).result).toBe('block');
});
it('blocks force-push (hard pattern, unchanged)', () => {
expect(cgc2('git push --force origin feature-x', na).result).toBe('block');
expect(cgc2('git push origin feature-x --force-with-lease', na).result).toBe('block');
});
});
```
- [ ] **Step 2: Run to verify PASS** (логика из Task 3 уже на месте)
Run: `npx vitest run --root app --config vitest.config.tools.mjs shell-content-rules.test`
Expected: PASS.
---
### Task 5: Полная регрессия + коммит в фичевую ветку + PR
- [ ] **Step 1: Полная регрессия tools**
Run: `npx vitest run --root app --config vitest.config.tools.mjs`
Expected: всё GREEN (baseline ~1989 + новые). 0 падений.
- [ ] **Step 2: Дымовая проверка живьём** — после правок гейт читается заново; проверить, что
ранее блокированное теперь проходит (а опасное — нет). Прогнать через Bash:
```
composer --version
```
Expected: проходит (раньше любой `composer install` блокировался; `--version` и так был ок — проверка, что не сломали). Затем убедиться, что `git worktree list` (readonly) и `git status` работают.
- [ ] **Step 3: Создать фичевую ветку + worktree (теперь разрешено) и закоммитить**
```bash
git worktree add "../worktree-gate-rescope" -b feat/gate-dev-prod-rescope origin/main
```
(или коммит в основной копии на новой ветке — на усмотрение исполнителя; main НЕ трогать)
```bash
git add tools/enforce-router-gate.mjs tools/shell-content-rules.mjs \
tools/enforce-router-gate.test.mjs tools/shell-content-rules.test.mjs \
docs/superpowers/specs/2026-06-02-router-gate-dev-prod-rescope-design.md \
docs/superpowers/plans/2026-06-02-router-gate-dev-prod-rescope.md
git commit -m "feat(gate): re-scope router-gate — allow local dev (composer/npm/git/worktree), keep prod+discipline blocks"
git push origin feat/gate-dev-prod-rescope
```
- [ ] **Step 4: Открыть PR (клик владельца)** — дать владельцу ссылку из вывода `git push`; слияние в main — его клик.
---
## Self-Review
- **Spec coverage:** composer (Task 1) ✓ / npm (Task 2) ✓ / git dev-subs + worktree (Task 3) ✓ /
push main-guard (Task 4) ✓ / discipline+prod untouched (явно не трогаем в Task 1-4) ✓ /
«main = owner» (push-guard + PR в Task 5) ✓.
- **Placeholders:** нет — весь код приведён дословно.
- **Type/имена:** `GIT_DEV_SUB` / `GIT_CONDITIONAL_SUB` согласованы Task 3↔4; `classifyGitCommand`,
`matchBashHardBlacklist`, `classifyBashCommand` — реальные экспортируемые имена (проверено по коду).
- **Bootstrap:** коммит батчем в Task 5 (git разрешается только после применения Task 3) — учтено.
@@ -0,0 +1,131 @@
# Router-gate re-scope: «боевое блокируем, локальную разработку разрешаем»
**Дата:** 2026-06-02
**Статус:** design (утверждён владельцем; реализация — отдельным планом)
**Автор контекста:** сессия lead-region-tails
## Проблема
Router-gate v4 (`tools/enforce-router-gate.mjs`) работает в режиме «по умолчанию запрещено»
(whitelist для Bash + hard-blacklist + MCP-классификатор + дисциплинарные хуки). Он задумывался
как защита **боевого** контура (выкат на liderra.ru, изменение боевой БД, секреты, запуск
воркфлоу), но по факту блокирует и **весь локальный инструмент разработки**: `composer install`,
`npm install`, `git worktree`, `git commit`/`push`, и даже правку тест-файлов (через
`enforce-tdd-real-test-verifier`). Это делает обычную разработку через контроллера непрактичной —
любая PHP/JS-задача с тестами упирается в стену (подтверждено в сессии 2026-06-02: попытка сделать
fix реестра Россвязи провалилась на цепочке взаимно-охраняющих замков).
## Цель
Перенастроить замок так, чтобы он блокировал **только боевое и опасное**, а **локальную
разработку разрешал** — сохранив при этом дисциплину работы контроллера и защиту боевого контура.
## Решения (утверждены владельцем 2026-06-02)
1. **Дисциплину оставляем.** Хуки качества (TDD-gate, tdd-real-test-verifier, chain-recommendation,
graph-first, override-limit, llm-judge, coverage-verify, memory-coverage и пр.) — **не трогаем**.
Контроллер продолжает писать тесты до кода и не срезать углы.
2. **Защиту боевого оставляем железно.** Выкат/боевая БД/секреты/запуск воркфлоу/защищённые
пути — без изменений.
3. **Инструменты разработки разрешаем.** composer/npm/pest/git/worktree.
4. **Граница git:** ветки — контроллер сам (commit/push в не-главную ветку + подготовка PR);
слияние в main, push в main, force-push, выкат — **клик владельца**.
## Подход
**Approach A (выбран):** точечно расширить whitelist дев-инструментами, сохранив философию
«по умолчанию запрещено». Правим **два файла**`tools/enforce-router-gate.mjs` (composer/npm) и
`tools/shell-content-rules.mjs` (git; там общий `classifyGitCommand`). MCP-классификатор
(`tools/mcp-tool-classifier.mjs`) и дисциплинарные хуки — без изменений.
Отвергнут **Approach B** (перевернуть в default-allow + blacklist опасного): любой пропуск в
перечне опасного = дыра; ломает безопасную философию default-deny.
## Матрица: что блокируем / что разрешаем
### Остаётся ЗАБЛОКИРОВАННЫМ
| Категория | Примеры | Где |
|---|---|---|
| Боевой контур | выкат на сайт, изменение боевой БД, секреты/`.env`, защищённые пути (CLAUDE.md, memory/, transcripts, `~/.claude/runtime`) | без изменений |
| GitHub на запись | `create_*`/`update_*`/`merge_*`/`push_files`/`actions_run_trigger` | MCP-классификатор без изменений (read-only, открытый 2026-06-02, остаётся) |
| Опасные команды | `rm`/`mv`/`cp`/`chmod`/`chown`, `curl -X POST/PUT/DELETE`, `wget`, `nc`/`ncat`/`socat`, `node -e` с `fs.*`, `eval`, `bash -c`/`sh -c`, `python -c`, redirects в protected | hard-blacklist без изменений |
| Дисциплина | TDD-gate, tdd-real-test-verifier, override-limit, chain-recommendation, graph-first, llm-judge, coverage | хуки без изменений |
| Главная ветка | `git push` в main, `git push --force`, слияние в main | новый «страж main» |
### Становится РАЗРЕШЁННЫМ (локальная разработка)
| Инструмент | Команды |
|---|---|
| Composer | `composer install`, `composer dump-autoload`, `composer require`, `composer update` |
| NPM | `npm install`, `npm ci`, `npm run <script>` |
| Тесты | `pest`, `vendor/bin/pest`, `php artisan test` (уже частично в whitelist) |
| Git (ветки) | `git commit`, `git add`, `git branch`, `git switch`/`checkout`, `git worktree`, `git stash`, `git push` **в не-главную ветку** |
## Изменения в коде (два файла)
Git-логика живёт не в самом router-gate, а в общем модуле `shell-content-rules.mjs`
(`classifyGitCommand`, используется и Bash-, и PowerShell-гейтом). Поэтому правок — два файла.
### `tools/enforce-router-gate.mjs` (composer / npm)
1. **Из hard-blacklist (`BASH_HARD_BLACKLIST`) убрать** строки про `composer install/update/require/remove`
и `npm install/i/update/remove/uninstall`. `yarn`/`pnpm` остаются заблокированными (проект на npm,
не нужны). Истинно-опасные fs/сеть/exec (`rm/mv/cp/chmod`, `curl POST`, `wget`, `nc`, `node -e fs`,
`eval`, `bash -c`, `python -c`, redirects) — **без изменений**.
2. **В whitelist (`SAFE_EXACT`) добавить:** `composer (install|update|require|remove|dump-autoload|dump)`,
`npm (install|i|ci)`, `npm run <script>` (любой скрипт). Существующие `composer show/outdated/test/...`
и `npm test/run test/run lint` — остаются.
### `tools/shell-content-rules.mjs` (git)
1. **Новый `GIT_DEV_SUB`** = `{add, commit, branch, switch, checkout, stash, worktree}` → в
`classifyGitCommand` после hard-pattern-проверки возвращать `allow`. Эти подкоманды **убрать** из
`GIT_CONDITIONAL_SUB`. (`worktree` сейчас падает в default-deny — попадёт в dev-allow.)
2. **`GIT_HARD_PATTERNS` не трогаем** — `--no-verify`, `git add -f`, `git -c`, force-push, `--output`/`-o`
и т.п. по-прежнему блокируются ПЕРВЫМИ, до dev-allow. То есть `git commit --no-verify` и `git add -f`
остаются заблокированы даже как «dev».
3. **Страж main для `push`** (`mainPushGuard`, чистая функция): `push` остаётся, но —
если в аргументах фигурирует `main`/`master` как ref (`git push origin main`, `HEAD:main`, `:main`)
**block** (клик владельца); force-push уже заблокирован `GIT_HARD_PATTERNS`. Иначе (`git push origin <feature>`,
bare `git push`) → allow. Допущение: bare `git push` считаем пушем не-главной ветки (контроллер по модели
всегда на не-главной ветке); пуш в main возможен только явным `origin main` → пойман.
4. **Conditional остаётся** для `merge, rebase, reset, cherry-pick, revert, pull, clean` (require approval) —
риск потери работы / слияние в main = клик владельца.
**Не меняем:** `tools/mcp-tool-classifier.mjs`, `tools/bash-tokenizer.mjs` (`isMutatingSegment` — чейн-правило
C13 «цепочка с мутацией → блок» сохраняется), любые `enforce-*` дисциплинарные хуки, `.claude/settings.json`.
## Тестирование (TDD)
Через `tools/enforce-router-gate.test.mjs` (vitest, работает в основной копии):
- `composer install` / `composer require x` → allow; `composer` (без подкоманды) → как раньше.
- `npm install` → allow; `npm run build` → allow.
- `git commit -m x` / `git worktree add ...` / `git push origin feature-x` → allow.
- `git push origin main` / `git push --force`**block** (страж main).
- Регресс: опасное по-прежнему блокируется — `rm -rf x`, `curl -X POST`, `node -e "...fs..."`,
`eval`, `python -c` → block.
- Полная регрессия tools-тестов (`npx vitest run --root app --config vitest.config.tools.mjs`).
## Граница реализации (bootstrap-нюанс)
Сам этот re-scope — bootstrap-исключение: его нельзя делать в worktree (worktree пока заблокирован).
Реализуется в основной копии (там активен живой замок и работает vitest). После правки замка
`git`/`worktree`/`composer` становятся разрешены — дальнейшие задачи (например, fix реестра)
пойдут уже по модели «ветка + PR».
## Остаточные риски (приняты)
- Разрешён `composer require`/`npm install` → теоретический supply-chain (установка пакета).
Принято: это собственный проект владельца; дисциплина и code-review остаются.
- `rm`/`mv`/`cp` остаются заблокированы — если реально мешают разработке, пересматриваем отдельно
(файловые правки покрываются инструментами Write/Edit).
- «Страж main» опирается на парсинг аргументов `git push`; экзотические формы (push по URL,
refspec-трюки) при сомнении → block (fail-safe в сторону защиты main).
## Что НЕ входит (YAGNI)
- Не инвертируем модель замка (default-deny остаётся).
- Не трогаем боевые воркфлоу, секреты, MCP-write.
- Не ослабляем дисциплину.
+2 -2
View File
@@ -72,8 +72,8 @@ describe('classifyPowerShellCommand', () => {
it('blocks reading a protected path', () => {
expect(classifyPowerShellCommand('Get-Content ~/.claude/settings.json', {}).result).toBe('block');
});
it('routes git through shared classifier (block unapproved commit)', () => {
expect(classifyPowerShellCommand('git commit -m "x"', { approvedGitOps: [], now }).result).toBe('block');
it('routes git through shared classifier (commit dev-allowed 2026-06-02 re-scope)', () => {
expect(classifyPowerShellCommand('git commit -m "x"', { approvedGitOps: [], now }).result).toBe('allow');
});
it('allows readonly git through PowerShell', () => {
expect(classifyPowerShellCommand('git status', {}).result).toBe('allow');
+21 -3
View File
@@ -56,8 +56,8 @@ export const BASH_HARD_BLACKLIST = [
{ re: /\bpython3?\s+-c\b/, reason: 'python -c запрещён' },
{ re: /\b(?:bash|sh)\s+-c\b/, reason: 'bash/sh -c запрещён' },
{ re: /(^|\s|;|&&|\|\|)eval\b/, reason: 'eval запрещён' },
{ re: /\bcomposer\s+(?:install|update|require|remove)\b/, reason: 'composer install/update/require/remove запрещён' },
{ re: /\bnpm\s+(?:install|i|update|remove|uninstall)\b/, reason: 'npm install/update/remove запрещён' },
// composer/npm перенесены в whitelist (dev-allow, 2026-06-02 re-scope) — это локальные
// инструменты разработки, не боевой контур. yarn/pnpm остаются заблокированы (проект на npm).
{ re: /\b(?:yarn|pnpm)\s+(?:add|install|remove)\b/, reason: 'yarn/pnpm add/install/remove запрещён' },
{ re: /\bnpx\s+claude-/, reason: 'npx claude-* запрещён' },
{ re: /\bcurl\b[^|;]*-X\s*(?:POST|PUT|DELETE|PATCH)\b/i, reason: 'curl -X POST/PUT/DELETE/PATCH запрещён' },
@@ -120,8 +120,10 @@ const READING_CMDS = new Set(['ls', 'pwd', 'wc', 'head', 'tail', 'file', 'stat',
const SAFE_EXACT = [
/^npx\s+vitest\s+(?:run|--version)\b/,
/^npm\s+(?:test|run\s+test|run\s+lint(?::[\w-]+)?)\b/,
/^npm\s+(?:install|i|ci)\b/, // dev-allow 2026-06-02 re-scope
/^npm\s+run\s+[\w:-]+/, // dev-allow 2026-06-02 re-scope (любой npm-скрипт)
/^php\s+artisan\s+(?:list|route:list|migrate:status)\b/,
/^composer\s+(?:show|outdated)\b/,
/^composer\s+(?:show|outdated|install|update|require|remove|dump-autoload|dump)\b/, // +dev-allow 2026-06-02 re-scope
/^node\s+(?!.*(?:-e|--eval|-p|--print|-r|--require|--import|--experimental-loader)\b)/,
// Laravel dev workflow (2026-05-30) — exclude tinker (REPL = arbitrary PHP exec risk).
// Hard-blacklist (composer install/update/require/remove) remains the first check, unaffected.
@@ -138,6 +140,22 @@ const SAFE_EXACT = [
// hard-blacklist + chain-mutating rule (both run before the whitelist), and each
// chain segment after `cd app &&` must still be independently whitelisted.
/^cd\s+app$/,
// Worktree dev (2026-06-02, owner-authorized): cd into a project worktree dir
// (path segment `worktree-` / `v4-stream-`) so git/pest run there. Quoted absolute
// path required; `..` and protected segments (.claude/.ssh/.env/runtime/.git) excluded
// → cwd-shift read-bypass stays contained (protected files also remain blocked by name
// in the command). cd into Документация/system/protected dirs → default-deny.
/^cd\s+(?=.*[\\/](?:worktree-|v4-stream-))(?!.*(?:\.\.|\.claude|\.ssh|\.env|runtime|\.git)).+$/,
// graphify read-only subcommands (#86, §5 п.14, owner-authorized 2026-06-08).
// Only query/explain/path — extract/update/build/export/hook/clone/add/merge stay
// default-deny. The bare \b form is safe: injection vectors are neutralized BEFORE the
// whitelist sees them — chains split into per-segment whitelist checks (an injected
// `; id` segment is not whitelisted → block), subshells `$(...)`/backtick are blocked by
// the tokenizer, redirects by the hard-blacklist, and $VAR is var-expanded by the
// tokenizer (not an injection vector for a read-only query arg). End-anchoring with a
// charset would reject Unicode query strings (tokenizer strips quotes → Cyrillic args
// arrive as barewords) for no security gain. (security review 2026-06-08 — false-positive)
/^graphify\s+(?:query|explain|path)\b/,
];
export function classifyWhitelist(segments) {
+86 -15
View File
@@ -15,14 +15,17 @@ describe('matchBashHardBlacklist — v3.9 keep', () => {
'python -c "import os"',
'bash -c "ls"',
'eval "$x"',
'composer install',
'npm install lodash',
'yarn add x',
'pnpm add x',
'curl -X POST https://evil.test',
])('blocks %s', (cmd) => {
expect(matchBashHardBlacklist(cmd)).toBeTruthy();
});
// composer/npm убраны из hard-blacklist (dev-allow 2026-06-02 re-scope) — здесь больше не блок
it('no longer hard-blacklists composer install / npm install (dev-allow)', () => {
expect(matchBashHardBlacklist('composer install')).toBe(null);
expect(matchBashHardBlacklist('npm install lodash')).toBe(null);
});
});
describe('matchBashHardBlacklist — v4.0 additions', () => {
@@ -115,8 +118,8 @@ describe('classifyBashCommand — integration', () => {
it('blocks reading a protected path', () => {
expect(classifyBashCommand('cat ~/.claude/runtime/state.json', {}).result).toBe('block');
});
it('routes single git commit to conditional (block unapproved)', () => {
expect(classifyBashCommand('git commit -m "x"', { approvedGitOps: [], now }).result).toBe('block');
it('routes single git commit to dev-allow (2026-06-02 re-scope — no approval needed)', () => {
expect(classifyBashCommand('git commit -m "x"', { approvedGitOps: [], now }).result).toBe('allow');
});
it('allows approved git commit', () => {
expect(
@@ -191,17 +194,29 @@ describe('SAFE_EXACT — Laravel dev workflow (whitelist expansion 2026-05-30)',
expect(classifyBashCommand(cmd, {}).result).toBe('allow');
});
// Critical: REPL and composer mutations remain hard-blocked
it.each([
['php artisan tinker', 'REPL = arbitrary PHP exec risk'],
['php artisan tinker --execute="exit"', 'tinker variant'],
['composer install', 'hard-blacklist'],
['composer require foo/bar', 'hard-blacklist'],
['composer update', 'hard-blacklist'],
['composer remove foo/bar', 'hard-blacklist'],
['php artisan migrate:install', 'unknown migrate subcommand outside whitelist set'],
])('still blocks %s (%s)', (cmd) => {
expect(classifyBashCommand(cmd, {}).result).toBe('block');
// Critical: REPL remains hard-blocked (composer/npm moved to dev-allow below, 2026-06-02 re-scope)
it('still blocks tinker REPL and unknown migrate subcommand', () => {
expect(classifyBashCommand('php artisan tinker', {}).result).toBe('block');
expect(classifyBashCommand('php artisan tinker --execute="exit"', {}).result).toBe('block');
expect(classifyBashCommand('php artisan migrate:install', {}).result).toBe('block');
});
// dev-allow (owner-authorized 2026-06-02 re-scope): composer is a local dev tool
it('now allows composer install/require/update/remove/dump-autoload', () => {
expect(classifyBashCommand('composer install', {}).result).toBe('allow');
expect(classifyBashCommand('composer install -d app --no-interaction', {}).result).toBe('allow');
expect(classifyBashCommand('composer require monolog/monolog', {}).result).toBe('allow');
expect(classifyBashCommand('composer update', {}).result).toBe('allow');
expect(classifyBashCommand('composer remove monolog/monolog', {}).result).toBe('allow');
expect(classifyBashCommand('composer dump-autoload', {}).result).toBe('allow');
});
// dev-allow (owner-authorized 2026-06-02 re-scope): npm is a local dev tool
it('now allows npm install/i/ci/run', () => {
expect(classifyBashCommand('npm install', {}).result).toBe('allow');
expect(classifyBashCommand('npm i', {}).result).toBe('allow');
expect(classifyBashCommand('npm ci', {}).result).toBe('allow');
expect(classifyBashCommand('npm run build', {}).result).toBe('allow');
});
// Critical: existing pre-existing v3.8 keep behaviour
@@ -226,6 +241,39 @@ describe('SAFE_EXACT — Laravel dev workflow (whitelist expansion 2026-05-30)',
expect(classifyBashCommand('composer show', {}).result).toBe('allow');
expect(classifyBashCommand('composer outdated', {}).result).toBe('allow');
});
// graphify read-only subcommands (owner-authorized 2026-06-08 — #86 graphify, §5 п.14)
it('allows graphify read-only subcommands (query/explain/path)', () => {
expect(classifyBashCommand('graphify query "x"', {}).result).toBe('allow');
expect(classifyBashCommand('graphify explain "Node"', {}).result).toBe('allow');
expect(classifyBashCommand('graphify path "A" "B"', {}).result).toBe('allow');
});
// graphify mutating/expensive subcommands stay default-deny
it('still blocks graphify mutating subcommands (extract/export/hook)', () => {
expect(classifyBashCommand('graphify extract .', {}).result).toBe('block');
expect(classifyBashCommand('graphify export html', {}).result).toBe('block');
expect(classifyBashCommand('graphify hook install', {}).result).toBe('block');
});
// graphify allowlist is not bypassable via chained commands / subshells — they are
// caught by the gate architecture BEFORE the whitelist regex (per-segment whitelist +
// tokenizer subshell-block + redirect hard-blacklist), so the simple subcommand
// allowlist is safe (security review 2026-06-08 finding = false-positive: $VAR is
// var-expanded away by the tokenizer, not a command-injection vector).
it('blocks graphify chained commands and subshell payloads', () => {
expect(classifyBashCommand('graphify query x; id', {}).result).toBe('block');
expect(classifyBashCommand('graphify query x && rm y', {}).result).toBe('block');
expect(classifyBashCommand('graphify path A `id`', {}).result).toBe('block');
expect(classifyBashCommand('graphify query x | sh', {}).result).toBe('block');
});
// legit read-only graphify with quoted (Cyrillic) question + flags still allowed —
// guards against over-tightening that would reject Unicode queries (tokenizer strips
// quotes → Cyrillic args arrive as barewords).
it('still allows graphify query with quoted question and flags', () => {
expect(classifyBashCommand('graphify query "конфликт дубль" --dfs --budget 1500', {}).result).toBe('allow');
});
});
describe('SAFE_EXACT — narrow `cd app` whitelist (2026-05-31, owner-authorized)', () => {
@@ -271,6 +319,29 @@ describe('SAFE_EXACT — narrow `cd app` whitelist (2026-05-31, owner-authorized
});
});
describe('SAFE_EXACT — worktree cd (2026-06-02, owner-authorized worktree dev)', () => {
// Allowed: enter a project worktree dir (segment `worktree-` / `v4-stream-`) so
// git/pest can run there. Quoted absolute path; cwd-shift read-bypass stays contained
// because protected files remain blocked by name in the command (cat .env / runtime).
it.each([
'cd "C:\\моя\\проекты\\портал crm\\worktree-deals-city"',
'cd "C:\\моя\\проекты\\портал crm\\worktree-deals-city\\app"',
'cd "C:\\моя\\проекты\\портал crm\\v4-stream-A"',
])('allows cd into a worktree dir: %s', (cmd) => {
expect(classifyBashCommand(cmd, {}).result).toBe('allow');
});
// Scope: protected / non-worktree dirs stay default-deny (no `worktree-` marker, or
// `..` / protected segment present → cwd-shift read-bypass prevented).
it.each([
'cd "C:\\Users\\Administrator\\.claude\\runtime"',
'cd "C:\\моя\\проекты\\портал crm\\worktree-x\\..\\..\\.claude"',
'cd "C:\\моя\\проекты\\портал crm\\Документация"',
])('still blocks cd into non-worktree / protected dir: %s', (cmd) => {
expect(classifyBashCommand(cmd, {}).result).toBe('block');
});
});
import { stripQuotedSpans } from './enforce-router-gate.mjs';
describe('quote-aware redirect (quirk 2)', () => {
+5
View File
@@ -108,6 +108,11 @@ function hasFailingTestRun(turn) {
// Numeric: "Tests N failed | M passed" with N>0
const m = txt.match(/Tests\s+(\d+)\s+failed/);
if (m && Number(m[1]) > 0) return true;
// JSON reporter (composer test / php artisan test → pest): {"result":"failed",...}
// or {"failed":N}/{"errors":N} with N>0. command-not-found / error REDs lack the
// English "Failed" keyword above, so recognise the structured marker too.
if (/"result"\s*:\s*"failed"/.test(txt)) return true;
if (/"(?:failed|errors)"\s*:\s*[1-9]/.test(txt)) return true;
}
}
}
+22
View File
@@ -168,3 +168,25 @@ describe('enforce-tdd-gate / decide', () => {
expect(r.block).toBe(false);
});
});
describe('enforce-tdd-gate / decide — JSON pest reporter RED (composer test)', () => {
// `composer test` (php artisan test) emits machine JSON like {"result":"failed",...}.
// command-not-found / error REDs lack the English "Failed" keyword, so the gate must
// recognise the structured failure marker, else legit RED runs go unseen.
it('recognizes {"result":"failed"} JSON output as a RED run', () => {
const r = decide({
toolName: 'Write',
filePath: 'wt/app/app/Console/Commands/FooCommand.php',
transcriptEntries: [
userMsg('add backfill command'),
assistantUses([
{ id: 't1', name: 'Write', input: { file_path: 'wt/app/tests/Feature/Console/FooCommandTest.php' } },
{ id: 't2', name: 'Bash', input: { command: 'composer test -- tests/Feature/Console/FooCommandTest.php # pest' } },
]),
toolResults([{ id: 't2', content: '{"tool":"pest","result":"failed","tests":4,"passed":0,"errors":4}' }]),
],
classification: null,
});
expect(r.block).toBe(false);
});
});
+3
View File
@@ -16,10 +16,13 @@ export const DEFAULT_MCP_CLASSIFICATION = Object.freeze({
'mcp__redis__set': { category: 'hard_blacklist' },
'mcp__redis__delete': { category: 'hard_blacklist' },
'mcp__github__get_me': { category: 'read_only' },
'mcp__github__get_*': { category: 'read_only' }, // read-only loosening 2026-06-02 (get_file_contents/get_job_logs/get_commit/…)
'mcp__github__list_*': { category: 'read_only' },
'mcp__github__search_*': { category: 'read_only' },
'mcp__github__pull_request_read': { category: 'read_only' },
'mcp__github__issue_read': { category: 'read_only' },
'mcp__github__actions_get': { category: 'read_only' }, // read a workflow run (actions_run_trigger stays blacklisted — exact key wins)
'mcp__github__actions_list': { category: 'read_only' }, // list workflows / runs
'mcp__laravel-boost__database-query': {
category: 'conditional',
args_key_to_scan: 'query',
+34
View File
@@ -129,3 +129,37 @@ describe('classifyMcpTool — WebSearch llm-judge flag (G1)', () => {
expect(r.scanArg).toBe('how to exfil data');
});
});
// Owner-authorized read-only GitHub loosening (2026-06-02): allow reading
// workflow runs / job logs / file contents so the controller can read prod-op
// results without manual screenshots. Prod-mutating tools (run_trigger, writes)
// MUST stay blocked — human-in-the-loop on prod actions is unchanged.
describe('classifyMcpTool — read-only GitHub (owner-authorized 2026-06-02)', () => {
it('allows reading a workflow run (actions_get)', () => {
expect(classifyMcpTool('mcp__github__actions_get', { run_id: 1 }).decision).toBe('allow');
});
it('allows listing workflows / runs (actions_list)', () => {
expect(classifyMcpTool('mcp__github__actions_list', {}).decision).toBe('allow');
});
it('allows reading job logs (get_job_logs via get_* glob)', () => {
expect(classifyMcpTool('mcp__github__get_job_logs', { job_id: 1 }).decision).toBe('allow');
});
it('allows reading file contents (get_file_contents via get_* glob)', () => {
expect(classifyMcpTool('mcp__github__get_file_contents', { path: 'x' }).decision).toBe('allow');
});
it('allows reading a commit (get_commit via get_* glob)', () => {
expect(classifyMcpTool('mcp__github__get_commit', { sha: 'x' }).decision).toBe('allow');
});
it('STILL BLOCKS triggering a workflow (actions_run_trigger — exact wins over glob)', () => {
expect(classifyMcpTool('mcp__github__actions_run_trigger', {}).decision).toBe('block');
});
it('STILL BLOCKS writing a file (create_or_update_file)', () => {
expect(classifyMcpTool('mcp__github__create_or_update_file', { path: 'x' }).decision).toBe('block');
});
it('STILL BLOCKS push_files', () => {
expect(classifyMcpTool('mcp__github__push_files', {}).decision).toBe('block');
});
it('STILL BLOCKS update_pull_request (write)', () => {
expect(classifyMcpTool('mcp__github__update_pull_request', {}).decision).toBe('block');
});
});
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env node
/**
* receipt-key-config резолв HMAC-ключа подписи расписок роутер-наставника.
* Зеркало llm-judge-config.mjs: keychain (service router-mentor-receipts) env null.
* Ключ провижинит владелец (как LLM-ключ судьи). Без ключа null подпись невозможна
* неподписанная расписка невалидна (fail-closed на стороне verifyReceipt).
*/
import { createRequire } from 'node:module';
const KEY_ENV = 'ROUTER_MENTOR_RECEIPT_KEY';
const KEYCHAIN_SERVICE = 'router-mentor-receipts';
const KEYCHAIN_ACCOUNT = 'default';
/**
* @param {object} [args]
* @param {object} [args.env] - окружение (default process.env)
* @param {Function} [args.keychainGet] - () => string|null (инъекция для тестов)
* @returns {string|null}
*/
export function resolveReceiptKey({ env = process.env, keychainGet = defaultKeychainGet } = {}) {
let chainKey = null;
try {
const v = keychainGet();
chainKey = v ? String(v) : null;
} catch {
chainKey = null;
}
const envKey = env[KEY_ENV] ? String(env[KEY_ENV]) : null;
return chainKey || envKey || null;
}
/** Lazy keytar-ридер; null если keytar отсутствует или записи нет. Никогда не бросает. */
export function defaultKeychainGet() {
try {
const require = createRequire(import.meta.url);
const keytar = require('keytar');
return (keytar.getPasswordSync?.(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT)) || null;
} catch {
return null;
}
}
export const _internals = { KEY_ENV, KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT };
+25
View File
@@ -0,0 +1,25 @@
// tools/receipt-key-config.test.mjs
import { describe, it, expect } from 'vitest';
import { resolveReceiptKey } from './receipt-key-config.mjs';
describe('resolveReceiptKey', () => {
it('returns keychain key when present', () => {
const r = resolveReceiptKey({ env: {}, keychainGet: () => 'k-from-chain' });
expect(r).toBe('k-from-chain');
});
it('falls back to env ROUTER_MENTOR_RECEIPT_KEY when keychain empty', () => {
const r = resolveReceiptKey({ env: { ROUTER_MENTOR_RECEIPT_KEY: 'k-env' }, keychainGet: () => null });
expect(r).toBe('k-env');
});
it('prefers keychain over env', () => {
const r = resolveReceiptKey({ env: { ROUTER_MENTOR_RECEIPT_KEY: 'k-env' }, keychainGet: () => 'k-chain' });
expect(r).toBe('k-chain');
});
it('returns null when neither present', () => {
expect(resolveReceiptKey({ env: {}, keychainGet: () => null })).toBe(null);
});
it('never throws when keychainGet throws → null', () => {
const r = resolveReceiptKey({ env: {}, keychainGet: () => { throw new Error('boom'); } });
expect(r).toBe(null);
});
});
+30 -5
View File
@@ -164,9 +164,13 @@ const GIT_READONLY_SUB = new Set([
'rev-parse', 'merge-base', 'remote', 'stash', // stash list/show resolved below
'fetch', 'ls-remote', // ref-only, no working-tree mutation — Stream H pre-flight requires §15.2 sync
]);
// dev-safe (owner-authorized 2026-06-02 re-scope): allow без approval. GIT_HARD_PATTERNS
// (--no-verify / add -f / -c / force / --output / -o) пре-фильтруют опасные варианты ВЫШЕ.
const GIT_DEV_SUB = new Set([
'add', 'commit', 'branch', 'switch', 'checkout', 'stash', 'worktree',
]);
const GIT_CONDITIONAL_SUB = new Set([
'add', 'commit', 'merge', 'rebase', 'reset', 'checkout', 'switch',
'branch', 'stash', 'cherry-pick', 'revert', 'pull', 'push', 'clean',
'merge', 'rebase', 'reset', 'cherry-pick', 'revert', 'pull', 'clean',
]);
// G5/G6 + force-push + add -f → always block (даже если "approved").
@@ -183,14 +187,23 @@ const GIT_HARD_PATTERNS = [
];
function gitSubcommand(command) {
const m = normalizeCommand(command).match(/\bgit\s+(?:-c\s+\S+\s+)*([a-z][\w-]*)/);
// Skip leading global flags `-c <val>` and `-C <path>`. `git -C <dir> <sub>` is the
// cwd-independent way to operate on a worktree (the shell resets cwd each call), so the
// real subcommand must be found after `-C`. `-C` (uppercase, working-dir) is case-distinct
// from the blocked `-c` config-injection (GIT_HARD_PATTERNS still scans the full command).
const m = normalizeCommand(command).match(
/\bgit\s+(?:(?:-c\s+\S+|-C\s+(?:"[^"]*"|'[^']*'|\S+))\s+)*([a-z][\w-]*)/,
);
return m ? m[1] : null;
}
export function classifyGitCommand(command, ctx = {}) {
const norm = normalizeCommand(command);
// Strip a leading `git -C <path>` (worktree-dir flag) so every rule below sees the real
// subcommand+flags. Without this, position-anchored hard-patterns (--no-verify / --force /
// add -f) and the push-main-guard would be bypassed by interposing `-C <dir>`.
const norm = normalizeCommand(command).replace(/(\bgit)\s+-C\s+(?:"[^"]*"|'[^']*'|\S+)\s+/, '$1 ');
if (!/\bgit\b/.test(norm)) return null;
const sub = gitSubcommand(command);
const sub = gitSubcommand(norm);
if (!sub) return null;
// 1. git-hard — block безусловно
@@ -212,6 +225,18 @@ export function classifyGitCommand(command, ctx = {}) {
return { result: 'block', reason: 'git remote (мутация) требует AskUser approval' };
}
// dev-safe git (owner-authorized 2026-06-02 re-scope): GIT_HARD_PATTERNS уже отсеяли
// опасные варианты (--no-verify / add -f / -c / force / --output / -o) на шаге 1.
if (GIT_DEV_SUB.has(sub)) return { result: 'allow', reason: `dev-safe git ${sub}` };
// push: фичевые ветки — allow; main/master — клик владельца (force уже заблокирован hard).
if (sub === 'push') {
if (/\b(?:main|master)\b/.test(norm)) {
return { result: 'block', reason: 'git push в main/master — клик владельца' };
}
return { result: 'allow', reason: 'git push в фичевую ветку' };
}
// 3. conditional → approve check
if (GIT_CONDITIONAL_SUB.has(sub)) {
const approved = isApproved(command, ctx.approvedGitOps, ctx.now ?? Date.now());
+66 -25
View File
@@ -167,40 +167,81 @@ describe('classifyGitCommand — readonly', () => {
);
});
describe('classifyGitCommand — conditional after approve', () => {
describe('classifyGitCommand — conditional (still needs approval after 2026-06-02 re-scope)', () => {
const now = 2_000_000;
it('blocks unapproved git commit', () => {
const r = classifyGitCommand('git commit -m "x"', { approvedGitOps: [], now });
expect(r.result).toBe('block');
expect(r.reason).toMatch(/approve/i);
});
it('allows approved git commit', () => {
const r = classifyGitCommand('git commit -m "x"', {
approvedGitOps: [{ command: 'git commit -m "x"', ts: now }],
now,
});
expect(r.result).toBe('allow');
});
it.each(['git rebase main', 'git reset --hard', 'git switch main', 'git stash pop', 'git push origin feat'])(
'blocks unapproved %s',
(cmd) => {
it('blocks unapproved rebase/reset/merge/cherry-pick/revert/pull/clean', () => {
for (const cmd of ['git rebase main', 'git reset --hard', 'git merge feat',
'git cherry-pick abc', 'git revert abc', 'git pull', 'git clean -fd']) {
expect(classifyGitCommand(cmd, { approvedGitOps: [], now }).result).toBe('block');
},
);
it('blocks unapproved git add (v4 Stream G addition)', () => {
const r = classifyGitCommand('git add .claude/settings.json', { approvedGitOps: [], now });
expect(r.result).toBe('block');
expect(r.reason).toMatch(/approve/i);
}
});
it('allows approved git add', () => {
const r = classifyGitCommand('git add .claude/settings.json', {
approvedGitOps: [{ command: 'git add .claude/settings.json', ts: now }],
it('allows approved git merge', () => {
const r = classifyGitCommand('git merge feat', {
approvedGitOps: [{ command: 'git merge feat', ts: now }],
now,
});
expect(r.result).toBe('allow');
});
});
describe('classifyGitCommand — dev-allow (owner-authorized 2026-06-02 re-scope)', () => {
const na = { approvedGitOps: [], now: 2_000_000 };
it('allows commit/add/branch/switch/checkout/stash/worktree without approval', () => {
for (const cmd of [
'git commit -m "x"', 'git add .', 'git branch feature-x',
'git switch -c feature-x', 'git switch feature-x', 'git checkout -b feature-x',
'git stash push -m wip', 'git stash pop',
'git worktree add ../wt -b feat origin/main',
]) {
expect(classifyGitCommand(cmd, na).result).toBe('allow');
}
});
it('still blocks commit --no-verify and add -f (hard patterns survive dev-allow)', () => {
expect(classifyGitCommand('git commit --no-verify -m x', na).result).toBe('block');
expect(classifyGitCommand('git add -f ignored.txt', na).result).toBe('block');
});
});
describe('classifyGitCommand — push main-guard (owner-authorized 2026-06-02 re-scope)', () => {
const na = { approvedGitOps: [], now: 2_000_000 };
it('allows push to a feature branch / bare push', () => {
expect(classifyGitCommand('git push origin worktree-lead-region-tails', na).result).toBe('allow');
expect(classifyGitCommand('git push', na).result).toBe('allow');
expect(classifyGitCommand('git push -u origin feature-x', na).result).toBe('allow');
});
it('blocks push to main/master (owner click)', () => {
expect(classifyGitCommand('git push origin main', na).result).toBe('block');
expect(classifyGitCommand('git push origin HEAD:main', na).result).toBe('block');
expect(classifyGitCommand('git push origin master', na).result).toBe('block');
});
it('blocks force-push (hard pattern unchanged)', () => {
expect(classifyGitCommand('git push --force origin feature-x', na).result).toBe('block');
expect(classifyGitCommand('git push origin feature-x --force-with-lease', na).result).toBe('block');
});
});
describe('classifyGitCommand — git -C <path> (worktree dev, 2026-06-02)', () => {
const na = { approvedGitOps: [], now: 4_000_000 };
// git -C points git at another working tree (cwd resets each shell call, so this is
// the cwd-independent way to commit in a worktree). Classify by the REAL subcommand
// after -C, with all hard-patterns / push-main-guard still applied to the full command.
it.each([
'git -C "C:\\моя\\проекты\\портал crm\\worktree-x" commit -m "y"',
'git -C "C:\\моя\\проекты\\портал crm\\worktree-x" add app/foo.php',
'git -C "/path/worktree-x" push origin feature-y',
'git -C /repo status',
])('classifies by real subcommand after -C: %s', (cmd) => {
expect(classifyGitCommand(cmd, na).result).toBe('allow');
});
it('still blocks push to main even with -C', () => {
expect(classifyGitCommand('git -C /repo push origin main', na).result).toBe('block');
});
it('still blocks --no-verify even with -C', () => {
expect(classifyGitCommand('git -C /repo commit --no-verify -m x', na).result).toBe('block');
});
});
describe('classifyGitCommand — git-hard (always block)', () => {
it.each([
'git push --force origin main',