diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 827759e..a469742 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -46,6 +46,9 @@ jobs: - name: Check formatting run: cargo fmt --all --check + - name: Check Rust code health + run: scripts/check-rust-code-health.sh + - name: Run clippy run: cargo clippy --workspace --all-targets --all-features --jobs "$CARGO_BUILD_JOBS" -- -D warnings diff --git a/docs/rust-code-health.md b/docs/rust-code-health.md new file mode 100644 index 0000000..3f43387 --- /dev/null +++ b/docs/rust-code-health.md @@ -0,0 +1,90 @@ +# Rust Code Health + +## Цель + +В Community-репозитории нужно удерживать Rust-код в состоянии, где модули можно безопасно менять без разрастания скрытых связей. + +Основные риски сейчас: + +- крупные production-файлы с тестами внутри; +- интеграционные сценарии, написанные рядом с кодом; +- смешение HTTP, registry, runtime и test fixtures в одном модуле; +- отсутствие автоматического контроля размера файлов. + +## Инструменты + +В Rust нет одного прямого аналога Python `import-linter`, который закрывает cohesion, coupling и connascence на уровне workspace. + +Используемая база: + +- `cargo fmt` — единый стиль. +- `cargo clippy --workspace --all-targets --all-features -- -D warnings` — локальные ошибки, smell-и и часть API-антипаттернов. +- `cargo test --workspace --all-targets` — unit и integration-style тесты. +- `scripts/check-rust-code-health.sh` — локальный repo-level gate для размера Rust-файлов. + +Что можно добавить позже: + +- `cargo-deny` для лицензий, security advisories и duplicate dependencies. +- `cargo-machete` для поиска неиспользуемых зависимостей. +- `cargo-udeps` для более строгой проверки зависимостей, если nightly допустим в отдельной job. +- `cargo-modules` или `cargo-guppy` для анализа графа модулей и зависимостей между crate-ами. + +## Правила размера + +Новые Rust-файлы не должны быть больше `1000` строк. + +Существующие крупные файлы зафиксированы baseline-ом в `scripts/check-rust-code-health.sh`. Они не должны расти дальше. При изменении таких файлов предпочтительно: + +- выносить вложенный `mod tests` в `tests/`, если тест проверяет публичное поведение, БД, HTTP или несколько модулей сразу; +- выделять route/service/runtime helper в отдельный модуль; +- оставлять рядом с production-кодом только короткие unit-тесты приватной логики. + +Текущие крупные файлы: + +- `apps/admin-api/src/service.rs` +- `apps/admin-api/src/app.rs` +- `apps/mcp-server/src/main.rs` +- `crates/crank-runtime/src/executor.rs` +- `crates/crank-registry/src/postgres/mod.rs` +- `crates/crank-community-mcp/src/app.rs` +- `crates/crank-registry/src/postgres/operation.rs` + +## Правила тестов + +Рядом с кодом допустимы: + +- маленькие pure unit tests; +- проверки приватных parser/mapper/helper-функций; +- тесты без сети, БД и поднятых серверов. + +В `tests/` нужно выносить: + +- PostgreSQL integration tests; +- admin-api HTTP сценарии; +- fake upstream servers; +- publish/test-run/YAML roundtrip flow; +- MCP Streamable HTTP сценарии. + +Целевая структура: + +```text +apps/admin-api/tests/ +crates/crank-registry/tests/ +crates/crank-runtime/tests/ +crates/crank-community-mcp/tests/ +``` + +## Архитектурные границы + +Базовое правило направления зависимостей: + +```text +apps/* -> crates/* +crank-community-mcp -> crank-registry + crank-runtime + crank-core +crank-runtime -> crank-core + crank-mapping + crank-schema + adapters +crank-registry -> crank-core +crank-adapter-rest -> crank-core + crank-mapping + crank-schema +crank-core -> no app crates +``` + +Если потребуется строгая автоматическая проверка границ, добавим отдельный script на основе `cargo metadata` или `cargo-guppy`. diff --git a/scripts/README.md b/scripts/README.md index d3c8324..d465aca 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -59,3 +59,15 @@ shell env file. - `REGISTRY_USERNAME -> DEPLOY_REGISTRY_USER` - `REGISTRY_PASSWORD -> DEPLOY_REGISTRY_TOKEN` + +## `check-rust-code-health.sh` + +Проверяет базовые правила сопровождаемости Rust-кода: + +- новые `.rs` файлы не больше 1000 строк; +- существующие крупные файлы не должны расти сверх текущего baseline; +- крупные inline test modules подсвечиваются как debt. + +```bash +./scripts/check-rust-code-health.sh +``` diff --git a/scripts/check-rust-code-health.sh b/scripts/check-rust-code-health.sh new file mode 100755 index 0000000..ca23e5b --- /dev/null +++ b/scripts/check-rust-code-health.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +DEFAULT_MAX_LINES=1000 + +# Existing debt baseline. New files must stay under DEFAULT_MAX_LINES. These +# files are allowed to exist, but CI fails if they grow further. +declare -A FILE_LIMITS=( + ["apps/admin-api/src/service.rs"]=4112 + ["apps/admin-api/src/app.rs"]=3823 + ["apps/mcp-server/src/main.rs"]=3505 + ["crates/crank-runtime/src/executor.rs"]=2829 + ["crates/crank-registry/src/postgres/mod.rs"]=2154 + ["crates/crank-community-mcp/src/app.rs"]=1647 + ["crates/crank-registry/src/postgres/operation.rs"]=1022 +) + +status=0 + +echo "Rust code health: checking file size limits" + +while IFS= read -r file; do + rel="${file#"$ROOT_DIR"/}" + lines="$(wc -l < "$file" | tr -d ' ')" + limit="${FILE_LIMITS[$rel]:-$DEFAULT_MAX_LINES}" + + if (( lines > limit )); then + echo "error: $rel has $lines lines; limit is $limit" >&2 + status=1 + fi +done < <( + find "$ROOT_DIR" \ + -path "$ROOT_DIR/target" -prune -o \ + -path "$ROOT_DIR/apps/ui/node_modules" -prune -o \ + -name '*.rs' -print | sort +) + +echo "Rust code health: checking new large inline test modules" + +while IFS=: read -r file line _; do + rel="${file#"$ROOT_DIR"/}" + if [[ -n "${FILE_LIMITS[$rel]:-}" ]]; then + continue + fi + lines="$(wc -l < "$file" | tr -d ' ')" + if (( lines > DEFAULT_MAX_LINES / 2 )); then + echo "warning: $rel contains inline tests and has $lines lines; prefer tests/ for integration-style tests" >&2 + fi +done < <( + grep -RIn --include='*.rs' '^[[:space:]]*mod tests[[:space:]]*{' \ + "$ROOT_DIR/apps" "$ROOT_DIR/crates" || true +) + +if (( status != 0 )); then + cat >&2 <<'EOF' + +Rust code health failed. + +Rules: +- new Rust files must stay at or below 1000 lines; +- existing large files are pinned to the current baseline and must not grow; +- when touching a large file, prefer extracting modules or moving integration-style tests to tests/. + +EOF +fi + +exit "$status"