ADR-0004: Linting, Quality, and Security Toolchain¶
- Date: 2025-08-01
- Status: Accepted
- Author: Bijan Mousavi
Context¶
We need a single, reproducible pipeline for code style, formatting, type-safety, complexity, documentation coverage, dead code, dependency hygiene, license compliance, and security checks — identical locally and in CI. Developers should be able to run:
make lint
make quality
make security
and get the same results everywhere.
We standardized on the following tools:
- Ruff for formatting, import sorting, and linting (with auto-fix where safe).
- Mypy and Pytype for static typing (Pytype runs where supported).
- Pyright for fast type checks (editor/CI parity).
- Pydocstyle (Google convention) for docstring style.
- Interrogate for documentation coverage.
- Radon for cyclomatic complexity.
- Vulture for dead code detection.
- Deptry for unused/incorrect dependencies.
- REUSE for SPDX license header compliance.
- Bandit for security static analysis.
- pip-audit for dependency vulnerability audits.
All configuration lives under config/ (with a few root files like REUSE.toml), ensuring CI/local parity.
Decision¶
Makefile Targets¶
We enforce Makefile targets to run the full toolchain consistently.
Lint (Makefile)
# Lint Configuration
RUFF := $(ACT)/ruff
MYPY := $(ACT)/mypy
PYRIGHT := $(ACT)/pyright
PYTYPE := $(ACT)/pytype
LINT_DIRS ?= src/bijux_rag tests
MYPY_DIRS ?= src/bijux_rag
PYRIGHT_DIRS ?= src/bijux_rag
LINT_ARTIFACTS_DIR ?= artifacts/lint
RUFF_CACHE_DIR ?= $(LINT_ARTIFACTS_DIR)/.ruff_cache
MYPY_CACHE_DIR ?= $(LINT_ARTIFACTS_DIR)/.mypy_cache
PYTYPE_OUT_DIR ?= $(LINT_ARTIFACTS_DIR)/pytype
.PHONY: lint lint-artifacts lint-clean fmt type
fmt: | $(VENV)
@mkdir -p "$(LINT_ARTIFACTS_DIR)" "$(RUFF_CACHE_DIR)"
@set -euo pipefail; $(RUFF) format --config config/ruff.toml --cache-dir "$(RUFF_CACHE_DIR)" $(LINT_DIRS)
@set -euo pipefail; $(RUFF) check --config config/ruff.toml --fix --cache-dir "$(RUFF_CACHE_DIR)" $(LINT_DIRS)
@printf "OK\n" > "$(LINT_ARTIFACTS_DIR)/_fmt"
lint: lint-artifacts type
@echo "✔ Linting completed (artifacts in '$(LINT_ARTIFACTS_DIR)')"
lint-artifacts: | $(VENV)
@mkdir -p "$(LINT_ARTIFACTS_DIR)" "$(RUFF_CACHE_DIR)" "$(MYPY_CACHE_DIR)"
@set -euo pipefail; $(RUFF) format --config config/ruff.toml --check --cache-dir "$(RUFF_CACHE_DIR)" $(LINT_DIRS) 2>&1 | tee "$(LINT_ARTIFACTS_DIR)/ruff-format.log"
@set -euo pipefail; $(RUFF) check --config config/ruff.toml --cache-dir "$(RUFF_CACHE_DIR)" $(LINT_DIRS) 2>&1 | tee "$(LINT_ARTIFACTS_DIR)/ruff.log"
@set -euo pipefail; $(MYPY) --config-file config/mypy.ini --cache-dir "$(MYPY_CACHE_DIR)" $(MYPY_DIRS) 2>&1 | tee "$(LINT_ARTIFACTS_DIR)/mypy.log"
@set -euo pipefail; $(PYRIGHT) --project config/pyrightconfig.json --pythonpath src $(PYRIGHT_DIRS) 2>&1 | tee "$(LINT_ARTIFACTS_DIR)/pyright.log"
@mkdir -p "$(PYTYPE_OUT_DIR)"
@set -euo pipefail; $(PYTYPE) --config=config/pytype.cfg --output="$(PYTYPE_OUT_DIR)" src/bijux_rag/boundaries 2>&1 | tee "$(LINT_ARTIFACTS_DIR)/pytype.log"
@[ -d .ruff_cache ] && rm -rf .ruff_cache || true
@[ -d .mypy_cache ] && rm -rf .mypy_cache || true
@printf "OK\n" > "$(LINT_ARTIFACTS_DIR)/_passed"
type: | $(VENV)
@mkdir -p "$(LINT_ARTIFACTS_DIR)" "$(MYPY_CACHE_DIR)"
@set -euo pipefail; $(PYRIGHT) --project config/pyrightconfig.json --pythonpath src $(PYRIGHT_DIRS) 2>&1 | tee "$(LINT_ARTIFACTS_DIR)/pyright.log"
lint-clean:
@echo "→ Cleaning lint artifacts"
@rm -rf "$(LINT_ARTIFACTS_DIR)" .ruff_cache .mypy_cache || true
@echo "✔ done"
##@ Lint
fmt: ## Auto-format with ruff (format + autofix)
lint: ## Run ruff + mypy + pyright (check-only, caches under artifacts/lint)
lint-clean: ## Remove lint artifacts
Quality (Makefile)
# Quality checks (dead code, deps, REUSE, doc coverage)
QUALITY_PATHS ?= src/bijux_rag
INTERROGATE_PATHS ?= src/bijux_rag
QUALITY_ARTIFACTS_DIR ?= artifacts/quality
QUALITY_OK_MARKER := $(QUALITY_ARTIFACTS_DIR)/_passed
VULTURE := $(ACT)/vulture
DEPTRY := $(ACT)/deptry
REUSE := $(ACT)/reuse
INTERROGATE := $(ACT)/interrogate
.PHONY: quality quality-clean interrogate-report
quality:
@echo "→ Quality checks"
@mkdir -p "$(QUALITY_ARTIFACTS_DIR)"
@echo " - Dead code (vulture)"
@$(VULTURE) $(QUALITY_PATHS) --min-confidence 80 2>&1 | tee "$(QUALITY_ARTIFACTS_DIR)/vulture.log"
@echo " - Dependency hygiene (deptry)"
@$(DEPTRY) $(QUALITY_PATHS) 2>&1 | tee "$(QUALITY_ARTIFACTS_DIR)/deptry.log"
@echo " - REUSE compliance"
@$(REUSE) lint 2>&1 | tee "$(QUALITY_ARTIFACTS_DIR)/reuse.log"
@$(MAKE) interrogate-report
@printf "OK\n" >"$(QUALITY_OK_MARKER)"
@echo "✔ Quality complete"
interrogate-report:
@mkdir -p "$(QUALITY_ARTIFACTS_DIR)"
@set +e; OUT="$$( $(INTERROGATE) --verbose $(INTERROGATE_PATHS) )"; rc=$$?; \
printf '%s\n' "$$OUT" >"$(QUALITY_ARTIFACTS_DIR)/interrogate.full.txt"; \
printf '%s\n' "$$OUT" | awk -F'|' 'NR>3 && $$0 ~ /^\|/ {name=$$2; cov=$$6; gsub(/^[ \t]+|[ \t]+$$/,"",name); gsub(/^[ \t]+|[ \t]+$$/,"",cov); if (name !~ /^-+$$/ && cov != "100%") printf(" - %s (%s)\n", name, cov);}' \
>"$(QUALITY_ARTIFACTS_DIR)/interrogate.offenders.txt"; \
exit $$rc
quality-clean:
@echo "→ Cleaning quality artifacts"
@rm -rf "$(QUALITY_ARTIFACTS_DIR)"
##@ Quality
quality: ## Run vulture, deptry, reuse, interrogate (artifacts under artifacts/quality)
quality-clean: ## Remove quality artifacts
Security (Makefile)
# Security checks (Bandit + pip-audit)
SECURITY_PATHS ?= src/bijux_rag
SECURITY_REPORT_DIR ?= artifacts/security
BANDIT := $(ACT)/bandit
PIP_AUDIT := $(ACT)/pip-audit
SECURITY_IGNORE_IDS ?= PYSEC-2022-42969
SECURITY_IGNORE_FLAGS = $(foreach V,$(SECURITY_IGNORE_IDS),--ignore-vuln $(V))
BANDIT_OPTS ?= --severity-level high --confidence-level high -s B101,B311
.PHONY: security security-bandit security-audit security-clean
security: security-bandit security-audit
security-bandit:
@mkdir -p "$(SECURITY_REPORT_DIR)"
@echo "→ Bandit"
@$(BANDIT) $(BANDIT_OPTS) -r "$(SECURITY_PATHS)" -x ".venv,.tox,build,dist,tests" -f json -o "$(SECURITY_REPORT_DIR)/bandit.json"
@$(BANDIT) $(BANDIT_OPTS) -r "$(SECURITY_PATHS)" -x ".venv,.tox,build,dist,tests" 2>&1 | tee "$(SECURITY_REPORT_DIR)/bandit.txt"
security-audit:
@mkdir -p "$(SECURITY_REPORT_DIR)"
@echo "→ pip-audit"
@$(PIP_AUDIT) $(SECURITY_IGNORE_FLAGS) --progress-spinner off --format json -o "$(SECURITY_REPORT_DIR)/pip-audit.json"
@$(PIP_AUDIT) $(SECURITY_IGNORE_FLAGS) --progress-spinner off 2>&1 | tee "$(SECURITY_REPORT_DIR)/pip-audit.txt"
security-clean:
@rm -rf "$(SECURITY_REPORT_DIR)"
##@ Security
security: ## Run Bandit + pip-audit (artifacts under artifacts/security)
security-clean: ## Remove security artifacts
This setup supports whole-project runs as well as per-directory/per-file runs, with reasonable exclusions for generated or template content.
Tool Configurations¶
The toolchain is driven by unified configs:
Ruff (config/ruff.toml)
line-length = 100
target-version = "py311"
extend-exclude = ["build", "dist", ".venv", "node_modules", "artifacts", "src/bijux_rag/_version.py"]
[lint]
select = ["E", "F", "I", "B"]
ignore = ["E501", "B905"]
[format]
quote-style = "double"
indent-style = "space"
Mypy (config/mypy.ini)
[mypy]
python_version = 3.11
mypy_path = src
ignore_missing_imports = True
follow_imports = normal
warn_unused_ignores = True
warn_redundant_casts = True
warn_unreachable = True
strict_optional = True
namespace_packages = True
pretty = True
show_error_codes = True
check_untyped_defs = True
disallow_incomplete_defs = True
disallow_untyped_defs = True
no_implicit_optional = True
warn_return_any = True
[pydantic-mypy]
init_forbid_extra = True
init_typed = True
warn_required_dynamic_aliases = True
warn_untyped_fields = True
Pyright (config/pyrightconfig.json)
{
"venvPath": "..",
"venv": ".venv",
"include": [
"../src/bijux_rag/boundaries"
],
"stubPath": "../typings",
"ignore": [
"../src/bijux_rag/rag",
"../src/bijux_rag/core",
"../src/bijux_rag/fp",
"../src/bijux_rag/result",
"../src/bijux_rag/domain",
"../src/bijux_rag/interop",
"../src/bijux_rag/pipelines",
"../src/bijux_rag/policies",
"../src/bijux_rag/streaming",
"../tests"
],
"exclude": [
"../tests",
"../build",
"../dist",
"../artifacts",
"../node_modules",
"../.venv",
"../.git",
"../src/bijux_rag/__init__.py",
"../src/bijux_rag/tree"
],
"typeCheckingMode": "basic",
"pythonVersion": "3.11",
"reportMissingTypeStubs": "warning",
"reportMissingImports": "warning",
"reportUnknownVariableType": "none",
"reportUnknownParameterType": "none",
"reportUnknownMemberType": "none",
"reportUnknownArgumentType": "none",
"reportUnusedFunction": "none",
"reportPrivateUsage": "none",
"reportUnnecessaryIsInstance": "none",
"useLibraryCodeForTypes": true
}
Deptry (pyproject.toml)
Interrogate (pyproject.toml)
REUSE (REUSE.toml)
version = 1
[[annotations]]
path = [
"**/*.png", "**/*.svg", "**/*.ico", "**/*.gif", "**/*.jpg", "**/*.jpeg",
"**/*.html", "**/*.toml", "**/*.ini", "**/*.cfg", "**/*.conf", "**/*.css",
"**/*.env", "**/*.env.*", "**/*.yaml", "**/*.yml", "**/*.json",
"**/*.cff", "**/.editorconfig", ".gitattributes", ".gitignore",
"artifacts/**"
]
precedence = "override"
SPDX-License-Identifier = "CC0-1.0"
SPDX-FileCopyrightText = "© 2025 Bijan Mousavi"
[[annotations]]
path = ["**/*.md"]
precedence = "closest"
SPDX-License-Identifier = "MIT"
SPDX-FileCopyrightText = "© 2025 Bijan Mousavi"
[[annotations]]
path = ["**/*.py", "**/*.pyi", "**/*.sh", "**/*.mk", "Makefile", "Dockerfile", "Dockerfile.*"]
precedence = "closest"
SPDX-License-Identifier = "MIT"
SPDX-FileCopyrightText = "© 2025 Bijan Mousavi"
Docstring Style Enforcement
We mandate Google-style docstrings via Pydocstyle (enforced in Makefile):
pydocstyle --convention=google path/to/file.py
Interrogate enforces documentation coverage thresholds as configured.
CI Integration¶
make lintruns oversrc/andtests/.make qualityandmake securityrun project-wide.- All Makefile targets are configured to write their reports and logs to the canonical locations defined in ADR-0005.
- Any failure blocks the build; no overrides.
Consequences¶
Pros¶
- Uniform enforcement across the repo; no drift.
- One tool (Ruff) handles formatting, import sorting, and linting with fast auto-fixes.
- Strong typing via Mypy, Pytype (where supported), and Pyright.
- Doc style & coverage enforced via Pydocstyle + Interrogate.
- Maintainability boosted by Vulture (dead code), Deptry (deps), Radon (complexity).
- SPDX compliance via REUSE.
- Security posture improved through Bandit + pip-audit.
- All configs centralized under
config/, ensuring local/CI parity.
Cons¶
- Initial setup and periodic rule maintenance.
- Contributors must align with strict rules and workflow.
Enforcement¶
- Code is accepted only if it passes all configured targets and checks in this ADR.
- Reviewers & CI must reject non-compliant changes (lint, quality, security, or config deviations).
- This policy is binding to preserve the integrity of the toolchain.