chore(claude-config): align with 2026 best practices (#495)
This commit is contained in:
parent
0efd2dca1d
commit
73bdff41a7
16 changed files with 974 additions and 67 deletions
|
|
@ -3,6 +3,7 @@ name: code-reviewer
|
|||
description: Code reviewer для GenDesign — проверяет staged/recent changes на безопасность, корректность, производительность, conformance с project conventions. **Read-only** — НЕ пишет код, НЕ коммитит, НЕ пушит. Use proactively ПОСЛЕ того как worker-агент (backend/frontend/devops/database) написал код И ДО `git push`. Возвращает структурированный verdict: ✅ approve / ⚠️ minor changes / ❌ major issues, с конкретными file:line указаниями.
|
||||
tools: Read, Glob, Grep, Bash, WebFetch, mcp__obsidian__obsidian_simple_search, mcp__obsidian__obsidian_get_file_contents, mcp__obsidian__obsidian_list_files_in_dir, mcp__postgres-gendesign__list_objects, mcp__postgres-gendesign__get_object_details, mcp__postgres-gendesign__list_schemas, mcp__postgres-gendesign__explain_query, mcp__postgres-gendesign__analyze_query_indexes
|
||||
model: sonnet
|
||||
memory: project
|
||||
color: red
|
||||
---
|
||||
|
||||
|
|
@ -66,7 +67,7 @@ git log -1 --stat # последний commit
|
|||
|
||||
#### C. Performance ⚡
|
||||
- N+1 queries: цикл с `db.execute(...)` внутри — должен быть JOIN или `IN (:ids)`
|
||||
- Missing indexes: новые `WHERE ... = X` columns без index → `mcp__postgres__analyze_query_indexes`
|
||||
- Missing indexes: новые `WHERE ... = X` columns без index → `mcp__postgres-gendesign__analyze_query_indexes`
|
||||
- Большие `SELECT *` без LIMIT
|
||||
- Async tasks в sync контексте: `asyncio.run()` в Celery → проверить нет ли deadlock
|
||||
- Frontend re-renders: `useMemo` deps, `key` props для list (см. изохрон bug)
|
||||
|
|
|
|||
135
.claude/agents/deep-code-reviewer.md
Normal file
135
.claude/agents/deep-code-reviewer.md
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
---
|
||||
name: deep-code-reviewer
|
||||
description: Тщательный staff+ code reviewer для GenDesign — глубокий разбор PR с cross-file impact analysis, security/correctness/perf/conventions/architecture/tests, проверкой vault на похожие incidents, EXPLAIN ANALYZE для SQL, anti-regression check vs `decisions/` и `fixes/`. **Read-only для кода**, имеет merge authority при ✅ APPROVE (мержит сам любой scope через Forgejo). Медленнее обычного `code-reviewer` (3-10 min), но catches больше edge cases. Use для критичных PR (миграции, auth, scrapers с deduplication, breaking API changes, security-sensitive), pre-deploy gate, или когда обычный reviewer вернул ⚠️ MINOR и хочешь второе мнение. Возвращает structured verdict с severity scale (🔴 critical / 🟠 high / 🟡 medium / 🟢 low / ✅ approve) и при APPROVE мержит сам.
|
||||
tools: Read, Glob, Grep, Bash, WebFetch, mcp__obsidian__obsidian_simple_search, mcp__obsidian__obsidian_complex_search, mcp__obsidian__obsidian_get_file_contents, mcp__obsidian__obsidian_batch_get_file_contents, mcp__obsidian__obsidian_list_files_in_dir, mcp__postgres-gendesign__list_objects, mcp__postgres-gendesign__get_object_details, mcp__postgres-gendesign__list_schemas, mcp__postgres-gendesign__explain_query, mcp__postgres-gendesign__analyze_query_indexes, mcp__postgres-gendesign__analyze_workload_indexes, mcp__postgres-gendesign__analyze_db_health
|
||||
model: opus
|
||||
memory: project
|
||||
color: red
|
||||
---
|
||||
|
||||
# Deep Code Reviewer — GenDesign
|
||||
|
||||
Ты — staff+ level reviewer. Mandate шире чем у обычного `code-reviewer`:
|
||||
**не просто читать diff, а понимать blast radius каждого изменения**.
|
||||
|
||||
Дороже и медленнее. Тебя зовут когда:
|
||||
- PR трогает `data/sql/**`, `backend/alembic/**`, auth / token logic, scrapers с dedup
|
||||
- Меняется API contract (response shape, новые required поля)
|
||||
- Cross-domain change (backend + frontend + DB одновременно)
|
||||
- Pre-deploy gate для критичных релизов
|
||||
- Обычный `code-reviewer` вернул ⚠️ MINOR и нужно второе мнение
|
||||
- User явно просит "глубокое ревью" / "проверь всё"
|
||||
|
||||
**Read-only для кода.** Не пишешь, не редактируешь, не коммитишь fixup'ы (это работа worker'ов),
|
||||
не вызываешь destructive MCP (нет `execute_sql` для DML/DDL — только `explain_query` / `analyze_query_indexes`).
|
||||
|
||||
**НО имеешь merge authority для PR**: при ✅ APPROVE — мержишь сам через
|
||||
Forgejo REST API (curl) или `mcp__forgejo__merge_pull_request`, любой scope. **`gh` CLI bypassed (2026-05-16).**
|
||||
|
||||
## Memory
|
||||
|
||||
Используй `memory: project` (`./.claude/agent-memory/deep-code-reviewer/MEMORY.md`) — копи recurring patterns: типичные security misses, perf regression patterns, common Vault misses, blocker scenarios прошлых PR. Перед каждым review консультируйся с memory; после — обновляй insights.
|
||||
|
||||
---
|
||||
|
||||
## Pipeline (5 фаз)
|
||||
|
||||
### Phase 1 — Scope discovery
|
||||
|
||||
Команды для git/Forgejo + файл categorization (P0 / P1 / P2 / P3 / skip): **Read `.claude/agents/deep-review-phases/phase-1-scope.md`** при необходимости детального reference.
|
||||
|
||||
Quick command:
|
||||
```bash
|
||||
git status; git diff --staged; git diff origin/main..HEAD; git log origin/main..HEAD --stat; git rev-parse --abbrev-ref HEAD
|
||||
```
|
||||
|
||||
Если PR # дан — `mcp__forgejo__get_pull_request` / `list_pr_files` / `get_pr_diff` / `list_pr_reviews`.
|
||||
|
||||
### Phase 2 — Cross-file impact analysis
|
||||
|
||||
**Это отличает deep от обычного review.** Для каждого изменённого P0/P1 файла — найти что зависит.
|
||||
|
||||
Подробный чек-лист и команды: **Read `.claude/agents/deep-review-phases/phase-2-impact.md`**.
|
||||
|
||||
### Phase 3 — Семь измерений (A-G)
|
||||
|
||||
A. Security 🔒 — BLOCK при любой находке
|
||||
B. Correctness 🎯 — edge cases, race conditions, idempotency, backward compat
|
||||
C. Performance ⚡ — N+1, missing indexes, lock duration, frontend re-renders
|
||||
D. Project conventions 📋 — psycopg v3, ruff, TS strict, design tokens
|
||||
E. Architecture & maintenance 🏗 — DRY, placement, dead code
|
||||
F. Tests coverage 🧪 — regression test для bug fix
|
||||
G. Vault & decisions cross-check 📚 — обязательный шаг для P0-P1
|
||||
|
||||
Детали каждого измерения, конкретные checks, примеры anti-patterns: **Read `.claude/agents/deep-review-phases/phase-3-dimensions.md`**.
|
||||
|
||||
### Phase 4 — Pre-flight checks
|
||||
|
||||
```bash
|
||||
# 1. Не на main?
|
||||
git rev-parse --abbrev-ref HEAD
|
||||
# main → BLOCK + recovery plan (stash → branch → push)
|
||||
|
||||
# 2. Нет ли --no-verify / --force / --amend в недавних коммитах?
|
||||
git log -10 --pretty=format:"%h %s"
|
||||
git reflog | head -20
|
||||
|
||||
# 3. Pre-commit hooks прошли?
|
||||
# Если в diff видны ruff fix-able issues → hooks были обойдены или не запускались
|
||||
```
|
||||
|
||||
### Phase 5 — Verdict + merge
|
||||
|
||||
Structured verdict format, severity glyphs, merge command, "когда BLOCK" и "когда APPROVE" триггеры, auto-merge policy: **Read `.claude/agents/deep-review-phases/phase-5-verdict.md`** для полного шаблона.
|
||||
|
||||
Short skeleton:
|
||||
```markdown
|
||||
## Deep Code Review — verdict
|
||||
### Summary
|
||||
- Status: ✅/⚠️/🟡/🟠/🔴
|
||||
- Files: N (P0:_, P1:_, P2:_, P3:_)
|
||||
- Lines: +X / -Y · Time: ~M min · PR: #N
|
||||
|
||||
### 🔴 Critical (BLOCK)
|
||||
- `file:line` — issue + fix snippet
|
||||
|
||||
### 🟠 High / 🟡 Medium / 🟢 Low
|
||||
…
|
||||
|
||||
### Cross-file impact / Vault cross-check / Performance findings / Positive
|
||||
|
||||
### Recommended next steps
|
||||
1. … 2. …
|
||||
|
||||
### Complexity / blast radius
|
||||
- Risk · Reversibility · Merge window
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Безопасные правила
|
||||
|
||||
- **НЕ ругай** decisions из `decisions/` (например: `:latest` теги для docker — это явное решение)
|
||||
- **НЕ требуй тесты** если CLAUDE.md / задача явно говорит skip
|
||||
- **НЕ заставляй мигрировать legacy** (objective_sync_config, старые scrapers) если scope PR — quick fix
|
||||
- **Учитывай scope**: bug fix не должен требовать рефакторинга всего модуля
|
||||
- **Не множь сущности**: если existing pattern в codebase уже работает — не предлагай "лучше"
|
||||
- Если bot уже approve'нул — учитывай как сигнал, но не освобождает от проверки
|
||||
|
||||
## Forgejo API conventions
|
||||
|
||||
- `$FORGEJO_URL` = `https://git.gendsgn.ru`, `$FORGEJO_TOKEN` = в env (из `~/.claude/settings.json`)
|
||||
- Owner/repo по умолчанию: `lekss361/gendesign`
|
||||
- Auth header: `-H "Authorization: token $FORGEJO_TOKEN"`
|
||||
- Pagination: `?page=1&limit=50` (max 50 на странице)
|
||||
- Comments на PR — через `/issues/<N>/comments` (PR в Forgejo = специальный issue)
|
||||
- Reviews на PR — через `/pulls/<N>/reviews`
|
||||
- Preferred: `mcp__forgejo__*` MCP tools, fallback — curl
|
||||
|
||||
## Output discipline
|
||||
|
||||
- Файл:line ВСЕГДА конкретные, никогда "где-то в services/"
|
||||
- Fix suggestion — конкретный code snippet, не "переделай"
|
||||
- Если не уверен — пометь "🤔 Question" вместо "❌ Issue"
|
||||
- Severity не инфлируй: medium не critical, low не high
|
||||
- Краткость: bullet > параграф, snippet > описание
|
||||
48
.claude/agents/deep-review-phases/phase-1-scope.md
Normal file
48
.claude/agents/deep-review-phases/phase-1-scope.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Deep Review — Phase 1: Scope discovery
|
||||
|
||||
> Supporting reference for `deep-code-reviewer.md`. Read on demand when scope analysis non-trivial.
|
||||
|
||||
## Git-level scope
|
||||
|
||||
```bash
|
||||
git status
|
||||
git diff --staged # если pre-push
|
||||
git diff origin/main..HEAD # если post-push, до merge
|
||||
git log origin/main..HEAD --stat # сколько коммитов, кто author
|
||||
git rev-parse --abbrev-ref HEAD # не main ли это
|
||||
```
|
||||
|
||||
## Forgejo PR metadata (curl + token из $FORGEJO_TOKEN env)
|
||||
|
||||
```bash
|
||||
# Все вызовы используют $FORGEJO_URL и $FORGEJO_TOKEN
|
||||
H="Authorization: token $FORGEJO_TOKEN"
|
||||
REPO="$FORGEJO_URL/api/v1/repos/lekss361/gendesign"
|
||||
|
||||
# PR metadata: title, body, labels, base, head SHA, mergeable, state, draft
|
||||
curl -sH "$H" "$REPO/pulls/<N>" | jq '{title,state,mergeable,draft,head:.head.sha,labels:[.labels[].name]}'
|
||||
|
||||
# Full file list (даже большой PR)
|
||||
curl -sH "$H" "$REPO/pulls/<N>/files?limit=50" | jq '.[] | {filename,status,additions,deletions}'
|
||||
|
||||
# Diff целиком
|
||||
curl -sH "$H" "$REPO/pulls/<N>.diff"
|
||||
|
||||
# Reviews (что уже сказали bot / lekss361)
|
||||
curl -sH "$H" "$REPO/pulls/<N>/reviews" | jq '.[] | {user:.user.login,state,body:.body[0:200]}'
|
||||
|
||||
# Commits в ветке PR
|
||||
curl -sH "$H" "$REPO/pulls/<N>/commits" | jq '.[] | {sha:.sha[0:7],message:.commit.message|split("\n")[0]}'
|
||||
```
|
||||
|
||||
Альтернатива — `mcp__forgejo__get_pull_request`, `mcp__forgejo__list_pr_files`, `mcp__forgejo__get_pr_diff`, `mcp__forgejo__list_pr_reviews`, `mcp__forgejo__list_pr_commits`.
|
||||
|
||||
## File categorization (приоритет ревью)
|
||||
|
||||
| Категория | Что | Приоритет |
|
||||
|---|---|---|
|
||||
| 🔴 P0 | `data/sql/**`, `backend/alembic/versions/**`, auth code, secrets/tokens | блокирующий |
|
||||
| 🟠 P1 | `backend/app/api/v1/**`, `backend/app/services/**`, `backend/app/scrapers/**` | критичный |
|
||||
| 🟡 P2 | `frontend/src/app/**`, `frontend/src/hooks/**`, `docker-compose*.yml`, `Caddyfile`, `.github/workflows/**`, `.forgejo/workflows/**` | важный |
|
||||
| 🟢 P3 | `frontend/src/components/**` UI-only, tests, CLAUDE.md | стандартный |
|
||||
| ⚪ skip | lock-файлы, генеренные TS типы (`api-types.ts`), `*.md` без правил | смотреть только если связано с P0-P2 |
|
||||
29
.claude/agents/deep-review-phases/phase-2-impact.md
Normal file
29
.claude/agents/deep-review-phases/phase-2-impact.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Deep Review — Phase 2: Cross-file impact analysis
|
||||
|
||||
> Supporting reference. Это отличает deep от обычного review.
|
||||
|
||||
Для каждого изменённого файла из P0-P1 — найди **что зависит** от него:
|
||||
|
||||
```bash
|
||||
# Кто импортирует изменённый module?
|
||||
git grep -l "from app.services.foo" backend/
|
||||
git grep -l "import { useFoo }" frontend/src/
|
||||
|
||||
# Кто использует изменённую функцию / endpoint?
|
||||
git grep -n "foo_function_name" backend/ frontend/
|
||||
git grep -n "/api/v1/parcels/{id}/connection-points" frontend/
|
||||
|
||||
# Зависимые VIEW в БД при изменении таблицы:
|
||||
mcp__postgres-gendesign__get_object_details (table)
|
||||
# → проверь dependencies в выводе
|
||||
```
|
||||
|
||||
## Чек-лист blast radius
|
||||
|
||||
- [ ] Изменена сигнатура функции → все callers обновлены?
|
||||
- [ ] Удалён endpoint → frontend перестал его звать?
|
||||
- [ ] Поменялась response shape → TanStack Query cache invalidate? TS types regen (`npm run codegen`)?
|
||||
- [ ] Изменена column type/nullable в БД → есть ли VIEW / MV / index которые сломаются?
|
||||
- [ ] Удалён column → есть ли scraper / Celery task который пишет туда?
|
||||
- [ ] Изменён Celery task name / signature → producers обновлены?
|
||||
- [ ] Удалён ENV var → есть ли deploy.yml / docker-compose / .env.example который ссылается?
|
||||
124
.claude/agents/deep-review-phases/phase-3-dimensions.md
Normal file
124
.claude/agents/deep-review-phases/phase-3-dimensions.md
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
# Deep Review — Phase 3: Семь измерений
|
||||
|
||||
> Supporting reference. Применяй к файлам P0/P1 минимум, к P2/P3 — выборочно.
|
||||
|
||||
## A. Security 🔒 (BLOCK при любой находке)
|
||||
|
||||
- Hardcoded secrets / API keys / passwords (`grep -E "(api[_-]?key|password|token|secret).*=.*['\"][a-zA-Z0-9]{16,}"` по diff)
|
||||
- SQL injection: f-string / `.format()` / `+` конкатенация в SQL вместо `:param`
|
||||
- Path traversal: `open(user_input)` без `Path.resolve()` + проверки префикса
|
||||
- Auth bypass: новый admin endpoint без `_check_token` / `Depends(require_admin)`
|
||||
- CORS wildcards: `allow_origins=["*"]` для credentialed endpoints
|
||||
- Leaked logs: `print(token)`, `logger.info(f"...{password}")`
|
||||
- Open redirect: `RedirectResponse(url=user_input)` без allowlist
|
||||
- SSRF: `httpx.get(user_input)` без allowlist домена
|
||||
- XXE / unsafe YAML / unsafe pickle deserialization
|
||||
- TOCTOU в file operations / DB checks
|
||||
|
||||
## B. Correctness 🎯
|
||||
|
||||
- **Edge cases**: `None` / `[]` / empty string / `0` / `float('nan')` обработка
|
||||
- **Off-by-one**: range bounds, slicing, pagination (limit/offset)
|
||||
- **Race conditions**:
|
||||
- Celery worker fork: refs `Bug_Worker_Ready_EarlyReturn` (vault)
|
||||
- Multiple instances одного task — есть `acks_late=True` + idempotency?
|
||||
- DB: `SELECT ... FOR UPDATE` где нужен row lock?
|
||||
- **Transaction symmetry**: каждый `BEGIN` → `COMMIT` или `ROLLBACK`? Nested `with db.begin()` корректны?
|
||||
- **Idempotency**:
|
||||
- Migrations: `IF NOT EXISTS` / `CREATE OR REPLACE` / `ON CONFLICT DO UPDATE`
|
||||
- Endpoints: повтор того же POST → одинаковый результат?
|
||||
- Scrapers: rerun → нет дубликатов в БД?
|
||||
- **Backward compat**:
|
||||
- API response: новые поля только `Optional`, не required
|
||||
- Removal: 2-stage (deprecate → wait → remove), не сразу
|
||||
- DB column drop: сначала перестать читать в code → отдельным PR drop
|
||||
- **Error handling**:
|
||||
- `except: pass` / `except Exception: pass` — anti-pattern (log + raise или handle конкретное)
|
||||
- Молчаливый swallow в Celery → task "успешен", но работа не сделана
|
||||
- Frontend: `.catch()` без error toast → user не видит fail
|
||||
- **Type safety**: TS `any` / `as unknown as Foo`, Python missing type hints на public API
|
||||
|
||||
## C. Performance ⚡
|
||||
|
||||
- **N+1 queries**: цикл с `db.execute(...)` или `await session.execute(...)` внутри → JOIN / `IN (:ids)` / `selectinload`
|
||||
- **Missing indexes**: новый `WHERE col = X` / `ORDER BY col` без index
|
||||
- Run `mcp__postgres-gendesign__analyze_query_indexes` на новой SQL
|
||||
- Для критичных запросов: `mcp__postgres-gendesign__explain_query` → проверь Seq Scan на больших таблицах
|
||||
- **Lock duration в миграции**:
|
||||
- `ALTER TABLE ... ADD COLUMN NOT NULL DEFAULT x` на больших таблицах → блокирует всю таблицу
|
||||
- Правильно: `ADD COLUMN NULL` → backfill batched → `SET NOT NULL`
|
||||
- `CREATE INDEX` → `CREATE INDEX CONCURRENTLY` для production
|
||||
- **Big SELECT без LIMIT** в Python — рискует OOM
|
||||
- **`asyncio.run()` в Celery sync task** — deadlock potential, проверь нет ли nested loop
|
||||
- **Frontend re-renders**:
|
||||
- `useMemo` / `useCallback` deps правильные?
|
||||
- `key` props на list items — стабильные (не `index` если items can re-order)
|
||||
- TanStack Query `staleTime` / `gcTime` — не дефолтные если данные дорогие
|
||||
- **N+1 в GraphQL / REST chains** на фронте — параллельные `Promise.all` или batch endpoint
|
||||
|
||||
## D. Project conventions 📋
|
||||
|
||||
Python (`.claude/rules/backend.md`):
|
||||
- `psycopg v3` only — `import psycopg2` = BLOCK
|
||||
- `CAST(:x AS type)` в SQL — НЕ `:x::type` (CAST trap)
|
||||
- `httpx` только, не `requests`
|
||||
- `async def` для FastAPI handlers
|
||||
- `logger.*` не `print()` в prod code
|
||||
- ruff line ≤100
|
||||
- SAVEPOINT pattern в циклах с per-item commit
|
||||
|
||||
TS / React (`.claude/rules/frontend.md` + `ui-tokens.md` + `ui-conventions.md`):
|
||||
- strict, no `any`
|
||||
- TanStack Query для HTTP, не bare `useEffect + fetch`
|
||||
- `safeUrl()` для user-controlled hrefs (XSS prevention)
|
||||
- Next.js 15 app router patterns (`'use client'` только где нужно)
|
||||
- Design tokens — только из `ui-tokens.md` списка (нет inline `#hex`)
|
||||
|
||||
SQL (`.claude/rules/sql.md`):
|
||||
- `data/sql/NN_topic.sql` — numbered, sequential
|
||||
- `BEGIN; ... COMMIT;` обёртка для DDL
|
||||
- `IF EXISTS` / `IF NOT EXISTS` — idempotent
|
||||
- VIEW dependencies — `DROP VIEW IF EXISTS X CASCADE` если refactor
|
||||
|
||||
Deploy / infra (`.claude/rules/deploy.md`):
|
||||
- `.env.runtime` не коммитится
|
||||
- `caddy reload` после Caddyfile change
|
||||
- Forgejo Actions secrets через UI, не в коде
|
||||
|
||||
## E. Architecture & maintenance 🏗
|
||||
|
||||
- **Right placement**:
|
||||
- HTTP / serialization → `api/v1/`
|
||||
- Business logic → `services/`
|
||||
- Long-running / scheduled → `tasks.py` (Celery) / `services/<area>/`
|
||||
- Не запихивай business logic в FastAPI handler
|
||||
- **DRY**: дубль логики в parcels.py vs analytics_queries.py vs services/?
|
||||
- **Dead code**: TODO / FIXME без issue link, commented-out blocks, unused imports
|
||||
- **Naming**: `_check_token` vs `check_admin_token` — consistency с остальным codebase
|
||||
- **Module boundaries**: scraper не должен импортить из `api/v1/`; frontend hook не зовёт другой hook напрямую если можно через service
|
||||
|
||||
## F. Tests coverage 🧪
|
||||
|
||||
- Если PR меняет бизнес-логику в `services/` — есть ли тест в `backend/tests/`?
|
||||
- Если added endpoint — есть ли smoke test?
|
||||
- Если fix bug — есть ли regression test покрывающий именно тот сценарий?
|
||||
- НЕ требуй тесты для тривиальных правок (typo, refactor без logic change)
|
||||
- Если CLAUDE.md / project явно говорит "не пиши тесты пока не попросят" — не настаивай
|
||||
|
||||
## G. Vault & decisions cross-check 📚
|
||||
|
||||
**Обязательный шаг для P0-P1 PR.**
|
||||
|
||||
```
|
||||
mcp__obsidian__obsidian_simple_search "<keyword из diff>"
|
||||
mcp__obsidian__obsidian_simple_search "<имя функции / эндпоинта>"
|
||||
```
|
||||
|
||||
Проверь:
|
||||
- [ ] `fixes/` — была ли уже эта же ошибка? Новый код не повторяет старый bug?
|
||||
- [ ] `decisions/` — есть ли архитектурный decision который этот PR нарушает?
|
||||
- [ ] `limitations/` — known issue который этот код должен учитывать?
|
||||
- [ ] `code/patterns/` — есть ли established pattern для этого типа изменения?
|
||||
|
||||
Если PR fix bug — должна быть entry в `fixes/<bug>.md` (per CLAUDE.md rule #6).
|
||||
Если PR architectural change — должна быть entry в `decisions/<decision>.md`.
|
||||
125
.claude/agents/deep-review-phases/phase-5-verdict.md
Normal file
125
.claude/agents/deep-review-phases/phase-5-verdict.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# Deep Review — Phase 5: Verdict + merge
|
||||
|
||||
> Supporting reference. Verdict шаблон + auto-merge policy.
|
||||
|
||||
## Verdict format (строго)
|
||||
|
||||
```markdown
|
||||
## Deep Code Review — verdict
|
||||
|
||||
### Summary
|
||||
- **Status**: ✅ APPROVE / ⚠️ MINOR / 🟡 MEDIUM / 🟠 HIGH / 🔴 BLOCK
|
||||
- **Files reviewed**: N (P0: a, P1: b, P2: c, P3: d)
|
||||
- **Lines**: +X / -Y
|
||||
- **Time spent**: ~M min
|
||||
- **PR**: #N (если есть)
|
||||
|
||||
### 🔴 Critical (BLOCK push/merge)
|
||||
- [ ] `path/file.py:42` — описание + конкретный fix (code snippet)
|
||||
- [ ] `data/sql/95_foo.sql:12` — `ALTER TABLE ... NOT NULL DEFAULT 0` на 50M rows заблокирует таблицу. Split: ADD NULL → backfill batched → SET NOT NULL отдельным PR
|
||||
|
||||
### 🟠 High (должен быть пофиксен до merge)
|
||||
- [ ] `path/file.ts:84` — race condition: state update в onSettled() при unmounted component
|
||||
- [ ] `backend/app/api/v1/parcels.py:201` — N+1: цикл с `await db.execute(...)`, переделать на `IN (:ids)`
|
||||
|
||||
### 🟡 Medium (желательно fix, но не блокирующее)
|
||||
- [ ] `file.py:120` — `except Exception: pass` swallow'ит ошибки
|
||||
|
||||
### 🟢 Low / nits (можно отдельным PR)
|
||||
- [ ] `file.py:50` — naming `data` слишком generic, `parcels_by_district`
|
||||
|
||||
### Cross-file impact analysis
|
||||
- `backend/app/services/site_finder/foo.py` изменил signature `find_parcels()` →
|
||||
callers обновлены: `api/v1/parcels.py:88` ✅, но `tasks.py:142` всё ещё передаёт старый kwarg ❌
|
||||
- `frontend/src/types/site-finder.ts` regen нужен — `npm run codegen` не запущен в этом PR
|
||||
|
||||
### Vault cross-check
|
||||
- Похожий fix был в `fixes/Bug_Worker_Ready_EarlyReturn.md` — паттерн использован корректно ✅
|
||||
- Decision `decisions/2026-05-12-no-mutable-defaults.md` — этот PR не нарушает
|
||||
- Если bug fix, но нет entry в `fixes/` → ⚠️ нужна docs (medium severity)
|
||||
|
||||
### Performance findings
|
||||
- `EXPLAIN ANALYZE` на новом query `parcels.find_by_district`:
|
||||
- Seq Scan на `rosreestr_parcels` (5.2M rows) — нужен index `(district_id, status)`
|
||||
- Estimated cost 12.4k → с индексом 0.8
|
||||
|
||||
### Positive observations
|
||||
- ✅ `ON CONFLICT DO UPDATE` использован корректно в bulk_upsert
|
||||
- ✅ `CREATE INDEX CONCURRENTLY` в миграции
|
||||
|
||||
### Recommended next steps
|
||||
1. Worker (backend-engineer): fix 🔴 #1 + 🟠 #1, push fixup commit
|
||||
2. После fixup: run `npm run codegen` в frontend
|
||||
3. Перед merge: проверить EXPLAIN на проде через MCP postgres
|
||||
4. Создать vault entry `fixes/<this-bug>.md`
|
||||
|
||||
### Сomplexity / blast radius score
|
||||
- **Risk**: Low / Medium / High / Critical
|
||||
- **Reversibility**: Easy revert / DB migration needs manual rollback / Data loss on revert
|
||||
- **Recommended merge window**: anytime / business hours only / pre-deploy freeze
|
||||
```
|
||||
|
||||
## Когда BLOCK (🔴) — обязательные триггеры
|
||||
|
||||
1. Hardcoded secret / token / password в commit (даже test files)
|
||||
2. SQL injection vector
|
||||
3. Auth bypass на admin endpoint
|
||||
4. Migration без rollback plan (если меняет / удаляет данные)
|
||||
5. Breaking API change без deprecation / migration path
|
||||
6. Удаление prod данных без явного approval
|
||||
7. `--no-verify` / `--force` / `--amend` в недавних коммитах ветки
|
||||
8. **Прямой push в main** (HEAD == main, не feature branch)
|
||||
9. `ALTER TABLE ... NOT NULL DEFAULT` на таблице >1M rows без batched backfill
|
||||
10. `CREATE INDEX` (не CONCURRENTLY) на prod таблице >1M rows
|
||||
11. `DROP COLUMN` / `DROP TABLE` без 2-stage rollout
|
||||
12. Celery task без idempotency маркера (вернёт дубли при retry)
|
||||
13. Scraper без dedup ключа в `ON CONFLICT`
|
||||
|
||||
## Когда APPROVE (✅)
|
||||
|
||||
- Нет critical / high issues
|
||||
- Cross-file impact проверен — все callers consistent
|
||||
- Conventions OK (psycopg v3, httpx, ruff, TS strict)
|
||||
- Backward compat сохранён
|
||||
- Performance не регрессирует (или регрессия документирована и acceptable)
|
||||
- Vault entry создана для нетривиальных fixes / decisions
|
||||
- Tests есть для новой бизнес-логики (если applicable per scope)
|
||||
- Pre-flight: на feature branch, нет force / no-verify в истории
|
||||
|
||||
## Auto-merge policy (PR review mode)
|
||||
|
||||
При review открытых Forgejo PR — если verdict **✅ APPROVE** (нет 🔴/🟠/🟡):
|
||||
**мержи сам, любой scope** (user override 2026-05-16: «после аппрув в ревью можешь мерджить все что угодно»).
|
||||
|
||||
### Pre-merge checks (все обязательны)
|
||||
|
||||
1. `curl -sH "$H" "$REPO/pulls/<N>" | jq '{state,mergeable,draft,head:.head.sha}'`
|
||||
→ `state=open` (не уже merged), `mergeable=true` (no conflicts), `draft=false`
|
||||
2. Head SHA не изменился после твоего review (нет fixup push'ей с момента scan'а файлов)
|
||||
3. CI status passing — Forgejo Actions UI или `curl -sH "$H" "$REPO/commits/<sha>/status"`
|
||||
|
||||
### Merge команда (Forgejo)
|
||||
|
||||
```bash
|
||||
# Через MCP (preferred):
|
||||
mcp__forgejo__merge_pull_request (Do=squash, delete_branch_after_merge=true)
|
||||
|
||||
# Или curl:
|
||||
curl -sH "$H" -H "Content-Type: application/json" \
|
||||
-X POST "$REPO/pulls/<N>/merge" \
|
||||
-d '{"Do":"squash","delete_branch_after_merge":true}'
|
||||
|
||||
# Post-merge confirmation comment:
|
||||
curl -sH "$H" -H "Content-Type: application/json" \
|
||||
-X POST "$REPO/issues/<N>/comments" \
|
||||
-d '{"body":"Merged via deep-code-reviewer — verdict ✅ APPROVE."}'
|
||||
```
|
||||
|
||||
Forgejo API возвращает пустой body при успехе merge → verify через GET pulls/<N> что `state == merged`.
|
||||
|
||||
### Когда НЕ мержить даже при APPROVE
|
||||
|
||||
- `mergeable=false` (conflicts) → comment "approved but has conflicts — rebase needed"
|
||||
- CI failing → comment "approved but CI red — wait for green"
|
||||
- Draft PR → comment "approved, ready when undrafted"
|
||||
- Head SHA changed после твоего scan'а → НЕ мержь stale verdict, re-review нужен
|
||||
115
.claude/agents/qa-tester.md
Normal file
115
.claude/agents/qa-tester.md
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
---
|
||||
name: qa-tester
|
||||
description: QA tester for GenDesign — runs post-deploy verification после успешного merge+deploy. Use proactively СРАЗУ после того как deploy.yml завершился success на main. Проверяет: HTTP endpoints (curl), UI smoke (playwright MCP), data integrity (postgres MCP), error tracking (glitchtip MCP), regression vs known-good baseline. НЕ для unit tests (это работа worker'а во время разработки) и НЕ для pre-merge CI (это GHA).
|
||||
tools: Read, Glob, Grep, Bash, mcp__obsidian__obsidian_simple_search, mcp__obsidian__obsidian_get_file_contents, mcp__obsidian__obsidian_list_files_in_dir, mcp__obsidian__obsidian_append_content, mcp__postgres-gendesign__execute_sql, mcp__postgres-gendesign__list_objects, mcp__postgres-gendesign__get_object_details, mcp__playwright__browser_navigate, mcp__playwright__browser_navigate_back, mcp__playwright__browser_snapshot, mcp__playwright__browser_take_screenshot, mcp__playwright__browser_console_messages, mcp__playwright__browser_network_requests, mcp__playwright__browser_click, mcp__playwright__browser_fill_form, mcp__playwright__browser_type, mcp__playwright__browser_press_key, mcp__playwright__browser_wait_for, mcp__playwright__browser_close, mcp__playwright__browser_evaluate, mcp__glitchtip__glitchtip_issues, mcp__glitchtip__glitchtip_latest_event
|
||||
model: sonnet
|
||||
memory: project
|
||||
color: cyan
|
||||
---
|
||||
|
||||
# QA Tester — GenDesign
|
||||
|
||||
Ты — QA-инженер, который запускается **после успешного deploy** на прод. Цель: подтвердить что фича/фикс реально работает в проде, а не только "тесты прошли в CI".
|
||||
|
||||
## Когда тебя зовут
|
||||
|
||||
Main session вызывает тебя, когда:
|
||||
1. PR замержен review-bot'ом → deploy.yml на main завершился `success` → есть свежий sha на проде
|
||||
2. Hotfix manual deploy через `workflow_dispatch` → success
|
||||
3. Скраперы / Celery beat задачи которые проявляются только под нагрузкой
|
||||
|
||||
**НЕ для тебя:**
|
||||
- Unit-тесты во время разработки (pytest / vitest — это backend/frontend-engineer)
|
||||
- Pre-merge CI gate (GHA)
|
||||
- Ревью кода (`code-reviewer`)
|
||||
- Расследование root cause багов (`tech-analyst`)
|
||||
|
||||
## Вход (что тебе дают)
|
||||
|
||||
Main session передаёт:
|
||||
- **Sha на проде** (`e97f96a` или подобный)
|
||||
- **PR number** который только что задеплоился (`#190`)
|
||||
- **Что именно тестировать**: новый endpoint? UI change? scraper? migration?
|
||||
- **Ожидаемое поведение** (1-2 sentences, golden path)
|
||||
- Опционально: **известные edge cases**
|
||||
|
||||
## Что ты делаешь
|
||||
|
||||
### 1. Скоуп тестов из PR diff
|
||||
|
||||
Forgejo PR files через curl (`$FORGEJO_URL`/`$FORGEJO_TOKEN` в env):
|
||||
|
||||
```bash
|
||||
H="Authorization: token $FORGEJO_TOKEN"
|
||||
REPO="$FORGEJO_URL/api/v1/repos/lekss361/gendesign"
|
||||
curl -sH "$H" "$REPO/pulls/<N>/files?limit=50" | jq -r '.[].filename'
|
||||
```
|
||||
|
||||
→ определи changed paths:
|
||||
- `backend/app/api/v1/<X>.py` → curl endpoint этого роутера
|
||||
- `frontend/src/app/**` → chrome-devtools navigate + snapshot
|
||||
- `data/sql/NN_*.sql` → postgres MCP проверь schema (column exists, index there, view OK)
|
||||
- `backend/app/scrapers/**` или `backend/app/workers/**` → запусти scraper, polling DB до terminal status
|
||||
- `docker-compose*.yml` / `Caddyfile` → smoke production URLs + проверь containers running
|
||||
|
||||
### 2. Golden path
|
||||
|
||||
Один happy-path test для главного use case PR'а:
|
||||
- API: `curl -fsS https://gendsgn.ru/<endpoint>` → 200 + ожидаемый JSON shape
|
||||
- UI: `mcp__playwright__browser_navigate` → `browser_snapshot` → проверь expected element visible → `browser_console_messages` (0 errors) + `browser_network_requests` (0 failed 4xx/5xx)
|
||||
- DB: SELECT и проверь что новые rows / columns / constraints на месте
|
||||
- Scraper: POST trigger → polling `cadastre_jobs.status` пока `done|failed|cancelled` → проверь `targets_done == targets_total` без `error`
|
||||
- Errors: `mcp__glitchtip__glitchtip_issues` после теста — нет новых ERROR events связанных с PR scope
|
||||
|
||||
### 3. Regression sanity (2-3 quick checks)
|
||||
|
||||
- `curl -fsS https://gendsgn.ru/` → 200 (frontend up)
|
||||
- Известный читабельный endpoint (`/api/v1/parcels/<known-cad>`) → 200, ответ не пустой
|
||||
- `docker ps` через SSH (если разрешено) — все critical containers `healthy` / `Up`
|
||||
|
||||
### 4. Edge cases (если указаны)
|
||||
|
||||
User передал "проверь что invalid input → 400, не 500" — гоняй именно это.
|
||||
|
||||
## Выход (report main session, ≤200 слов)
|
||||
|
||||
```
|
||||
## QA verdict: ✅ PASS | ⚠️ PARTIAL | ❌ FAIL
|
||||
|
||||
### Tested (PR #N, sha XXX on prod)
|
||||
- Endpoint POST /api/v1/X → 200, response OK
|
||||
- Frontend /admin/Y → element visible, 0 console errors
|
||||
- DB: column Z exists в таблице, NOT NULL, default OK
|
||||
|
||||
### Regression baseline
|
||||
- frontend root: 200 ✅
|
||||
- backend health: 200 ✅
|
||||
- known parcel API: 200 ✅
|
||||
|
||||
### Failures (если есть)
|
||||
- [path/endpoint] → [actual vs expected] → [возможная гипотеза]
|
||||
|
||||
### Suggestion
|
||||
- если PASS → можно закрыть task
|
||||
- если PARTIAL → main session делегирует back to worker для фикса
|
||||
- если FAIL → rollback (revert PR) или urgent fixup PR
|
||||
```
|
||||
|
||||
## Доступные ресурсы
|
||||
|
||||
- **HTTP** curl/wget — для API smoke
|
||||
- **playwright MCP** — UI smoke: `browser_navigate` + `browser_snapshot` + `browser_console_messages` + `browser_network_requests` + `browser_click`/`browser_fill_form`/`browser_type` для интерактива. `browser_evaluate` для inline JS (eg. читать localStorage / data-attributes)
|
||||
- **postgres-gendesign MCP** — для DB queries (read-only по умолчанию)
|
||||
- **glitchtip MCP** — `glitchtip_issues` / `glitchtip_latest_event` для проверки что после deploy не появилось новых exception events
|
||||
- **Forgejo REST API** (curl + `$FORGEJO_TOKEN` env) — `$REPO/pulls/<N>` / `/files` / `.diff` для скоупа теста; `POST $REPO/issues/<N>/comments` для verdict; `POST $REPO/issues` + `GET $REPO/issues?state=open` для регрессий. Auth header: `-H "Authorization: token $FORGEJO_TOKEN"`. Owner/repo: `lekss361/gendesign`. Полные примеры команд: см. `.claude/agents/deep-code-reviewer.md` Phase 1
|
||||
- **Vault** — credentials (`meta/00_credentials.md`), известные fixes (`fixes/`)
|
||||
- **SSH gendesign** — производ docker ps / health checks (только read-only ops, никаких compose down / restart)
|
||||
|
||||
## Запреты
|
||||
|
||||
- ❌ Не пиши код / не правь файлы — ты только тестируешь
|
||||
- ❌ Не делай deploy / restart containers / drop tables
|
||||
- ❌ Не мержи PR / не пушь
|
||||
- ❌ Не доверяй "тесты прошли в CI" — verify в проде через реальные HTTP/UI/SQL вызовы
|
||||
- ❌ Не зацикливайся на 100% coverage — golden path + 2-3 regression + edge cases указанные в задаче, всё
|
||||
- ❌ Не пиши длинных отчётов — verdict + 5-10 bullets, всё что нужно main session'у
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
name: tech-analyst
|
||||
description: Tech analyst / planner для GenDesign. Use proactively когда пользователь приходит с НЕЧЁТКОЙ задачей ("надо добавить фичу X", "почему так медленно", "что починить дальше"), для рефакторинговых разборов, для cross-domain задач затрагивающих 2+ слоя (backend + frontend + db). Read-only — НЕ пишет код. Возвращает структурированный план: что делать, в каком порядке, какой subagent отвечает за каждый шаг.
|
||||
tools: Read, Glob, Grep, WebSearch, WebFetch, mcp__obsidian__obsidian_simple_search, mcp__obsidian__obsidian_complex_search, mcp__obsidian__obsidian_get_file_contents, mcp__obsidian__obsidian_batch_get_file_contents, mcp__obsidian__obsidian_list_files_in_dir, mcp__obsidian__obsidian_get_recent_changes, mcp__postgres-gendesign__list_objects, mcp__postgres-gendesign__get_object_details, mcp__postgres-gendesign__list_schemas, mcp__postgres-gendesign__explain_query, mcp__postgres-gendesign__analyze_query_indexes, mcp__postgres-gendesign__analyze_db_health, mcp__postgres-gendesign__get_top_queries, Bash
|
||||
model: sonnet
|
||||
model: haiku
|
||||
color: yellow
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ paths:
|
|||
- docker-compose*.yml
|
||||
- Caddyfile
|
||||
- .github/workflows/**
|
||||
- .forgejo/workflows/**
|
||||
- scripts/setup-*.sh
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
paths:
|
||||
- .claude/**
|
||||
- .github/**
|
||||
- .forgejo/**
|
||||
---
|
||||
|
||||
# Git + PR workflow
|
||||
|
|
@ -11,17 +12,17 @@ paths:
|
|||
## Branch + PR (MANDATORY)
|
||||
|
||||
```
|
||||
1. git checkout -b feat/<scope> # или fix/ refactor/ docs/ chore/ perf/
|
||||
1. git fetch forgejo && git checkout -b feat/<scope> forgejo/main # или fix/ refactor/ docs/ chore/ perf/
|
||||
2. [worker делает код, staged]
|
||||
3. code-reviewer subagent на staged changes (pre-push)
|
||||
4. main session: git commit -m "feat(scope): ..."
|
||||
5. git push -u origin feat/<scope>
|
||||
6. gh pr create --title "..." --body "$(cat <<EOF...)"
|
||||
7. Вернуть PR URL пользователю
|
||||
8. СРАЗУ ScheduleWakeup polling (90-120s)
|
||||
5. git push -u forgejo feat/<scope>
|
||||
6. mcp__forgejo__create_pull_request (или curl POST $REPO/pulls)
|
||||
7. Вернуть PR URL пользователю (https://git.gendsgn.ru/lekss361/gendesign/pulls/<N>)
|
||||
8. СРАЗУ Skill loop polling (см. Polling loop)
|
||||
```
|
||||
|
||||
**Никаких direct push в main.** Только через PR merge.
|
||||
**Никаких direct push в main.** Только через PR merge через Forgejo. `gh` CLI bypassed (2026-05-16) — все PR-операции через `mcp__forgejo__*` или curl + `$FORGEJO_TOKEN`.
|
||||
|
||||
## Commit messages
|
||||
|
||||
|
|
@ -48,15 +49,15 @@ Closes #N
|
|||
|
||||
## Polling loop
|
||||
|
||||
После `gh pr create` — СРАЗУ `ScheduleWakeup` (90-120s):
|
||||
После `mcp__forgejo__create_pull_request` — СРАЗУ `Skill loop` (self-paced, 90-120s) или background Bash poll:
|
||||
|
||||
1. `gh pr view <N> --json state,mergeable,comments,headRefOid`
|
||||
2. `state == MERGED` → stop polling
|
||||
3. Новый bot comment: распарси `<!-- gendesign-review-bot: sha=<sha7> verdict=<approve|changes> -->`
|
||||
- **SHA guard**: `marker.sha7 == headRefOid[:7]` — иначе устаревший approval до fixup-push, игнорируй
|
||||
1. `mcp__forgejo__get_pull_request` (или `curl -sH "$H" "$REPO/pulls/<N>"`) → читай `state`, `mergeable`, `head.sha`
|
||||
2. `state == merged` → stop polling
|
||||
3. Новый review/comment: `mcp__forgejo__list_pr_reviews` / `list_issue_comments`. Парсь marker `<!-- gendesign-review-bot: sha=<sha7> verdict=<approve|changes> -->`
|
||||
- **SHA guard**: `marker.sha7 == head.sha[:7]` — иначе устаревший approval до fixup-push, игнорируй
|
||||
- **Scope guard** (см. ниже): если blocked paths → ping user, не auto-merge
|
||||
- `verdict=approve` + SHA match + scope OK → `gh pr merge <N> --squash --delete-branch`
|
||||
- `verdict=changes` → fixup commits + push + re-poll
|
||||
- `verdict=approve` + SHA match + scope OK → `mcp__forgejo__merge_pull_request` (squash + delete branch)
|
||||
- `verdict=changes` → fixup commits + push в `forgejo feat/<scope>` + re-poll
|
||||
4. Нет новых comments → re-schedule 60s
|
||||
5. **Cap**: 30 iter без resolution → stop, ping user. Blocked-scope cap = 5.
|
||||
|
||||
|
|
@ -95,8 +96,9 @@ Issues ≥ 1.5 day → 3-4 sub-PRs: **Foundation → Schema → Workers → Inte
|
|||
|
||||
## Запреты
|
||||
|
||||
- ❌ `git push origin main` / direct push в main
|
||||
- ❌ `gh pr merge` без approval (human "merge it" или bot verdict=approve + SHA match)
|
||||
- ❌ `git push forgejo main` / direct push в main
|
||||
- ❌ `mcp__forgejo__merge_pull_request` без approval (human "merge it" или bot verdict=approve + SHA match)
|
||||
- ❌ `gh pr *` — bypassed 2026-05-16, используй Forgejo MCP или curl + `$FORGEJO_TOKEN`
|
||||
- ❌ `--no-verify` / `--amend` / `--no-edit` / `--force` без явного approval
|
||||
- ❌ `@claude` в PR comments — plain text only (`feedback_no_claude_mentions`)
|
||||
- ❌ Параллельные PR на одни файлы (`feedback_sequential_prs`)
|
||||
|
|
|
|||
129
.claude/rules/ui-conventions.md
Normal file
129
.claude/rules/ui-conventions.md
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
---
|
||||
paths:
|
||||
- frontend/src/components/**
|
||||
- frontend/src/app/**/page.tsx
|
||||
- frontend/src/app/**/layout.tsx
|
||||
---
|
||||
|
||||
# UI components & data-viz conventions — GenDesign
|
||||
|
||||
> Часть набора `ui-*.md`. Tokens см. `ui-tokens.md`, microcopy/copy — `ui-microcopy.md`.
|
||||
|
||||
## Tone & audience
|
||||
|
||||
- **Аудитория**: финдиректор / продукт-директор девелопера №4-15 РФ (PRINZIP-tier). НЕ архитектор, НЕ ритейл.
|
||||
- **Tone of voice**: DOM.РФ × CoStar × Bloomberg-density. «Государственная аналитика, которая считает за тебя». НЕ стартап-AI-маркетинг.
|
||||
- **Информационная плотность > красивости.** Цифры читаются с проектора с 5 метров.
|
||||
|
||||
## Component conventions
|
||||
|
||||
### KpiCard
|
||||
- Использовать `<KpiCard>` из `components/analytics/KpiCard.tsx` — НЕ городить inline.
|
||||
- KPI row: `display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px;` — НЕ flex+wrap (даёт ровные ряды).
|
||||
- 3-4 KPI выше fold — максимум. Больше → под аккордеон «Детали».
|
||||
- `delta.positive: null` (нейтральный) — для метрик где «больше/меньше» не имеет валентности (например, средняя цена).
|
||||
|
||||
### Section
|
||||
- Заворачивать каждый блок графика/таблицы в `<Section title="..." subtitle="...">`.
|
||||
- `subtitle` — одно предложение объясняющее «откуда данные / что значит цифра», не маркетинг.
|
||||
- `right` slot — для controls (filter dropdown, export button, slicer chips).
|
||||
|
||||
### Headline-bar (тёмная плашка-вердикт)
|
||||
- Pattern: `--bg-headline` фон, `--fg-on-dark` текст, padding 14/18, radius 12.
|
||||
- **Одно предложение-ответ JTBD**: «В районе X класс Y · S м² → mix А · срок B мес · выручка C ₽».
|
||||
- Под основным текстом — separator + caveats / context (ИЖК ставка, POI, дата snapshot).
|
||||
- Размещать выше всех KPI-карточек.
|
||||
|
||||
### Badge
|
||||
- Использовать `<Badge variant="success|warn|danger|neutral">`. НЕ inline-span с hex.
|
||||
- Если компонента нет — создать в `components/ui/Badge.tsx`, не дублировать stylesheet'ы по pages.
|
||||
|
||||
### Drawer (drill-down правым слайдером)
|
||||
- Pattern для `/analytics/developers` детализации. НЕ навигировать на отдельную страницу при клике на строку таблицы — открывать right drawer (CoStar pattern).
|
||||
- Width 480px desktop, full-screen на phone.
|
||||
|
||||
## Data-viz (ECharts) rules
|
||||
|
||||
- **Ось Y ВСЕГДА с подписью** (`yAxis.name`). Без подписи — это нарушение review.
|
||||
- **Tooltip ВСЕГДА с единицей измерения**: «1 240 ₽/м²», «73%», «28 мес».
|
||||
- **Цвета серий** — только из `--viz-1..5` в указанном порядке.
|
||||
- **Forecast / prediction** — пунктирная линия + band ±p25/p75 (HouseCanary). Не показывать точку без интервала.
|
||||
- **Никаких 3D charts**, никаких pie с >5 сегментов (заменить на bar). Treemap — OK.
|
||||
- Lazy-mount тяжёлых charts через `dynamic(() => import(...), { ssr: false })` (см. `objects/[id]/page.tsx`).
|
||||
|
||||
## Iconography
|
||||
|
||||
- **Lucide React only** — `lucide-react` пакет. Размеры 16 / 20 / 24px, stroke 1.5px.
|
||||
- **НЕ emoji в UI** — 🚀 ❄ 💼 📊 → `<TrendingUp/>`, `<Snowflake/>`, `<Briefcase/>`, `<BarChart3/>`.
|
||||
- Trend-glyph в KPI — Unicode arrow `↑ ↓ →` или Lucide, НЕ emoji.
|
||||
- Иконка-без-текста → `aria-label`.
|
||||
|
||||
## Карта (Leaflet) — когда и как
|
||||
|
||||
- **Аналитика overview без карты — фейл** (Bnmap-parity). На `/analytics` нужна секция «Карта рынка» с точками новостроек DOM.РФ, цвет по `sold%`.
|
||||
- Leaflet через `dynamic(..., { ssr: false })` — НИКОГДА не импортировать в server component.
|
||||
- Высота карты: 360px (drill-in), 480px (overview), full-screen toggle FAB на phone.
|
||||
- Layers: OSM base + наш geojson. Не подключать платные tile-серверы без согласования.
|
||||
|
||||
## Above-the-fold rule
|
||||
|
||||
Финдиректор закрывает вкладку через 8 секунд если не видит:
|
||||
1. Что это за страница (h1 + один subtitle).
|
||||
2. **Один вердикт** одним предложением (headline-bar).
|
||||
3. **3 главных числа** (KPI row).
|
||||
|
||||
Всё остальное (sliders, deep tables, comparables) — **под аккордеон** «Подробнее». По умолчанию закрыто.
|
||||
|
||||
## Responsive (см. issue #66)
|
||||
|
||||
- 3 контекста: phone `<768`, tablet `768-1280`, desktop `>1280`. НЕ Tailwind sm/md/lg напрямую.
|
||||
- Phone: nav-tabs → horizontal scroll + snap, НЕ hamburger.
|
||||
- Phone: KPI grid → 1 col full-width.
|
||||
- Chart высоты фиксированные, `ResizeObserver` для ECharts.
|
||||
- НЕ делать PWA / offline. Аудитория с десктопа.
|
||||
|
||||
## Accessibility (минимум, не максимум)
|
||||
|
||||
- Контраст AA для body (4.5:1). Hex-token подобраны под AA на белом / slate-900.
|
||||
- `aria-label` для icon-only buttons.
|
||||
- Focus ring видимый (НЕ `outline: none` без замены).
|
||||
- Кликабельный элемент — `<button>` или `<a>`, не `<div onClick>`.
|
||||
|
||||
## CTA hierarchy
|
||||
|
||||
- **Primary** (`--accent` blue): главное действие на странице («Рассчитать», «Создать концепцию»).
|
||||
- **Secondary** (`--accent-2` orange): «Export CSV», «Поделиться», «Снимок отчёта».
|
||||
- **Tertiary** (text-only link с underline на hover): nav, breadcrumbs, «← Назад».
|
||||
- Одна primary CTA на экран. Больше — расфокус.
|
||||
|
||||
## Запреты (анти-паттерны)
|
||||
|
||||
- ❌ Dark mode toggle (аудитория читает с проектора)
|
||||
- ❌ Emoji в UI (🚀 ❄ 💼 📊 🎉) — Lucide вместо
|
||||
- ❌ Tab nav в Tailwind sm/md/lg breakpoints (использовать наши 3 контекста)
|
||||
- ❌ Pie chart с >5 сегментов
|
||||
- ❌ 3D-чарты, изометрия, lifestyle-фото в analytics
|
||||
- ❌ Live-chat widget (Intercom) — наша аудитория звонит/пишет
|
||||
- ❌ Customer logos без подтверждённого права
|
||||
- ❌ Анимации >200ms (это финансовый дашборд)
|
||||
- ❌ Skeleton с shimmer — простой grey fade
|
||||
- ❌ TestFit / Spacemaker / Figma-dribbble стиль — НЕ наш референс
|
||||
- ❌ Chart без axis label / unit в tooltip
|
||||
- ❌ Predictions без confidence interval (точка без диапазона выглядит «слишком уверенно»)
|
||||
- ❌ Drill-down переходом на новую страницу (использовать Drawer)
|
||||
|
||||
## Что проверять в pre-push review (UI-PR)
|
||||
|
||||
1. Цвета все из токенов? (grep `#[0-9a-fA-F]{3,8}` в .tsx — должно быть пусто кроме токенов).
|
||||
2. Emoji в UI? (`grep -P '[\x{1F300}-\x{1FAFF}]'` — пусто).
|
||||
3. KPI label uppercase + letter-spacing?
|
||||
4. Chart с axis label и tooltip unit?
|
||||
5. Section title не центрирован?
|
||||
6. Lazy-mount для Leaflet / тяжёлых ECharts?
|
||||
7. Headline-bar присутствует на странице с JTBD-ответом?
|
||||
|
||||
## References
|
||||
|
||||
- vault `research/UX_Analytics_Developers_Report_May17.md` — полный отчёт с бенчмарком
|
||||
- vault `audits/batch_03_b_series_generative` — issues #64-66 (landing, analytics search, mobile)
|
||||
- `frontend/src/components/analytics/KpiCard.tsx`, `Section.tsx` — эталонные компоненты
|
||||
29
.claude/rules/ui-microcopy.md
Normal file
29
.claude/rules/ui-microcopy.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
paths:
|
||||
- frontend/src/app/**/*.{ts,tsx}
|
||||
- frontend/src/components/**/*.{ts,tsx}
|
||||
---
|
||||
|
||||
# UI microcopy & export — GenDesign
|
||||
|
||||
> Часть набора `ui-*.md`. Tokens — `ui-tokens.md`, components/viz — `ui-conventions.md`.
|
||||
|
||||
## Microcopy
|
||||
|
||||
- Русский. Типографика: `₽ / м²` (пробелы), `п.п.` (не «pp»), `м²` (не «м2»), `≥` `≤` `±` (Unicode, не `>=`).
|
||||
- Числа: `toLocaleString("ru")` для тысяч (`6 832 540`, не `6,832,540`).
|
||||
- Деньги: `2.4 млрд ₽` для крупных, `145 320 ₽` для точных. Round по контексту.
|
||||
- **Никаких** «Yay!» / «Ой!» / «Готово 🎉». Neutral business: «Расчёт обновлён», «Данные за период недоступны».
|
||||
- Caveat — полным предложением, не аббревиатурой: «Рекомендация основана на городском распределении сделок» — OK.
|
||||
|
||||
## Запреты (microcopy)
|
||||
|
||||
- ❌ Microcopy «Yay!» / emoji в success-state
|
||||
- ❌ Англицизмы без перевода (например "Submit" в кнопке вместо «Подтвердить»)
|
||||
- ❌ Сокращения без расшифровки на первой странице (ИЖК → расшифровать в caveat)
|
||||
|
||||
## Export-кнопка (dataflat lesson)
|
||||
|
||||
- На каждой Section с табличными данными — кнопка `Export CSV` / `Export Excel` в `Section.right`.
|
||||
- Endpoint backend должен отдать CSV с UTF-8 BOM (Excel-friendly), `Content-Disposition: attachment; filename=...`.
|
||||
- Имя файла: `gendesign_<page>_<scope>_<YYYY-MM-DD>.csv`.
|
||||
87
.claude/rules/ui-tokens.md
Normal file
87
.claude/rules/ui-tokens.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
---
|
||||
paths:
|
||||
- frontend/**/*.css
|
||||
- frontend/src/components/**/*.{ts,tsx}
|
||||
- frontend/src/app/**/*.{ts,tsx}
|
||||
---
|
||||
|
||||
# UI design tokens — GenDesign
|
||||
|
||||
> Часть набора `ui-*.md` (token / conventions / microcopy). Полное обоснование (бенчмарк vs Bnmap/DOM.РФ/CoStar/Bloomberg) — vault `research/UX_Analytics_Developers_Report_May17.md`.
|
||||
|
||||
## Design tokens (HARD — НЕ выдумывать новые)
|
||||
|
||||
Цвета — только из этого списка. Inline `#hex` вне токенов = нарушение (ругать в review).
|
||||
|
||||
```css
|
||||
/* Surface */
|
||||
--bg-app: #F6F7F9;
|
||||
--bg-card: #FFFFFF;
|
||||
--bg-card-alt: #FAFBFC; /* skeleton, lazy-mount */
|
||||
--bg-headline: #0F172A; /* slate-900, тёмная headline-bar */
|
||||
|
||||
/* Borders */
|
||||
--border-soft: #EEF0F3;
|
||||
--border-card: #E6E8EC;
|
||||
--border-strong: #D1D5DB;
|
||||
|
||||
/* Text */
|
||||
--fg-primary: #111111;
|
||||
--fg-secondary: #5B6066;
|
||||
--fg-tertiary: #73767E;
|
||||
--fg-on-dark: #E2E8F0;
|
||||
--fg-on-dark-muted: #94A3B8;
|
||||
|
||||
/* Brand */
|
||||
--accent: #1D4ED8; /* primary CTA, tabs, focused link */
|
||||
--accent-hover: #1E40AF;
|
||||
--accent-soft: #DBEAFE;
|
||||
--accent-2: #F2994A; /* secondary CTA: Export, Share, Snapshot */
|
||||
|
||||
/* Semantic */
|
||||
--success: #0A7A3A; --success-soft: #DCFCE7;
|
||||
--warn: #9A6700; --warn-soft: #FEF3C7;
|
||||
--danger: #B3261E; --danger-soft: #FEE2E2;
|
||||
|
||||
/* Data-viz sequence (порядок ОБЯЗАТЕЛЕН — viz-1 всегда «свой» / focused) */
|
||||
--viz-1: #1D4ED8; --viz-2: #0EA5E9; --viz-3: #14B8A6;
|
||||
--viz-4: #F59E0B; --viz-5: #8B5CF6;
|
||||
|
||||
/* Prediction / confidence (HouseCanary pattern) */
|
||||
--prediction-line: #0EA5E9;
|
||||
--prediction-band: rgba(14, 165, 233, 0.18);
|
||||
```
|
||||
|
||||
**Цвет в chart серий** применяется ТОЛЬКО для сравнения серий. Одиночная линия — всегда `#374151`.
|
||||
|
||||
## Layout & spacing tokens
|
||||
|
||||
- Page `max-width: 1280px` (analytics-deep — `clamp(1280px, 90vw, 1440px)` на ≥1600px).
|
||||
- Spacing scale: **4 / 8 / 12 / 16 / 24 / 32**. Никаких 6/10/14/18.
|
||||
- Card padding: **20px** (Section), **16/18** (KpiCard) — НЕ менять.
|
||||
- Card radius: **12px** (cards), 8px (chart skeleton), 6px (badges).
|
||||
- Section vertical rhythm: `margin-top: 24` между секциями (НЕ 16).
|
||||
- **НЕ использовать `box-shadow`** на cards. Border `--border-card` достаточно. Тень — только для modals / popovers.
|
||||
- Inline `style={{}}` допустим только для динамических значений (height по data, transform, color по delta). Статический стиль → Tailwind utility или token.
|
||||
|
||||
## Typography tokens
|
||||
|
||||
- **Font stack**: `Inter, -apple-system, "Segoe UI", system-ui, sans-serif` через `next/font/google` (self-hosted, privacy).
|
||||
- **Tabular-nums везде** для цифр: `font-variant-numeric: tabular-nums; font-feature-settings: "tnum";` в `:root`.
|
||||
- Размеры:
|
||||
- **display** 28/36 weight 600 — KPI value
|
||||
- **h1** 22/28 weight 600 — page title
|
||||
- **h2** 18/24 weight 600 — Section title
|
||||
- **body** 14/20 weight 400
|
||||
- **caption** 12/16 weight 400
|
||||
- **label** 12/16 weight 500 uppercase letter-spacing 0.04em — KPI label
|
||||
- Заголовок (h1, h2) не центрировать. Только left-align.
|
||||
|
||||
## Запреты (token violations)
|
||||
|
||||
- ❌ Inline hex вне токенов (`color: "#1d4ed8"` → `color: var(--accent)`)
|
||||
- ❌ Box-shadow на cards (border достаточно)
|
||||
- ❌ Gradient backgrounds — flat tokens only
|
||||
- ❌ Centered h1/h2 (только left-align)
|
||||
- ❌ Spacing не из scale (6/10/14/18 → 4/8/12/16/24/32)
|
||||
- ❌ Хардкод цвета серии в ECharts (брать из `--viz-N` по порядку)
|
||||
37
.gitignore
vendored
37
.gitignore
vendored
|
|
@ -29,19 +29,42 @@ frontend/.sentryclirc
|
|||
.mcp.json
|
||||
# креды-заметки — НЕ коммитить (см. issue #391)
|
||||
sshkey.txt
|
||||
# Cian session cookies (browser-export) для local-sweep — НЕ коммитить
|
||||
tradein-mvp/scripts/.cian-cookies.json
|
||||
tradein-mvp/scripts/*.cookies.json
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
|
||||
# Claude Code local state (cache, locks, scheduled tasks, local settings)
|
||||
.claude/
|
||||
# Track team-shared agents + path-scoped rules
|
||||
!.claude/agents/
|
||||
!.claude/agents/**
|
||||
!.claude/rules/
|
||||
!.claude/rules/**
|
||||
# Claude Code — local state only (cache, locks, sessions, schedules).
|
||||
# Agents and rules ARE tracked: they are team-shared config.
|
||||
# Explicit per-path list — `.claude/` + `!.claude/agents/` whitelist pattern
|
||||
# does not work because git cannot re-include files under an ignored parent.
|
||||
.claude/settings.json
|
||||
.claude/settings.local.json
|
||||
.claude/cache/
|
||||
.claude/sessions/
|
||||
.claude/locks/
|
||||
.claude/file-history/
|
||||
.claude/shell-snapshots/
|
||||
.claude/tasks/
|
||||
.claude/plans/
|
||||
.claude/telemetry/
|
||||
.claude/projects/
|
||||
.claude/worktrees/
|
||||
.claude/daemon/
|
||||
.claude/session-env/
|
||||
.claude/stats-cache.json
|
||||
.claude/history.jsonl
|
||||
.claude/mcp-needs-auth-cache.json
|
||||
.claude/paste-cache/
|
||||
.claude/downloads/
|
||||
.claude/backups/
|
||||
.claude/plugins/
|
||||
.claude/agent-memory*/
|
||||
.claude/scheduled_tasks.lock
|
||||
|
||||
# Local data (raw CSVs from Rosreestr/etc — too large for git)
|
||||
data/raw/
|
||||
|
|
|
|||
51
CLAUDE.md
51
CLAUDE.md
|
|
@ -12,43 +12,14 @@ Live: `https://gendsgn.ru/` — Свердловская обл. (ЕКБ, ПЗЗ
|
|||
| Frontend | Next.js 15 app router, React 19, TypeScript 5 strict, Tailwind 4, TanStack Query |
|
||||
| Export | WeasyPrint (PDF), ezdxf (DXF), openpyxl (Excel) |
|
||||
| Tooling | uv, ruff, mypy strict (selective), pytest, pre-commit, prettier |
|
||||
| Deploy | Beget VPS · GHA → GHCR → SSH → docker-compose · Caddy |
|
||||
| Deploy | Beget VPS · Forgejo Actions → GHCR → SSH → docker-compose · Caddy |
|
||||
| Knowledge | Obsidian vault `obsidian.gendsgn.ru` (CouchDB LiveSync) |
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Dev (Docker):
|
||||
docker compose up -d --build
|
||||
curl http://localhost:8000/health
|
||||
|
||||
# Backend without Docker:
|
||||
cd backend && uv sync && uv run uvicorn app.main:app --reload
|
||||
|
||||
# Frontend:
|
||||
cd frontend && npm install && npm run dev
|
||||
|
||||
# Migrations:
|
||||
make migration NAME=add_foo_column # Alembic
|
||||
# или raw SQL: data/sql/NN_topic.sql # views, partitions, indexes
|
||||
|
||||
# Tests:
|
||||
cd backend && uv run pytest
|
||||
cd frontend && npm test
|
||||
|
||||
# Lint + typecheck:
|
||||
cd backend && uv run --no-project --with ruff ruff check app/
|
||||
cd frontend && npx tsc --noEmit && npm run lint
|
||||
|
||||
# Regen TS types from OpenAPI:
|
||||
cd frontend && npm run codegen
|
||||
```
|
||||
|
||||
## Critical rules
|
||||
|
||||
1. **Knowledge = Obsidian vault only.** `mcp__obsidian__*` tools. Local mirror: `C:\Users\user\Documents\gendesign-memory`. Deprecated: `memory/memory-gendesign.jsonl` — не читай, не пиши.
|
||||
|
||||
2. **Branch + PR mandatory.** Никаких direct push в main. `feat/...` / `fix/...` / `refactor/...` / `docs/...` / `chore/...` → `gh pr create` → PR URL пользователю. После PR create — сразу `ScheduleWakeup` polling. **Полные правила: `.claude/rules/git-pr.md`**.
|
||||
2. **Branch + PR mandatory.** Никаких direct push в main. `feat/...` / `fix/...` / `refactor/...` / `docs/...` / `chore/...` → ветка от `forgejo/main` → `mcp__forgejo__create_pull_request` → PR URL пользователю. После PR create — сразу `Skill loop` polling. `gh` CLI bypassed (2026-05-16). **Полные правила: `.claude/rules/git-pr.md`**.
|
||||
|
||||
3. **Agent-first workflow.** Main session orchestrates ONLY — не пишет код inline. Любая задача >1 файл / >50 строк → subagent. Pre-push: spawn `code-reviewer` subagent на staged changes. Post-push review — внешнее окно Claude (НЕ дублировать).
|
||||
|
||||
|
|
@ -56,9 +27,9 @@ cd frontend && npm run codegen
|
|||
|
||||
5. **No `--no-verify` / `--force` / `--amend`.** Pre-commit hooks обязательны. Hook падает → fix root cause.
|
||||
|
||||
6. **Vault обновляется вместе с кодом** — в той же сессии. Bug → `fixes/`, module → `code/modules/`, decision → `decisions/`, runbook → `runbooks/`.
|
||||
6. **Vault обновляется вместе с кодом** — в той же сессии. **Используй `/vault-write`** для новой записи (auto-routes через inbox → vault-overlord → правильная папка). НЕ пиши напрямую в `fixes/` / `decisions/` / `code/modules/`. Periodic cleanup: `/sort-inbox` (раскладка inbox), `/retention-pass` (архив устаревшего active в `old/`). Полные правила: `meta/02_claude_usage.md` + `inbox/README.md` в vault.
|
||||
|
||||
7. **Тестируй каждый merge** — chrome-devtools MCP или curl ДО/сразу после deploy. Не доверяй "тесты прошли" без verify.
|
||||
7. **Тестируй каждый merge** — `mcp__playwright__*` или curl ДО/сразу после deploy (post-deploy spawn `qa-tester`). Не доверяй "тесты прошли" без verify.
|
||||
|
||||
## Subagents (`.claude/agents/`)
|
||||
|
||||
|
|
@ -68,30 +39,24 @@ cd frontend && npm run codegen
|
|||
| `backend-engineer` | `backend/app/**` — FastAPI, Celery, scrapers, services |
|
||||
| `frontend-engineer` | `frontend/src/**` — Next.js, React, hooks |
|
||||
| `database-expert` | `data/sql/**.sql`, Alembic, EXPLAIN ANALYZE |
|
||||
| `devops-engineer` | `docker-compose*.yml`, `Caddyfile`, `.github/workflows/**` |
|
||||
| `devops-engineer` | `docker-compose*.yml`, `Caddyfile`, `.github/workflows/**`, `.forgejo/workflows/**` |
|
||||
| `code-reviewer` | Pre-push lint (security, correctness, conventions) |
|
||||
|
||||
**Routing:** тривиально (typo, 1-line) → main session. Single-domain clear → worker. Cross-domain / нечётко → `tech-analyst` first. Worker → `code-reviewer` → main commits → push → PR.
|
||||
|
||||
## Where to look
|
||||
|
||||
- **Path-scoped rules:** `.claude/rules/` (backend, frontend, sql, git-pr, deploy) — auto-loaded при работе с соответствующими файлами
|
||||
- **Path-scoped rules:** `.claude/rules/` (backend, frontend, sql, git-pr, deploy, ui-tokens, ui-conventions, ui-microcopy) — auto-loaded при работе с соответствующими файлами через `paths:` frontmatter
|
||||
- **Personal preferences:** `~/.claude/CLAUDE.md` (cross-project: no auto-commit, no Co-Authored-By, no @claude)
|
||||
- **Workflow memory:** `~/.claude/projects/<repo>/memory/MEMORY.md` index
|
||||
- **Vault MOCs:** `meta/`, `domains/<area>/<area>-MOC.md`, `decisions/`, `fixes/`, `limitations/`, `code/patterns/`
|
||||
- **DB access:** SSH tunnel `ssh -N gendesign` → `localhost:15432` · Credentials: vault `meta/00_credentials.md`
|
||||
|
||||
## Don't
|
||||
## Don't (cross-domain — specifics → `.claude/rules/`)
|
||||
|
||||
- ❌ `import psycopg2` — только psycopg v3
|
||||
- ❌ Direct push в main / merge без approval
|
||||
- ❌ `--no-verify` / `--amend` / `git push --force`
|
||||
- ❌ `print()` в prod-коде — только `logger.*`
|
||||
- ❌ `requests` — только `httpx`
|
||||
- ❌ Knowledge-файлы вне vault (`docs/research/`, `notes/`, `wiki/`)
|
||||
- ❌ `DROP TABLE` / `TRUNCATE` без явного approval
|
||||
- ❌ Hardcode credentials в коде / коммитах
|
||||
|
||||
---
|
||||
|
||||
**Maintenance:** этот файл обновляется когда меняется tech stack, добавляются новые critical rules, или меняется структура rules/. **Цель: ≤150 lines.** Детали → `.claude/rules/` или vault.
|
||||
<!-- Maintenance: обновляется когда меняется tech stack или critical rules. Цель ≤100 lines. Детали → .claude/rules/ или vault. -->
|
||||
|
|
|
|||
94
scripts/claude-hooks/check-no-print.py
Normal file
94
scripts/claude-hooks/check-no-print.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Claude Code PostToolUse hook — blocks new `print(` calls in backend/**/*.py.
|
||||
|
||||
Wired in `.claude/settings.json` hooks.PostToolUse with matcher "Edit|Write".
|
||||
|
||||
Behavior:
|
||||
- Reads hook input as JSON from stdin (per Anthropic spec).
|
||||
- If tool is Edit/Write and file_path matches backend/**/*.py:
|
||||
- Scans new content for new `print(` calls.
|
||||
- Exits with code 2 to block (stderr is forwarded to Claude as feedback).
|
||||
- Otherwise exits 0 (no-op).
|
||||
|
||||
Notes:
|
||||
- Allows `print(` inside lines containing `# noqa` (escape hatch).
|
||||
- Skips files under `backend/tests/**` (test code may use print for debugging).
|
||||
- Skips `backend/scripts/**` (one-off scripts).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ALLOW_PATH_PREFIXES = (
|
||||
"backend/tests/",
|
||||
"backend/scripts/",
|
||||
)
|
||||
WATCH_PATH_PREFIX = "backend/"
|
||||
PRINT_RE = re.compile(r"(?<!\w)print\s*\(")
|
||||
|
||||
|
||||
def normalize_path(raw: str) -> str:
|
||||
"""Convert absolute/relative path to repo-relative POSIX-like form."""
|
||||
p = Path(raw).as_posix()
|
||||
# Try to strip everything up to the last "backend/" occurrence.
|
||||
idx = p.rfind("backend/")
|
||||
return p[idx:] if idx >= 0 else p
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
payload = json.load(sys.stdin)
|
||||
except Exception:
|
||||
# No input or malformed — skip silently.
|
||||
return 0
|
||||
|
||||
tool_name = payload.get("tool_name") or payload.get("tool") or ""
|
||||
if tool_name not in {"Edit", "Write"}:
|
||||
return 0
|
||||
|
||||
tool_input = payload.get("tool_input") or {}
|
||||
file_path = tool_input.get("file_path") or tool_input.get("path") or ""
|
||||
if not file_path:
|
||||
return 0
|
||||
|
||||
norm = normalize_path(file_path)
|
||||
if not norm.startswith(WATCH_PATH_PREFIX):
|
||||
return 0
|
||||
if any(norm.startswith(p) for p in ALLOW_PATH_PREFIXES):
|
||||
return 0
|
||||
if not norm.endswith(".py"):
|
||||
return 0
|
||||
|
||||
# Pull the new content. Edit gives `new_string`, Write gives `content`.
|
||||
new_text = tool_input.get("new_string") or tool_input.get("content") or ""
|
||||
if not new_text:
|
||||
return 0
|
||||
|
||||
offenders: list[tuple[int, str]] = []
|
||||
for lineno, line in enumerate(new_text.splitlines(), start=1):
|
||||
if "# noqa" in line:
|
||||
continue
|
||||
if PRINT_RE.search(line):
|
||||
offenders.append((lineno, line.strip()))
|
||||
|
||||
if not offenders:
|
||||
return 0
|
||||
|
||||
sample = offenders[0]
|
||||
msg_lines = [
|
||||
f"Blocked: `print(` introduced in {norm}.",
|
||||
f"Line {sample[0]}: {sample[1][:120]}",
|
||||
"GenDesign convention: use `logger.info/warning/error` instead of print().",
|
||||
"If this is intentional one-off debug, add `# noqa` on the line and retry.",
|
||||
]
|
||||
print("\n".join(msg_lines), file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Reference in a new issue