From 104f24ad697d2fbe8cadd9c2b4f739067ac46f12 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sat, 7 Mar 2026 22:00:25 +0000 Subject: [PATCH] Define and enforce coding standards and style guide (closes #15) --- .editorconfig | 2 +- CHANGELOG.md | 12 +++ CLAUDE.md | 2 +- CONTRIBUTING.md | 93 +++++++++++++++++++++++ architecture/adrs/009-coding-standards.md | 67 ++++++++++++++++ architecture/adrs/README.md | 1 + backend/pyproject.toml | 59 ++++++++++++++ docs/CODING_STANDARDS.md | 2 +- 8 files changed, 235 insertions(+), 3 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 architecture/adrs/009-coding-standards.md create mode 100644 backend/pyproject.toml diff --git a/.editorconfig b/.editorconfig index 4047ae1a..b5c49b1c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,7 +10,7 @@ insert_final_newline = true [*.py] indent_size = 4 -max_line_length = 99 +max_line_length = 100 [*.{ts,tsx,js,jsx,json,css,scss,html}] indent_size = 2 diff --git a/CHANGELOG.md b/CHANGELOG.md index a08f06da..491f2676 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.0] - 2026-03-07 + +### Added + +- `backend/pyproject.toml` with ruff (line length 100, Python 3.12, security/typing/style rules) and mypy strict config +- `CONTRIBUTING.md` documenting coding standards, architecture rules, branch strategy, and testing conventions +- ADR-009: Coding Standards and Tooling + +### Changed + +- Line length updated from 99 to 100 in CODING_STANDARDS.md, .editorconfig, and CLAUDE.md + ## [0.4.0] - 2026-03-07 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index ff5b43b6..77a17b9e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,7 +29,7 @@ docs/ # Design documentation **Read `docs/CODING_STANDARDS.md` before writing any code.** Key rules: ### Python -- Formatter/Linter: `ruff` (line length 99) +- Formatter/Linter: `ruff` (line length 100) - Type checker: `mypy --strict` - All public functions require type annotations - Google-style docstrings on public API boundaries only diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..44b69355 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,93 @@ +# Contributing to Ground Control + +## Getting Started + +1. Fork and clone the repository +2. Create a feature branch from `dev`: `git checkout -b feature/your-feature` +3. Make your changes +4. Push and open a PR against `dev` + +## Branch Strategy + +- `main` — production-ready, protected +- `dev` — integration branch, all PRs target this +- `feature/*` — feature branches, branched from `dev` + +## Coding Standards + +Read [`docs/CODING_STANDARDS.md`](docs/CODING_STANDARDS.md) for the complete reference. Key points below. + +### Python Backend + +| Tool | Purpose | Command | +|------|---------|---------| +| `ruff check` | Linting | `cd backend && ruff check src/` | +| `ruff format` | Formatting | `cd backend && ruff format src/` | +| `mypy` | Type checking | `cd backend && mypy src/` | +| `pytest` | Testing | `cd backend && pytest` | + +- **Line length**: 100 +- **Type hints**: Required on all public functions and methods +- **Docstrings**: Google style, on public API boundaries only +- **Imports**: stdlib, third-party, local (enforced by ruff) +- **No `Any`** unless unavoidable (comment why) +- **No `print()`** — use `structlog` + +### TypeScript Frontend + +| Tool | Purpose | Command | +|------|---------|---------| +| `biome check` | Lint + format | `cd frontend && npx biome check src/` | +| `tsc` | Type checking | `cd frontend && npx tsc --noEmit` | + +- **Strict mode**: `strict: true` in `tsconfig.json` +- **No `any`** — use `unknown` and narrow + +### Naming Conventions (Python) + +| Element | Convention | Example | +|---------|-----------|---------| +| Modules | `snake_case` | `risk_service.py` | +| Classes | `PascalCase` | `RiskService` | +| Functions/methods | `snake_case` | `create_risk()` | +| Constants | `UPPER_SNAKE_CASE` | `MAX_RETRY_COUNT` | +| Private | `_leading_underscore` | `_validate_score()` | + +## Architecture Rules + +The dependency rule is enforced by `import-linter` in CI: + +``` +api/ -> domain/ <- infrastructure/ +``` + +- `domain/` has **zero** framework imports (no FastAPI, no SQLAlchemy) +- `api/` depends on `domain/` and `schemas/` — never imports `infrastructure/` +- `infrastructure/` implements interfaces defined in `domain/` + +See [ADR-008](architecture/adrs/008-clean-architecture.md) for rationale. + +## Commit Messages + +- Imperative mood: `Add risk scoring engine` not `Added risk scoring engine` +- Every commit updates `CHANGELOG.md` + +## Pull Requests + +- Target `dev`, not `main` +- PRs require passing CI (lint + typecheck + tests + import-linter) +- No coverage regression +- Use the [PR template](.github/PULL_REQUEST_TEMPLATE.md) + +## Testing + +``` +tests/ +├── unit/ # Domain logic only. No DB, no HTTP. +├── integration/ # With real PostgreSQL (testcontainers). +└── e2e/ # Playwright, full stack. +``` + +- Test names describe behavior: `test_create_risk_fails_when_likelihood_out_of_range` +- Coverage minimums: 80% domain, 70% api/infrastructure +- Tests are independent — no shared mutable state diff --git a/architecture/adrs/009-coding-standards.md b/architecture/adrs/009-coding-standards.md new file mode 100644 index 00000000..f7bedecd --- /dev/null +++ b/architecture/adrs/009-coding-standards.md @@ -0,0 +1,67 @@ +# ADR-009: Coding Standards and Tooling + +## Status + +Accepted + +## Date + +2026-03-07 + +## Context + +Ground Control is a multi-language monorepo (Python backend, TypeScript frontend). Without enforced coding standards, style drift and inconsistent patterns will slow development and increase bugs. We need automated tooling that enforces standards in CI and at commit time, not just documentation. + +## Decision + +### Python Backend + +- **Linter and formatter**: `ruff` (replaces flake8, isort, black in a single binary) + - Line length: 100 + - Target: Python 3.12 + - Rule sets: pycodestyle (E), pyflakes (F), isort (I), pep8-naming (N), bugbear (B), annotations (ANN), security (S), pydocstyle (D), pyupgrade (UP) +- **Type checker**: `mypy` with `strict = true` + - Disallow untyped defs, no implicit optional +- **Docstrings**: Google style convention, required on public API boundaries only +- **Import ordering**: stdlib, third-party, local (enforced by ruff isort rules) + +### TypeScript Frontend + +- **Linter and formatter**: `biome` (replaces ESLint + Prettier in a single binary) +- **Type checker**: `tsc --strict` +- **No `any` types** — use `unknown` and narrow + +### Shared + +- `.editorconfig` for consistent whitespace (4 spaces Python, 2 spaces TS/YAML) +- Pre-commit hooks run ruff, mypy, and gitleaks before every commit +- CI pipeline enforces all checks — failures block merge + +### Naming Conventions (Python) + +| Element | Convention | +|---------|-----------| +| Modules | `snake_case` | +| Classes | `PascalCase` | +| Functions/methods | `snake_case` | +| Constants | `UPPER_SNAKE_CASE` | +| Private | `_leading_underscore` | + +## Consequences + +### Positive + +- Single tool per language (ruff, biome) reduces config complexity and speeds up checks +- Strict type checking catches bugs before runtime +- Automated enforcement means standards are not optional +- Consistent style reduces cognitive load during code review + +### Negative + +- Strict mypy can require verbose type annotations in some cases +- Developers must learn ruff/biome if unfamiliar (mitigated: both are fast and well-documented) +- Some valid code patterns are rejected by strict rules (use targeted ignores with justification) + +### Risks + +- Tool upgrades may introduce new lint rules that break CI (mitigated: pin versions in pre-commit and pyproject.toml) diff --git a/architecture/adrs/README.md b/architecture/adrs/README.md index 24b70d9d..fbc64595 100644 --- a/architecture/adrs/README.md +++ b/architecture/adrs/README.md @@ -30,3 +30,4 @@ We use [MADR](https://adr.github.io/madr/) (Markdown Any Decision Records). Each | [006](006-multi-tenancy-strategy.md) | Multi-Tenancy Strategy (Shared Schema Default) | Accepted | | [007](007-agent-first-design.md) | Agent-First Design (AI Agents as First-Class Actors) | Accepted | | [008](008-clean-architecture.md) | Clean Architecture (API / Domain / Infrastructure) | Accepted | +| [009](009-coding-standards.md) | Coding Standards and Tooling | Accepted | diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 00000000..2590dbe7 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,59 @@ +[project] +name = "ground-control" +version = "0.1.0" +description = "Open IT Risk Management Platform" +requires-python = ">=3.12" +license = "MIT" + +[tool.ruff] +target-version = "py312" +line-length = 100 +src = ["src"] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "F", # pyflakes + "I", # isort + "N", # pep8-naming + "B", # flake8-bugbear + "ANN", # flake8-annotations + "S", # flake8-bandit (security) + "D", # pydocstyle + "UP", # pyupgrade + "RUF", # ruff-specific rules +] +ignore = [ + "D100", # missing docstring in public module + "D104", # missing docstring in public package + "D106", # missing docstring in public nested class + "D203", # one-blank-line-before-class (conflicts with D211) + "D213", # multi-line-summary-second-line (conflicts with D212) + "ANN101", # missing type annotation for self + "ANN102", # missing type annotation for cls +] + +[tool.ruff.lint.isort] +known-first-party = ["ground_control"] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.lint.per-file-ignores] +"tests/**/*.py" = ["S101", "ANN", "D"] + +[tool.mypy] +python_version = "3.12" +strict = true +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +no_implicit_optional = true +check_untyped_defs = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_functions = ["test_*"] +addopts = "-ra -q --strict-markers" diff --git a/docs/CODING_STANDARDS.md b/docs/CODING_STANDARDS.md index 7c923ca2..8987a15f 100644 --- a/docs/CODING_STANDARDS.md +++ b/docs/CODING_STANDARDS.md @@ -301,7 +301,7 @@ Every error response includes `request_id` for correlation. - Linter: `ruff check` - Type checker: `mypy --strict` - All functions have type annotations. No `Any` unless unavoidable (and commented why). -- Line length: 99 characters. +- Line length: 100 characters. - Imports: sorted by `ruff` (isort-compatible). - Docstrings: only on public API boundaries (use case functions, service classes). Not on every method. Code should be self-explanatory.