Skip to content

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 lint runs over src/ and tests/.
  • make quality and make security run 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.