Skip to content

Commit e201656

Browse files
Berik AshimovBerik Ashimov
authored andcommitted
docs: wave 2 hawkapi doctor spec — one-shot health check CLI with 18 rules
1 parent aa786c6 commit e201656

1 file changed

Lines changed: 198 additions & 0 deletions

File tree

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# Wave 2 — `hawkapi doctor` — design spec
2+
3+
**Status:** Approved — ready for implementation
4+
**Date:** 2026-04-19
5+
**Scope:** Ship `hawkapi doctor app:app` — a one-shot health check that lints a running HawkAPI application for common misconfigurations across security, observability, performance, correctness, and dependency hygiene. Produces human-readable or JSON output, exits non-zero on warn/error findings.
6+
7+
---
8+
9+
## Goal
10+
11+
```bash
12+
$ hawkapi doctor app:app
13+
🩺 hawkapi doctor — app:app
14+
Scanned 42 routes, 8 middleware, 3 plugins
15+
16+
✗ DOC010 CORS allows '*' in production
17+
Fix: whitelist specific origins in CORSMiddleware.
18+
https://hawkapi.ashimov.com/doctor/DOC010
19+
20+
⚠ DOC003 /api/v1/users:POST
21+
No response_model declared; handler returns dict.
22+
Fix: add a msgspec.Struct return annotation.
23+
https://hawkapi.ashimov.com/doctor/DOC003
24+
25+
Summary: 1 errors, 1 warnings, 0 info · exit 1
26+
```
27+
28+
## CLI surface
29+
30+
```
31+
hawkapi doctor <APP_SPEC> [--format={human,json}] [--severity={info,warn,error}] [--fix]
32+
```
33+
34+
- `APP_SPEC``module:attr` form (same as `hawkapi dev`, `hawkapi check`).
35+
- `--format``human` (default, coloured if TTY) or `json` (machine-readable).
36+
- `--severity` — minimum severity to report. Default `info` (everything).
37+
- `--fix` — apply safe, deterministic fixes where a rule supports `auto_fix`. v1: very few rules support this; print `"no auto-fixes available"` when none apply.
38+
- Exit codes: `0` = no findings at the chosen severity or above. `1` = at least one warn. `2` = at least one error.
39+
40+
## Rule architecture
41+
42+
```python
43+
from enum import IntEnum
44+
from dataclasses import dataclass
45+
from typing import Protocol
46+
47+
class Severity(IntEnum):
48+
INFO = 1
49+
WARN = 2
50+
ERROR = 3
51+
52+
@dataclass(frozen=True, slots=True)
53+
class Finding:
54+
rule_id: str
55+
severity: Severity
56+
message: str
57+
fix: str | None = None
58+
location: str | None = None # e.g. "POST /api/v1/users"
59+
docs_url: str | None = None
60+
61+
class Rule(Protocol):
62+
id: str
63+
category: str # security | observability | performance | correctness | deps
64+
severity: Severity # default severity (actual finding can override)
65+
title: str
66+
docs_url: str
67+
def check(self, app: "HawkAPI") -> list[Finding]: ...
68+
```
69+
70+
Rules are discovered from a static list in `src/hawkapi/doctor/rules/__init__.py` (no dynamic plugin discovery in v1 — keep it simple, auditable).
71+
72+
## Rules v1
73+
74+
### Security (5)
75+
- `DOC010` **CORS allows `*` in production.** Detect `CORSMiddleware(allow_origins=["*"])` or equivalent.
76+
- `DOC011` **CSRFMiddleware not installed but state-changing routes exist.** Warns if any POST/PUT/PATCH/DELETE route is registered and CSRFMiddleware is absent.
77+
- `DOC012` **TrustedProxyMiddleware missing behind a known proxy.** Warns if `X-Forwarded-*` headers may be parsed without trust configured.
78+
- `DOC013` **Bearer/OAuth2 auth with hardcoded secrets.** Scan `OAuth2PasswordBearer(tokenUrl=...)` for URL + check Settings/env for obvious placeholders (`changeme`, `secret`, `dev`).
79+
- `DOC014` **HTTPSRedirectMiddleware absent.** Info-level in dev, warn if `ENV=production` env var is set.
80+
81+
### Observability (4)
82+
- `DOC020` **No Request-ID / structured-logging / observability middleware installed.** Warn — one-line fix.
83+
- `DOC021` **No `/metrics` endpoint.** If `PrometheusMiddleware` absent and an observability stack is likely (OTel plugin, Prometheus plugin deps installed), info.
84+
- `DOC022` **No OTel wiring.** If `opentelemetry` is importable but no `hawkapi-otel` plugin registered, info.
85+
- `DOC023` **No Sentry wiring.** If `sentry_sdk` is importable but `hawkapi-sentry` plugin absent, info.
86+
87+
### Performance (4)
88+
- `DOC030` **`debug=True` on HawkAPI constructor in what looks like a production app.** Error if `ENV=production` else info.
89+
- `DOC031` **GZipMiddleware absent and routes return >1 KiB JSON on average.** Warn — static analysis of `response_model` payloads.
90+
- `DOC032` **Handler returning `dict`/`list` primitives without `response_model`.** Warn — auto-inference only catches msgspec/Pydantic types. Bare dict response bypasses filtering.
91+
- `DOC033` **No bulkhead on routes that look like heavy I/O** (handler signature contains `db` / `session` / `http_client` / `redis`). Info.
92+
93+
### Correctness (3)
94+
- `DOC040` **Route handler missing return annotation.** Info — blocks auto-`response_model` inference.
95+
- `DOC041` **Route without docstring or `summary=`.** Info — produces empty OpenAPI summary.
96+
- `DOC042` **Middleware order suspicious.** Warn if `CORSMiddleware` appears after authentication-style middleware (heuristic: any middleware named `*Auth*` / class of `HTTPBearer`-using middleware). Order matters: CORS should run first.
97+
98+
### Dependencies (2)
99+
- `DOC050` **HawkAPI version older than latest published.** Info — read `hawkapi.__version__`, fetch latest from PyPI (best-effort; skip if offline).
100+
- `DOC051` **`msgspec` version < 0.19.** Warn — known perf gap.
101+
102+
### Total: 18 rules. Easy to extend by adding a file under `rules/`.
103+
104+
## Output formats
105+
106+
### Human (default)
107+
- Group by severity (errors first, then warnings, then info).
108+
- Emoji prefix per severity (`` error / `` warn / `` info), coloured when stdout is a TTY (use `rich` if already in deps, else plain ANSI codes).
109+
- Footer summary line with counts and exit code.
110+
111+
### JSON
112+
```json
113+
{
114+
"app": "app:app",
115+
"summary": {"errors": 1, "warnings": 1, "info": 0, "total": 2},
116+
"findings": [
117+
{"rule_id": "DOC010", "severity": "error", "message": "...", "fix": "...", "location": null, "docs_url": "..."}
118+
]
119+
}
120+
```
121+
122+
## `--fix` mode (v1)
123+
124+
Applies only to rules that declare `auto_fix: Callable[[HawkAPI], bool]`. v1 ships zero `auto_fix` implementations — print `"--fix: no auto-fixable findings"` and exit per severity rule. The infrastructure is there, but each rule must opt in carefully (most need user judgement).
125+
126+
## Module layout
127+
128+
```
129+
src/hawkapi/doctor/
130+
__init__.py # re-exports Rule, Finding, Severity, run()
131+
_runner.py # orchestration: load app, run rules, filter, format, exit
132+
_formatter.py # human + json output
133+
_types.py # Rule Protocol, Finding, Severity
134+
rules/
135+
__init__.py # ALL_RULES = [...] static list
136+
security.py # DOC010–DOC014
137+
observability.py # DOC020–DOC023
138+
performance.py # DOC030–DOC033
139+
correctness.py # DOC040–DOC042
140+
deps.py # DOC050–DOC051
141+
142+
src/hawkapi/cli.py
143+
+doctor subcommand wiring
144+
```
145+
146+
Each file < 200 lines.
147+
148+
## Tests — `tests/unit/test_doctor.py`
149+
150+
~25 tests. For each rule: one happy-path (clean app → no finding), one failing-path (misconfigured app → finding). Plus:
151+
- `_runner.run(app)` returns findings list.
152+
- `--format=json` output shape.
153+
- `--severity=warn` filters info.
154+
- Exit code mapping.
155+
- CLI smoke test (`hawkapi doctor app:app` via `subprocess`).
156+
157+
## Docs — `docs/guide/doctor.md`
158+
159+
- Overview + usage.
160+
- Full rule reference table (ID, category, severity, description, fix).
161+
- `--fix` caveat.
162+
- CI integration example (one-liner GitHub Actions step).
163+
164+
## Mkdocs nav + CHANGELOG
165+
166+
- `mkdocs.yml`: `- Doctor: guide/doctor.md` after "OpenAPI linter".
167+
- `CHANGELOG.md`: `[Unreleased] ### Added` bullet → target v0.1.4.
168+
- README.md: add a short "Doctor" subsection under "CLI" or "Production Features".
169+
170+
## Out of scope (v2+)
171+
172+
- Dynamic rule discovery (entry points / plugins).
173+
- `--fix` with actual auto-fixes (needs per-rule careful design).
174+
- Runtime traffic analysis (would require proxy/sniffer).
175+
- Config-file output (`.hawkapi-doctor.toml` for rule overrides).
176+
- IDE integration (SARIF/LSP) — could be a follow-up.
177+
178+
## Success criteria
179+
180+
1. `hawkapi doctor app:app` runs all 18 rules on a real HawkAPI app.
181+
2. Exit codes map: clean → 0, warn → 1, error → 2.
182+
3. `--format=json` produces stable schema.
183+
4. CHANGELOG + docs + mkdocs nav entry present and mkdocs strict-clean.
184+
5. Full suite + ruff + pyright-strict clean.
185+
186+
## Files touched
187+
188+
- `src/hawkapi/doctor/**` — new (10 files)
189+
- `src/hawkapi/cli.py` — add `doctor` subcommand
190+
- `tests/unit/test_doctor.py` — new
191+
- `docs/guide/doctor.md` — new
192+
- `mkdocs.yml` — nav entry
193+
- `CHANGELOG.md` — bullet
194+
- `README.md` — add Doctor subsection
195+
196+
## Rollback
197+
198+
New module + new CLI subcommand + new docs. No existing paths change. Revert = delete `doctor/` package, remove one argparse subparser, revert three doc diffs.

0 commit comments

Comments
 (0)