Old CLAUDE.md was 700 lines — exceeded Anthropic 200-line recommendation,
adherence degraded under context pressure.
Refactor per research findings:
- CLAUDE.md 700 -> 97 lines (essential only, pointers to rules)
- .claude/rules/{backend,frontend,sql,git-pr,deploy}.md (NEW, path-scoped)
- ~/.claude/CLAUDE.md (NEW, 21 lines) — personal cross-project prefs
- memory/ — deleted 12 feedback files promoted to proper layer (CLAUDE.md
rules table, .claude/rules/, ~/.claude/CLAUDE.md). 7 remaining = workflow
details.
- .gitignore: !.claude/agents/, !.claude/rules/ exceptions
Total auto-loaded per session: 97 (CLAUDE.md) + 21 (~/.claude) = 118 lines
vs prior 700. Rules in .claude/rules/ load only when matching paths touched.
Resolves conflict no_self_review vs pre_push_review: now single rule in
git-pr.md Review workflow section (pre-push = local lint; post-push =
external Claude window posting as lekss361).
Code-reviewer feedback applied:
- paths: frontmatter added to git-pr.md
- Auto-merge scope blocked-list now includes .claude/rules/*.md changes
to prevent self-extending rules without human approval.
Co-authored-by: lekss361 <claudestars@proton.me>
81 lines
3.4 KiB
Markdown
81 lines
3.4 KiB
Markdown
---
|
||
paths: backend/**/*.py
|
||
---
|
||
|
||
# Backend conventions — Python 3.12 / FastAPI
|
||
|
||
## psycopg v3 (CRITICAL — recurring bug class)
|
||
|
||
- `import psycopg2` → **ModuleNotFoundError** — его нет в зависимостях. Только `psycopg[binary]>=3.2.0`.
|
||
- Bulk INSERT: `cur.executemany()` или COPY — НЕ `execute_values` (это psycopg2 API).
|
||
- В SQL `text(...)`: всегда `CAST(:name AS type)` — НИКОГДА `:name::type`.
|
||
- SQLAlchemy 2.0 + psycopg3 **игнорирует** `::type` после `:name` — `psycopg.errors.SyntaxError: syntax error at or near ':'`
|
||
- Исключение: `ARRAY[:rc]::int[]` РАБОТАЕТ — `::` приклеено к `)`, не к bind-name
|
||
- Pre-PR check: `grep -nE ':[a-z_]+::[a-z]' backend/app` → должен быть пуст
|
||
- Reference: vault `Pattern_CAST_AS_Type`, `Bug_SQLAlchemy_DoubleColon_Cast`
|
||
|
||
## SAVEPOINT pattern (loop upserts)
|
||
|
||
В цикле INSERT/UPSERT — **обязательно** `with db.begin_nested():` per-row:
|
||
|
||
```python
|
||
for row in items:
|
||
try:
|
||
with db.begin_nested():
|
||
db.execute(text("INSERT INTO ..."), {...})
|
||
except Exception as e:
|
||
logger.warning("row failed: %s", e)
|
||
```
|
||
|
||
❌ Bare `db.rollback()` в loop — откатывает **всю outer tx**, счётчики `inserted`/`updated` продолжают расти → inconsistent state.
|
||
|
||
Reference: vault `Bug_Pzz_Loader_Missing_Savepoint_May14`, `domrf_kn.py:427/452/472`.
|
||
|
||
## SQL injection prevention
|
||
|
||
❌ **Никогда**:
|
||
- f-string в SQL: `text(f"INSERT ... VALUES ({values})")`
|
||
- Home-rolled quote escape: `c.replace(chr(39), chr(39)*2)`
|
||
- String concat: `"... WHERE id = " + str(id)`
|
||
|
||
✅ **Canonical**: parametrized batch через SQLAlchemy `text` + list of dicts:
|
||
|
||
```python
|
||
db.execute(
|
||
text("INSERT INTO targets (job_id, cad_num) VALUES (:job_id, :cad_num) "
|
||
"ON CONFLICT (job_id, cad_num) DO NOTHING"),
|
||
[{"job_id": j, "cad_num": c} for c in cad_nums],
|
||
)
|
||
```
|
||
|
||
Reference: vault `Bug_Nspd_Geo_Sql_Injection_May14`.
|
||
|
||
## Ruff / code style
|
||
|
||
- **Line length 100** (`backend/pyproject.toml`)
|
||
- Типичные ловушки E501:
|
||
- SQL-литералы в `text("""...""")` — переноси после `FROM` / `WHERE` / `AND`
|
||
- `logger.info("...", a, b, c, d)` — разбивай на 2 строки
|
||
- Длинная сигнатура — аргументы по одному
|
||
- Ruff rules: `E F I B UP N RUF ASYNC` (RUF001/002/003 игнор — кириллица OK)
|
||
- mypy `strict`: `app.services.generative.*` + `app.services.site_finder.scorer`
|
||
- target-version `py312` — `dict | None`, `list[int]`, walrus OK
|
||
|
||
## Async / Celery
|
||
|
||
- FastAPI handlers: `async def`
|
||
- Celery tasks: `def` (Celery sync) — НЕ `async def` внутри task
|
||
- Sync↔async bridge через `asyncio.run()` внутри Celery task
|
||
- Tests: pytest + `asyncio_mode = auto`
|
||
|
||
## Запреты
|
||
|
||
- ❌ `print()` — только `logger.info/warning/error`
|
||
- ❌ `import requests` — только `httpx`
|
||
- ❌ `except Exception: pass` без re-raise или `logger.exception`
|
||
- ❌ Hardcode credentials — `os.environ.get("KEY")` или `settings.KEY`
|
||
- ❌ `Any` без комментария почему
|
||
|
||
## Worker crash checklist
|
||
|
||
Worker падает на старте → скорее всего import error: `pyproject.toml` имеет dep но `uv.lock` не обновлён → `cd backend && uv lock` → commit lock → push.
|