diff --git a/.github/workflows/code_standards.yml b/.github/workflows/code_standards.yml new file mode 100644 index 000000000..acc2176ee --- /dev/null +++ b/.github/workflows/code_standards.yml @@ -0,0 +1,54 @@ +name: Code Standards + +# Python Code Standards Compliance Framework (code-standards/). +# The framework itself is enforced strictly (examples + compliance tests must +# pass). The framework validator is also run against src/ in ADVISORY mode so +# the tiered report is visible without blocking merges on the existing baseline. + +on: + push: + branches: + - main + pull_request: + +jobs: + + code-standards: + runs-on: ubuntu-latest + defaults: + run: + shell: bash -l {0} + + steps: + + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + + - name: Set up Python + run: uv python install 3.11 + + - name: Install dependencies + run: uv sync --frozen --extra dev + + - name: Install ruff (framework tooling) + run: uv pip install ruff + + # --- Strict: the framework's own artifacts must be compliant --- + - name: Compliant example is formatted + run: uv run ruff format --config code-standards/ruff.toml --check code-standards/examples/compliant.py + + - name: Compliant example passes analysis + run: uv run ruff check --config code-standards/ruff.toml code-standards/examples/compliant.py + + - name: Compliance tests + run: uv run pytest code-standards/tests/ -v + + # --- Advisory: report the framework's findings on the existing codebase --- + - name: Framework report on src/ (advisory) + continue-on-error: true + run: ./code-standards/scripts/validate.sh src/ diff --git a/code-standards/README.md b/code-standards/README.md new file mode 100644 index 000000000..be8358490 --- /dev/null +++ b/code-standards/README.md @@ -0,0 +1,150 @@ +# Python Code Standards Compliance Framework + +> **Drop-in ruff + pyright configuration for safety-critical Python development with Windsurf AI.** +> +> Framework version **1.0** — the Python port of +> [`COG-GTM/-C-Code-Standards-Compliance-Framework`](https://github.com/COG-GTM/-C-Code-Standards-Compliance-Framework), +> using the same patterns, severity model, and rule-ID numbering. + +--- + +## What Is This? + +A **complete, ready-to-use code standards enforcement system** for Python. It +combines: + +1. **ruff format** — automatic code formatting (consistent style; Black-compatible) +2. **ruff check** — static analysis (bug + security detection) +3. **pyright** — IDE intelligence (type diagnostics, autocomplete, go-to-def) +4. **Severity Classification** — triage issues as Critical / Major / Minor +5. **Windsurf AI Rules** — intelligent code review with fix suggestions + +It is the direct analog of the C/C++ framework. The mapping: + +| Framework role | C/C++ tool | Python tool | Config file | +|----------------|-----------|-------------|-------------| +| Formatting | clang-format | **ruff format** | `ruff.toml` `[format]` | +| Static analysis | clang-tidy | **ruff check** | `ruff.toml` `[lint]` | +| Language server | clangd | **pyright** | `pyrightconfig.json` | + +## Severity Classification + +| Severity | Icon | Rule IDs | Action | Blocks Merge? | Exit code | +|----------|------|----------|--------|---------------|-----------| +| **Critical** | 🔴 | 20–29 | Must fix immediately | Yes | 1 | +| **Major** | 🟡 | 30–39 | Requires review | Recommended | 0 | +| **Minor** | 🟢 | 40–49 | Fix when convenient | No | 0 | + +The **Critical gate** is enforced by `validate.sh` running `ruff check` restricted +to the critical rule codes (the analog of clang-tidy's `WarningsAsErrors`). See +[`rule-severity-mapping.yaml`](rule-severity-mapping.yaml) for the full mapping. + +## Rule Reference (summary) + +| Critical (🔴) | Major (🟡) | Minor (🟢) | +|--------------|-----------|-----------| +| 20 No swallowed errors | 30 Unsafe comparisons | 40 Formatting | +| 21 No injection | 31 Signature/docstring consistency | 41 Naming | +| 22 Guard None | 32 No dead code | 42 Explicit comparisons | +| 23 No resource leaks | 33 Control-flow errors | 43 Simplify booleans | +| 24 No mutable defaults | 34 Concurrency safety | 44 No else-after-return | +| 25 No use-before-assign | 35 Performance | 45 Modern syntax | +| 26 No insecure APIs | | 46 No unused imports/args | + +Full detail with bad/good examples: [`docs/rule-reference.md`](docs/rule-reference.md). + +## Quick Start + +```bash +# 1. Install ruff (and optionally pyright) +pip install ruff # or: uv add --dev ruff + +# 2. Validate a directory (format + tiered analysis) +./code-standards/scripts/validate.sh src/ + +# 3. Auto-fix formatting + safe lint fixes +./code-standards/scripts/validate.sh src/ --fix +``` + +Or invoke ruff directly with the framework config: + +```bash +ruff format --config code-standards/ruff.toml --check src/ +ruff check --config code-standards/ruff.toml src/ +``` + +## Repository Contents + +``` +code-standards/ +├── README.md # This documentation +├── ruff.toml # Formatting + static-analysis config +├── pyrightconfig.json # Language-server config (clangd analog) +├── rule-severity-mapping.yaml # Severity classification (version 1.0) +├── vscode-settings.json.template # VSCode/Windsurf editor settings +├── windsurf/ +│ └── python-safety-critical-rules.md # Windsurf AI rules (trigger: always_on) +├── examples/ +│ ├── compliant.py # Passes all checks +│ └── violations.py # Intentional violations (one per rule) +├── scripts/ +│ └── validate.sh # Validation wrapper (tiered exit codes) +├── tests/ +│ ├── __init__.py +│ └── test_compliance.py # Automated tests for the configs +└── docs/ + ├── rule-reference.md # Detailed rule documentation + └── setup.md # Setup / IDE integration guide +``` + +## CI/CD Integration + +Add a job that runs the validator (fails on Critical + format): + +```yaml +name: Code Standards +on: [push, pull_request] +jobs: + standards: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v4 + - run: uv sync --frozen --extra dev + - run: uv run ruff format --config code-standards/ruff.toml --check src/ + - run: ./code-standards/scripts/validate.sh src/ + - run: uv run pytest code-standards/tests/ -v +``` + +## Windsurf AI Integration + +```bash +mkdir -p .windsurf/rules +cp code-standards/windsurf/python-safety-critical-rules.md .windsurf/rules/ +``` + +Cascade then detects violations, classifies them by severity, reports them with +the Rule ID (e.g. "🔴 Critical - Rule 20 violation"), explains why, and suggests +a compliant fix. + +## Adopting Repo-Wide + +This framework is intentionally **additive and non-breaking**: `ruff.toml` uses +`line-length = 100` so `ruff format` output matches the repo's existing Black +config, and it is only applied when invoked with `--config`. To make it the +project's primary tooling, copy the `ruff.toml` settings into a root `ruff.toml` +or `[tool.ruff]` in `pyproject.toml`, and `pyrightconfig.json` to the root. See +[`docs/setup.md`](docs/setup.md). + +## Requirements + +| Tool | Minimum | Purpose | +|------|---------|---------| +| ruff | 0.6+ | Formatting + static analysis | +| pyright | 1.1+ (optional) | IDE type intelligence | +| Python | 3.10+ | Running the project/tests | +| pytest, PyYAML | — | Running compliance tests | + +## License + +Apache-2.0 (matches the AgentLab project license). diff --git a/code-standards/docs/rule-reference.md b/code-standards/docs/rule-reference.md new file mode 100644 index 000000000..3459caf01 --- /dev/null +++ b/code-standards/docs/rule-reference.md @@ -0,0 +1,187 @@ +# Rule Reference (Python) + +Complete reference for all code standards rules. This is the Python +instantiation of the Code Standards Compliance Framework; rule IDs and +severities match the source C/C++ framework so findings are comparable across +languages. + +## Overview + +Rules are organized by severity: +- **Critical (Rule 20–29):** Safety/security issues that must be fixed +- **Major (Rule 30–39):** Quality issues that should be reviewed +- **Minor (Rule 40–49):** Style issues that improve maintainability + +Each rule lists the ruff/pyright checks that enforce it. + +--- + +## Critical Rules (Block Merge) + +### Rule 20: Do Not Silently Swallow Errors +**Severity:** 🔴 Critical   **Checks:** `E722`, `BLE001` + +Never use a bare `except:` or blind `except Exception: pass`. + +```python +# ❌ BAD +try: + risky() +except: + pass + +# ✅ GOOD +try: + risky() +except ValueError as err: + logger.error("risky failed: %s", err) + raise +``` + +### Rule 21: Prevent Injection / Unsafe Input +**Severity:** 🔴 Critical   **Checks:** `S602`, `S603`, `S604`, `S605`, `S608` + +```python +# ❌ BAD +subprocess.run(f"ls {user_input}", shell=True) +# ✅ GOOD +subprocess.run(["ls", user_input], shell=False) +``` + +### Rule 22: Guard Optional / None Values +**Severity:** 🔴 Critical   **Checks:** `reportOptionalMemberAccess`, `reportOptionalSubscript` (pyright) + +```python +# ❌ BAD +value = data.get("key") +return value.strip() +# ✅ GOOD +value = data.get("key") +if value is None: + return None +return value.strip() +``` + +### Rule 23: Prevent Resource Leaks +**Severity:** 🔴 Critical   **Checks:** `SIM115` + +```python +# ❌ BAD +f = open(path) +data = f.read() +# ✅ GOOD +with open(path) as f: + data = f.read() +``` + +### Rule 24: Avoid Dangerous Shared Mutable State +**Severity:** 🔴 Critical   **Checks:** `B006`, `B008` + +```python +# ❌ BAD +def add(item, bucket=[]): + bucket.append(item) +# ✅ GOOD +def add(item, bucket=None): + if bucket is None: + bucket = [] + bucket.append(item) +``` + +### Rule 25: No Undefined / Use-Before-Assignment +**Severity:** 🔴 Critical   **Checks:** `F821`, `F822`, `F823`, `reportUnboundVariable` + +```python +# ❌ BAD +if cond: + count = 10 +return count +# ✅ GOOD +count = 0 +if cond: + count = 10 +return count +``` + +### Rule 26: Avoid Insecure APIs +**Severity:** 🔴 Critical   **Checks:** `S102`, `S307`, `S301`, `S506`, `S324`, `S311` + +| Insecure | Secure | +|----------|--------| +| `eval`/`exec` | `ast.literal_eval` / explicit parsing | +| `pickle.loads(untrusted)` | `json.loads` | +| `yaml.load` | `yaml.safe_load` | +| `random` (secrets) | `secrets` | +| `md5`/`sha1` (security) | `sha256` | + +--- + +## Major Rules (Requires Review) + +### Rule 30: Avoid Unsafe Comparisons / Conversions +**Severity:** 🟡 Major   **Checks:** `E721`, `F632` + +### Rule 31: Consistent Signatures & Docstrings +**Severity:** 🟡 Major   **Checks:** `darglint` + +### Rule 32: No Redundant / Dead Code +**Severity:** 🟡 Major   **Checks:** `F811`, `F841` + +### Rule 33: Prevent Control-Flow Errors +**Severity:** 🟡 Major   **Checks:** `F701`, `F702`, `F706` + +### Rule 34: Concurrency / Shared-State Safety +**Severity:** 🟡 Major   **Checks:** `B006` + +### Rule 35: Performance Issues +**Severity:** 🟡 Major   **Checks:** `PERF*`, `C4*` + +--- + +## Minor Rules (Style) + +### Rule 40: Consistent Formatting +**Severity:** 🟢 Minor   **Enforced by:** `ruff format` (`ruff.toml`) + +### Rule 41: Naming Conventions +**Severity:** 🟢 Minor   **Checks:** `N` (pep8-naming) + +| Kind | Convention | Example | +|------|------------|---------| +| Functions | `snake_case` | `process_data()` | +| Variables | `snake_case` | `buffer_size` | +| Constants | `UPPER_CASE` | `MAX_SIZE` | +| Classes | `PascalCase` | `DataBuffer` | + +### Rule 42: Explicit Comparisons +**Severity:** 🟢 Minor   **Checks:** `E711`, `E712`, `E713`, `E714` + +### Rule 43: Simplify Boolean Expressions +**Severity:** 🟢 Minor   **Checks:** `SIM` + +### Rule 44: Avoid Else After Return +**Severity:** 🟢 Minor   **Checks:** `RET505`, `RET506`, `RET507`, `RET508` + +### Rule 45: Use Modern Syntax +**Severity:** 🟢 Minor   **Checks:** `UP` (pyupgrade) + +### Rule 46: Remove Unused Imports / Arguments +**Severity:** 🟢 Minor   **Checks:** `F401`, `ARG` + +--- + +## Suppression Guide + +```python +result = risky_call() # noqa: BLE001 (explain why) +``` + +File-level: add `# ruff: noqa: ` at the top of the file. Always document +why suppression is needed; prefer fixing over suppressing. + +--- + +## References +- [ruff rules](https://docs.astral.sh/ruff/rules/) +- [pyright configuration](https://microsoft.github.io/pyright/#/configuration) +- [CWE](https://cwe.mitre.org/) · [OWASP Top 10](https://owasp.org/www-project-top-ten/) diff --git a/code-standards/docs/setup.md b/code-standards/docs/setup.md new file mode 100644 index 000000000..383b0e93d --- /dev/null +++ b/code-standards/docs/setup.md @@ -0,0 +1,79 @@ +# Setup Guide + +How to enable the Python Code Standards Compliance Framework in a project. This +is the Python analog of the source framework's `docs/clangd-setup.md`. + +## What You Need (3 Things) + +| Component | What It Is | Where to Get It | +|-----------|-----------|-----------------| +| **1. ruff** | Formatter + static analyzer (the clang-format + clang-tidy analog) | `pip install ruff` or `uv add --dev ruff` | +| **2. pyright / Pylance** | Language server for IDE intelligence (the clangd analog) | Bundled with the VSCode/Windsurf Python extension, or `pip install pyright` | +| **3. This framework** | Pre-configured rules & severity mapping | `code-standards/` in this repo | + +## Step 1: Install the tools + +```bash +pip install ruff +# optional CLI type checker +pip install pyright +``` + +Or, in a `uv`-managed repo (like AgentLab): + +```bash +uv add --dev ruff +uv run ruff --version +``` + +## Step 2: Validate your code + +```bash +# One-shot validation of src/ (format + tiered analysis) +./code-standards/scripts/validate.sh src/ + +# Auto-fix formatting + safe lint fixes +./code-standards/scripts/validate.sh src/ --fix + +# Or invoke ruff directly with the framework config +ruff format --config code-standards/ruff.toml --check src/ +ruff check --config code-standards/ruff.toml src/ +``` + +Exit codes from `validate.sh`: `0` pass, `1` format, `2` Critical lint (block +merge), `3` both. + +## Step 3: Wire up your editor + +```bash +mkdir -p .vscode +cp code-standards/vscode-settings.json.template .vscode/settings.json +``` + +This makes Windsurf/VSCode: +- format on save with `ruff format` (Rule 40), +- show ruff diagnostics in real time (Rules 20–46), +- run pyright type analysis (Rules 22, 25, 30, 32). + +## Step 4 (optional): Windsurf AI reviewer + +```bash +mkdir -p .windsurf/rules +cp code-standards/windsurf/python-safety-critical-rules.md .windsurf/rules/ +``` + +Cascade will then enforce the rules and report violations by Rule ID + severity. + +## Step 5 (optional): Adopt the config repo-wide + +To make ruff the project's real formatter/linter (rather than only via +`--config`), copy the settings from `code-standards/ruff.toml` into a root +`ruff.toml` or a `[tool.ruff]` block in `pyproject.toml`, and copy +`pyrightconfig.json` to the repo root. Keep `line-length = 100` so it stays +compatible with an existing Black setup. + +## Verify + +```bash +pytest code-standards/tests/test_compliance.py -v +``` diff --git a/code-standards/examples/compliant.py b/code-standards/examples/compliant.py new file mode 100644 index 000000000..dcdf95627 --- /dev/null +++ b/code-standards/examples/compliant.py @@ -0,0 +1,129 @@ +"""Compliant example. + +Every construct here passes the Code Standards Compliance Framework checks +(``ruff check`` + ``ruff format`` with ``code-standards/ruff.toml``). It is the +Python analog of ``examples/compliant.c`` in the source framework. +""" + +import json +import secrets +import subprocess + +MAX_ITEMS = 100 # Rule 41: constants are UPPER_CASE + + +def read_config(path): + """Read a JSON config file safely. + + Args: + path: Path to the JSON file. + + Returns: + The parsed config dict, or None on failure. + """ + # Rule 23: context manager; Rule 20: specific exception handling. + try: + with open(path, encoding="utf-8") as handle: + return json.load(handle) + except (OSError, json.JSONDecodeError) as err: + print(f"Failed to read config {path}: {err}") + return None + + +def get_name(record): + """Return a record's name, guarding against missing values. + + Args: + record: A mapping that may contain a ``name`` key. + + Returns: + The stripped name, or an empty string if absent. + """ + # Rule 22: guard the optional value before use. + value = record.get("name") + if value is None: + return "" + return value.strip() + + +def accumulate(item, bucket=None): + """Append an item to a bucket without a mutable default. + + Args: + item: The item to append. + bucket: An optional existing list to append to. + + Returns: + The updated list. + """ + # Rule 24: avoid the mutable default argument trap. + if bucket is None: + bucket = [] + bucket.append(item) + return bucket + + +def double_all(items): + """Double every element. + + Args: + items: An iterable of numbers. + + Returns: + A list with each element doubled. + """ + # Rule 35: comprehension instead of manual append loop. + return [x * 2 for x in items] + + +def make_token(): + """Return a cryptographically secure token. + + Returns: + A hex token string. + """ + # Rule 26: use the secrets module, not random. + return secrets.token_hex(16) + + +def run_listing(directory): + """List a directory without a shell. + + Args: + directory: The directory to list. + + Returns: + The completed process. + """ + # Rule 21: argument list, no shell. + return subprocess.run(["ls", directory], shell=False, check=True) + + +def classify(value): + """Classify a value's sign. + + Args: + value: The number to classify. + + Returns: + -1, 0, or 1. + """ + # Rule 44: no else after return. + if value < 0: + return -1 + if value > 0: + return 1 + return 0 + + +def main(): + """Entry point demonstrating compliant usage.""" + config = read_config("config.json") + if config is None: + config = {} + bucket = accumulate(1) + print(get_name(config), double_all(bucket), make_token(), classify(-5)) + + +if __name__ == "__main__": + main() diff --git a/code-standards/examples/violations.py b/code-standards/examples/violations.py new file mode 100644 index 000000000..bda4e166e --- /dev/null +++ b/code-standards/examples/violations.py @@ -0,0 +1,125 @@ +"""Examples of code violations for testing ruff / pyright detection. + +WARNING: This file intentionally contains violations! DO NOT use in production. + +Run: ``ruff check --config code-standards/ruff.toml code-standards/examples/violations.py`` +to see the detected issues. Each function demonstrates one rule violation and +documents the expected ruff/pyright code(s). Python analog of ``violations.c``. +""" + +import hashlib +import pickle +import random +import subprocess + +# ========================================================================== +# CRITICAL VIOLATIONS (Rule 20-26) - should block merges in CI/CD +# ========================================================================== + + +def rule_20_violation_bare_except(): + """Rule 20 VIOLATION: swallowed error. Expected: E722.""" + try: + risky = 1 / 0 + return risky + except: # noqa is intentionally omitted so the violation is reported + pass + + +def rule_21_violation_shell_injection(user_input): + """Rule 21 VIOLATION: shell injection. Expected: S602/S605.""" + return subprocess.run(f"ls {user_input}", shell=True) # noqa: S602 is omitted intentionally + + +def rule_23_violation_resource_leak(path): + """Rule 23 VIOLATION: unclosed file. Expected: SIM115.""" + handle = open(path) + return handle.read() + + +def rule_24_violation_mutable_default(item, bucket=[]): + """Rule 24 VIOLATION: mutable default argument. Expected: B006.""" + bucket.append(item) + return bucket + + +def rule_25_violation_use_before_assignment(condition): + """Rule 25 VIOLATION: possibly-unbound variable. Expected: F821/pyright.""" + if condition: + count = 10 + return count # noqa: F821 omitted intentionally + + +def rule_26_violation_insecure_apis(untrusted): + """Rule 26 VIOLATION: insecure APIs. Expected: S311, S301, S324.""" + token = random.random() + obj = pickle.loads(untrusted) + digest = hashlib.md5(b"data").hexdigest() + return token, obj, digest + + +# ========================================================================== +# MAJOR VIOLATIONS (Rule 30-35) - require review +# ========================================================================== + + +def rule_30_violation_type_comparison(x): + """Rule 30 VIOLATION: type comparison / literal identity. Expected: E721, F632.""" + if type(x) == int: + return x is "int" # noqa: F632 omitted intentionally + return None + + +def rule_32_violation_dead_code(): + """Rule 32 VIOLATION: unused local variable. Expected: F841.""" + result = compute_something() + return 42 + + +def rule_35_violation_performance(items): + """Rule 35 VIOLATION: manual list build. Expected: PERF401.""" + out = [] + for x in items: + out.append(x * 2) + return out + + +# ========================================================================== +# MINOR VIOLATIONS (Rule 40-46) - style warnings only +# ========================================================================== + + +def RuleFortyOneBadName(BadParam): + """Rule 41 VIOLATION: non-snake_case naming. Expected: N802, N803.""" + return BadParam + + +def rule_42_violation_none_comparison(x): + """Rule 42 VIOLATION: comparison to None with ==. Expected: E711.""" + if x == None: + return True + return False + + +def rule_44_violation_else_after_return(error): + """Rule 44 VIOLATION: else after return. Expected: RET505.""" + if error: + return -1 + else: + return 0 + + +def rule_45_violation_old_syntax(name): + """Rule 45 VIOLATION: legacy syntax. Expected: UP.*.""" + mapping = dict() + return "%s" % name, mapping + + +def rule_46_violation_unused_arg(used, unused): + """Rule 46 VIOLATION: unused argument. Expected: ARG001.""" + return used + + +def compute_something(): + """Helper so rule_32 references a defined name.""" + return 0 diff --git a/code-standards/pyrightconfig.json b/code-standards/pyrightconfig.json new file mode 100644 index 000000000..d19ffe2de --- /dev/null +++ b/code-standards/pyrightconfig.json @@ -0,0 +1,56 @@ +{ + // ========================================================================== + // Python Language-Server Configuration (Pyright) + // -------------------------------------------------------------------------- + // Python equivalent of the framework's `.clangd` file. It configures the + // language server that powers IDE intelligence in Windsurf/VSCode: + // autocomplete, go-to-definition, hover, and real-time type diagnostics -- + // the analog of clangd's real-time squiggles. + // Copy to the project root (as pyrightconfig.json) to activate, or point your + // editor's Python analysis at it. pyrightconfig.json supports JSONC comments. + // ========================================================================== + + "include": [ + "src" + ], + "exclude": [ + "**/__pycache__", + "**/.venv", + "**/build", + "**/dist", + "**/node_modules", + "code-standards/examples/violations.py" + ], + + "pythonVersion": "3.10", + "pythonPlatform": "All", + + // Real-time analysis level. 'basic' keeps signal high and noise low, + // matching the framework's philosophy of actionable checks. + "typeCheckingMode": "basic", + + // Rule 22: Optional / None access is the Python analog of null-dereference. + "reportOptionalMemberAccess": "warning", + "reportOptionalSubscript": "warning", + "reportOptionalCall": "warning", + "reportOptionalIterable": "warning", + "reportOptionalOperand": "warning", + + // Rule 25: use-before-definition / unbound variables. + "reportUnboundVariable": "error", + "reportUndefinedVariable": "error", + + // Rule 32: redundant / dead code. + "reportUnusedImport": "warning", + "reportUnusedVariable": "warning", + "reportUnnecessaryComparison": "warning", + "reportUnreachable": "warning", + + // Rule 30: type-safety issues (narrowing analog). + "reportGeneralTypeIssues": "warning", + "reportArgumentType": "warning", + + "useLibraryCodeForTypes": true, + "reportMissingImports": "none", + "reportMissingModuleSource": "none" +} diff --git a/code-standards/ruff.toml b/code-standards/ruff.toml new file mode 100644 index 000000000..92a69d33c --- /dev/null +++ b/code-standards/ruff.toml @@ -0,0 +1,109 @@ +# ============================================================================= +# Python Code Standards - Formatting + Static Analysis Configuration +# ============================================================================= +# Reference: https://docs.astral.sh/ruff/settings/ +# +# This single file is the Python equivalent of the framework's `.clang-format` +# (formatting) AND `.clang-tidy` (static analysis) configs. Ruff performs both +# roles: `ruff format` handles style, `ruff check` handles bug/security analysis. +# +# To use (non-destructively, against a target dir): +# ruff format --config code-standards/ruff.toml --check src/ +# ruff check --config code-standards/ruff.toml src/ +# Or via the wrapper: +# ./code-standards/scripts/validate.sh src/ +# +# This configuration enables safety-critical checks including: +# - flake8-bandit security guidelines (insecure APIs, injection) +# - Undefined / use-before-assignment detection +# - Resource-leak (unclosed file) detection +# - Naming, redundancy, performance, and modernization checks +# ============================================================================= + +# --- Formatting (Rule 40) --- kept Black-compatible (line-length 100) so this +# framework does NOT fight the repo's existing Black-based CI. +line-length = 100 +target-version = "py310" + +# Match the repo's existing exclusions. +extend-exclude = [ + ".eggs", + ".git", + ".mypy_cache", + ".nox", + ".tox", + ".venv", + "_build", + "build", + "dist", +] + +[format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +[lint] +# ============================================================================= +# ENABLED CHECKS - grouped by severity tier (see rule-severity-mapping.yaml) +# ============================================================================= +select = [ + # ---- Critical (Rule 20-26): safety / security ---- + "E722", # Rule 20: bare except (hides failures) + "BLE001", # Rule 20: blind except Exception + "S", # Rule 21 & 26: flake8-bandit (injection, eval/exec, pickle, + # yaml.load, weak hashes, subprocess shell, insecure random) + "SIM115", # Rule 23: open() without a context manager (resource leak) + "B006", # Rule 24: mutable default argument + "B008", # Rule 24: function call in default argument + "F821", # Rule 25: undefined name + "F822", # Rule 25: undefined name in __all__ + "F823", # Rule 25: local variable referenced before assignment + + # ---- Major (Rule 30-35): quality / potential bugs ---- + "E721", # Rule 30: type comparison with == instead of is + "F632", # Rule 30: use of == to compare constant literals + "F811", # Rule 32: redefinition of unused name + "F841", # Rule 32: local variable assigned but never used + "F701", # Rule 33: break outside loop + "F702", # Rule 33: continue outside loop + "F706", # Rule 33: return outside function + "PERF", # Rule 35: perflint performance checks + "C4", # Rule 35: flake8-comprehensions + + # ---- Minor (Rule 40-46): style / maintainability ---- + "N", # Rule 41: pep8-naming + "E711", # Rule 42: comparison to None should use `is` + "E712", # Rule 42: comparison to True/False should use `is`/truthiness + "E713", # Rule 42: test for membership should be `not in` + "E714", # Rule 42: test for object identity should be `is not` + "SIM", # Rule 43: flake8-simplify (boolean/branch simplification) + "RET505", # Rule 44: superfluous else after return + "RET506", # Rule 44: superfluous else after raise + "RET507", # Rule 44: superfluous else after continue + "RET508", # Rule 44: superfluous else after break + "UP", # Rule 45: pyupgrade (modern syntax - the "use nullptr" analog) + "F401", # Rule 46: unused import + "ARG", # Rule 46: unused function/method argument +] + +# Noisy checks intentionally disabled (mirrors the C framework disabling +# easily-swappable-parameters, magic-numbers, cognitive-complexity, etc.) +ignore = [ + "S101", # assert used - common and acceptable in tests + "S603", # subprocess-without-shell-equals-true - fires on ALL subprocess use (low signal) + "S607", # start-process-with-partial-path - low signal; Rule 21 targets shell=True (S602/S604/S605) + "SIM108", # ternary-instead-of-if - stylistic, too noisy + "PERF203", # try-except in loop - often unavoidable +] + +[lint.per-file-ignores] +# Test files may use asserts, fixtures, and looser conventions. +"**/tests/**" = ["S101", "ARG", "N802", "N803"] +# NOTE: examples/violations.py is intentionally NOT ignored -- validate.sh and +# the compliance tests rely on it producing findings. + +[lint.flake8-bandit] +# Rule 26: treat these as the insecure-API surface. +check-typed-exception = true diff --git a/code-standards/rule-severity-mapping.yaml b/code-standards/rule-severity-mapping.yaml new file mode 100644 index 000000000..db7d92a91 --- /dev/null +++ b/code-standards/rule-severity-mapping.yaml @@ -0,0 +1,277 @@ +# ============================================================================= +# Rule Severity Mapping (Python port) +# ============================================================================= +# Maps ruff / pyright checks to severity levels for reporting and CI/CD +# decisions. This is the Python instantiation of the Code Standards Compliance +# Framework -- it preserves the framework's severity model, rule-ID numbering +# (Critical 20-29, Major 30-39, Minor 40-49) and schema; only the underlying +# `checks:` change from clang-tidy check names to ruff/pyright codes. +# +# Severity Levels: +# - critical: Block merge, must fix immediately (safety/security) +# - major: Requires review, should fix before release (potential bugs) +# - minor: Style/preference, fix when convenient (maintainability) +# +# Usage: +# - CI/CD can parse this file to determine pass/fail criteria +# - Windsurf rules reference these rule IDs +# - Reports can group issues by severity +# ============================================================================= + +version: "1.0" +last_updated: "2026-07-01" +language: "python" +tools: + format: "ruff format" + analysis: "ruff check" + language_server: "pyright" + +severity_levels: + + # =========================================================================== + # CRITICAL - Block merge, potential safety/security issues + # =========================================================================== + critical: + description: "Must fix before merge - safety or security risk" + action: "block_merge" + exit_code: 1 + rules: + + - rule_id: "Rule 20" + name: "Do not silently swallow errors" + rationale: "Bare/blind except hides failures, leading to undefined behavior (C analog: unchecked return values)" + checks: + - E722 # bare-except + - BLE001 # blind-except + examples: + bad: "try:\n risky()\nexcept:\n pass" + good: "try:\n risky()\nexcept ValueError as err:\n logger.error(\"risky failed: %s\", err)\n raise" + + - rule_id: "Rule 21" + name: "Prevent injection / unsafe input handling" + rationale: "Untrusted input flowing into shells/SQL/templates is the #1 source of security vulnerabilities (C analog: buffer overflow)" + checks: + - S602 # subprocess with shell=True + - S603 # subprocess without shell check + - S604 # call with shell=True + - S605 # start process with a shell + - S608 # hardcoded SQL expression + examples: + bad: "subprocess.run(f\"ls {user_input}\", shell=True)" + good: "subprocess.run([\"ls\", user_input], shell=False)" + + - rule_id: "Rule 22" + name: "Guard optional / None values before use" + rationale: "Accessing attributes/items on None raises AttributeError/TypeError at runtime (C analog: null dereference)" + checks: + - reportOptionalMemberAccess # pyright + - reportOptionalSubscript # pyright + examples: + bad: "value = data.get(\"k\")\nreturn value.strip()" + good: "value = data.get(\"k\")\nif value is None:\n return None\nreturn value.strip()" + + - rule_id: "Rule 23" + name: "Prevent resource leaks" + rationale: "Files/sockets not closed leak descriptors and can crash long-running systems (C analog: malloc leak)" + checks: + - SIM115 # open-file-with-context-handler + examples: + bad: "f = open(path)\ndata = f.read()" + good: "with open(path) as f:\n data = f.read()" + + - rule_id: "Rule 24" + name: "Avoid dangerous shared mutable state" + rationale: "Mutable default arguments are shared across calls, causing subtle state-corruption bugs (C analog: use-after-free)" + checks: + - B006 # mutable-argument-default + - B008 # function-call-in-default-argument + examples: + bad: "def add(item, bucket=[]):\n bucket.append(item)" + good: "def add(item, bucket=None):\n if bucket is None:\n bucket = []\n bucket.append(item)" + + - rule_id: "Rule 25" + name: "No undefined / use-before-assignment" + rationale: "Referencing names that may be unbound causes NameError/UnboundLocalError at runtime (C analog: uninitialized memory)" + checks: + - F821 # undefined-name + - F822 # undefined-name-in-all + - F823 # local-variable-referenced-before-assignment + - reportUnboundVariable # pyright + examples: + bad: "if cond:\n count = 10\nreturn count" + good: "count = 0\nif cond:\n count = 10\nreturn count" + + - rule_id: "Rule 26" + name: "Avoid insecure APIs" + rationale: "eval/exec/pickle/yaml.load/weak-hash/insecure-random enable code execution and crypto weaknesses (C analog: rand/strcpy)" + checks: + - S102 # exec-builtin + - S307 # suspicious-eval-usage + - S301 # suspicious-pickle-usage + - S506 # unsafe-yaml-load + - S324 # hashlib-insecure-hash-function + - S311 # suspicious-non-cryptographic-random-usage + examples: + bad: "token = random.random()\nobj = pickle.loads(untrusted)" + good: "token = secrets.token_hex(16)\nobj = json.loads(untrusted)" + + # =========================================================================== + # MAJOR - Requires review, potential bugs + # =========================================================================== + major: + description: "Requires review - potential bugs or quality issues" + action: "require_review" + exit_code: 0 + rules: + + - rule_id: "Rule 30" + name: "Avoid unsafe comparisons / conversions" + rationale: "Comparing types with == or literals with `is` produces surprising results (C analog: narrowing conversions)" + checks: + - E721 # type-comparison + - F632 # is-literal + examples: + bad: "if type(x) == int:\n ...\nif name is \"root\":\n ..." + good: "if isinstance(x, int):\n ...\nif name == \"root\":\n ..." + + - rule_id: "Rule 31" + name: "Consistent signatures & docstrings" + rationale: "Docstring params that drift from the signature confuse callers (C analog: inconsistent parameter names)" + checks: + - darglint # docstring/signature consistency (existing repo tool) + examples: + bad: "def f(count):\n \"\"\"Do it.\n\n Args:\n n: the number.\n \"\"\"" + good: "def f(count):\n \"\"\"Do it.\n\n Args:\n count: the number.\n \"\"\"" + + - rule_id: "Rule 32" + name: "Avoid redundant / dead code" + rationale: "Redefinitions and unused locals indicate logic errors or copy-paste mistakes" + checks: + - F811 # redefinition-of-unused + - F841 # unused-variable + examples: + bad: "result = compute()\nreturn other" + good: "return other" + + - rule_id: "Rule 33" + name: "Prevent control-flow errors" + rationale: "break/continue/return outside their valid context is a structural bug (C analog: infinite loops)" + checks: + - F701 # break-outside-loop + - F702 # continue-outside-loop + - F706 # return-outside-function + examples: + bad: "def f():\n x = 1\n break" + good: "for x in items:\n if done:\n break" + + - rule_id: "Rule 34" + name: "Concurrency / shared-state safety" + rationale: "Shared mutable defaults and globals cause data races in concurrent code" + checks: + - B006 # mutable-argument-default + examples: + bad: "def worker(cache={}):\n ..." + good: "def worker(cache=None):\n cache = {} if cache is None else cache" + + - rule_id: "Rule 35" + name: "Performance issues" + rationale: "Unnecessary copies, list-in-comprehension, and inefficient loops waste CPU and memory" + checks: + - PERF401 # manual-list-comprehension + - PERF402 # manual-list-copy + - C400 # unnecessary-generator-list + - C401 # unnecessary-generator-set + examples: + bad: "out = []\nfor x in items:\n out.append(x * 2)" + good: "out = [x * 2 for x in items]" + + # =========================================================================== + # MINOR - Style/maintainability warnings + # =========================================================================== + minor: + description: "Style or maintainability - fix when convenient" + action: "warn_only" + exit_code: 0 + rules: + + - rule_id: "Rule 40" + name: "Consistent formatting" + rationale: "Consistent style improves readability and reduces merge conflicts" + checks: + - "ruff-format" + enforced_by: "ruff.toml ([format], line-length 100, Black-compatible)" + + - rule_id: "Rule 41" + name: "Naming conventions" + rationale: "Consistent naming makes code more predictable and searchable" + checks: + - N # pep8-naming + conventions: + functions: "snake_case" + variables: "snake_case" + constants: "UPPER_CASE" + classes: "PascalCase" + + - rule_id: "Rule 42" + name: "Explicit comparisons" + rationale: "Comparing to None/True/False with == is error-prone (C analog: always use braces / no implicit)" + checks: + - E711 # none-comparison + - E712 # true-false-comparison + - E713 # not-in-test + - E714 # not-is-test + examples: + bad: "if x == None:\n ..." + good: "if x is None:\n ..." + + - rule_id: "Rule 43" + name: "Simplify boolean expressions" + rationale: "Complex boolean expressions are error-prone and hard to read" + checks: + - SIM # flake8-simplify + examples: + bad: "if flag == True:\n ..." + good: "if flag:\n ..." + + - rule_id: "Rule 44" + name: "Avoid else after return" + rationale: "Unnecessary else after return/raise increases indentation" + checks: + - RET505 # superfluous-else-return + - RET506 # superfluous-else-raise + - RET507 # superfluous-else-continue + - RET508 # superfluous-else-break + examples: + bad: "if error:\n return -1\nelse:\n process()" + good: "if error:\n return -1\nprocess()" + + - rule_id: "Rule 45" + name: "Use modern syntax" + rationale: "Modern idioms are clearer and safer (Python analog of the C++ 'use nullptr' rule)" + checks: + - UP # pyupgrade + examples: + bad: "d = dict()\ns = \"%s\" % name" + good: "d = {}\ns = f\"{name}\"" + + - rule_id: "Rule 46" + name: "Remove unused imports / arguments" + rationale: "Unused imports and parameters indicate dead code or missing implementation" + checks: + - F401 # unused-import + - ARG # flake8-unused-arguments + examples: + bad: "import os\n\ndef process(x, unused):\n return x" + good: "def process(x):\n return x" + +# ============================================================================= +# SUPPRESSION GUIDANCE +# ============================================================================= +suppression: + single_line: "# noqa: " + block: "not supported by ruff - use per-line # noqa or per-file-ignores in ruff.toml" + file_level: "# ruff: noqa: (at top of file)" + notes: + - "Always add a comment explaining why the suppression is needed" + - "Prefer fixing the issue over suppressing it" + - "Suppressions should be reviewed in code review" diff --git a/code-standards/scripts/validate.sh b/code-standards/scripts/validate.sh new file mode 100755 index 000000000..7e0a24be6 --- /dev/null +++ b/code-standards/scripts/validate.sh @@ -0,0 +1,170 @@ +#!/bin/bash +# ============================================================================= +# Python Code Standards Validation Script +# ============================================================================= +# Python port of the framework's scripts/validate.sh. Wraps `ruff format` and +# `ruff check` and preserves the framework's tiered exit-code contract. +# +# Usage: ./code-standards/scripts/validate.sh [directory] [--fix] +# +# Arguments: +# directory Target directory to validate (default: src/) +# --fix Apply automatic fixes (formatting + safe lint fixes) +# +# Exit codes: +# 0 - All checks passed +# 1 - Format violations found +# 2 - Critical lint violations found (block merge) +# 3 - Both format and critical violations found +# +# Examples: +# ./code-standards/scripts/validate.sh # Check src/ +# ./code-standards/scripts/validate.sh src/agentlab # Check a subdir +# ./code-standards/scripts/validate.sh src/ --fix # Auto-fix +# ============================================================================= + +set -uo pipefail + +# Colors +RED='\033[0;31m' +YELLOW='\033[0;33m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +BOLD='\033[1m' +NC='\033[0m' + +# Paths +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FRAMEWORK_ROOT="$(dirname "$SCRIPT_DIR")" +CONFIG="$FRAMEWORK_ROOT/ruff.toml" + +# Critical lint rule codes (the "block merge" gate). Mirrors the C framework's +# clang-tidy `WarningsAsErrors` list -- only these promote to a failing exit. +# A curated flake8-bandit subset is used (not bare `S`) so noisy, low-signal +# checks (S603/S607) are not re-enabled by the CLI --select override. +CRITICAL_CODES="E722,BLE001,S102,S301,S302,S307,S311,S324,S506,S602,S604,S605,S608,S609,SIM115,B006,B008,F821,F822,F823" + +# Parse arguments +TARGET_DIR="src" +FIX_MODE=false +for arg in "$@"; do + case $arg in + --fix) FIX_MODE=true ;; + *) [[ -d "$arg" ]] && TARGET_DIR="$arg" ;; + esac +done + +# Resolve the ruff invocation (prefer `uv run ruff`, fall back to `ruff`). +if command -v ruff &> /dev/null; then + RUFF=(ruff) +elif command -v uv &> /dev/null; then + RUFF=(uv run ruff) +else + echo -e "${RED}ruff not found. Install with: pip install ruff${NC}" + exit 4 +fi + +print_header() { + echo "" + echo -e "${BOLD}${BLUE}======================================================${NC}" + echo -e "${BOLD}${BLUE} $1${NC}" + echo -e "${BOLD}${BLUE}======================================================${NC}" + echo "" +} +print_section() { echo ""; echo -e "${BOLD}--- $1 ---${NC}"; echo ""; } +print_pass() { echo -e "${GREEN}✓ PASS${NC}: $1"; } +print_fail() { echo -e "${RED}✗ FAIL${NC}: $1"; } +print_warn() { echo -e "${YELLOW}⚠ WARN${NC}: $1"; } +print_info() { echo -e "${BLUE}ℹ INFO${NC}: $1"; } + +print_header "Python Code Standards Validation" +echo "Configuration:" +echo " Target directory: $TARGET_DIR" +echo " Fix mode: $FIX_MODE" +echo " Config: $CONFIG" + +if [ ! -d "$TARGET_DIR" ]; then + print_warn "Target directory '$TARGET_DIR' not found." + exit 0 +fi + +FORMAT_ERRORS=0 +CRITICAL_ERRORS=0 +ADVISORY_WARNINGS=0 + +# ============================================================================= +# STEP 1: Format check (Rule 40) +# ============================================================================= +print_section "ruff format check" +if $FIX_MODE; then + if "${RUFF[@]}" format --config "$CONFIG" "$TARGET_DIR"; then + print_pass "Formatting applied" + else + print_fail "Formatting failed" + FORMAT_ERRORS=1 + fi +else + if "${RUFF[@]}" format --config "$CONFIG" --check "$TARGET_DIR"; then + print_pass "All files properly formatted" + else + print_fail "Some files need formatting (run with --fix)" + FORMAT_ERRORS=1 + fi +fi + +# ============================================================================= +# STEP 2a: Critical static analysis (block merge) +# ============================================================================= +print_section "ruff check - Critical (block merge)" +if $FIX_MODE; then + "${RUFF[@]}" check --config "$CONFIG" --select "$CRITICAL_CODES" --fix "$TARGET_DIR" || true +fi +if "${RUFF[@]}" check --config "$CONFIG" --select "$CRITICAL_CODES" "$TARGET_DIR"; then + print_pass "No Critical violations" +else + print_fail "Critical violations found (🔴 must fix before merge)" + CRITICAL_ERRORS=1 +fi + +# ============================================================================= +# STEP 2b: Major / Minor static analysis (advisory) +# ============================================================================= +print_section "ruff check - Major / Minor (advisory)" +if "${RUFF[@]}" check --config "$CONFIG" "$TARGET_DIR" > /dev/null 2>&1; then + print_pass "No Major/Minor violations" +else + print_warn "Major/Minor violations found (🟡/🟢 review recommended)" + "${RUFF[@]}" check --config "$CONFIG" --statistics "$TARGET_DIR" 2>/dev/null || true + ADVISORY_WARNINGS=1 +fi + +# ============================================================================= +# SUMMARY +# ============================================================================= +print_header "Summary" +EXIT_CODE=0 + +if [ "$FORMAT_ERRORS" -gt 0 ]; then + echo -e "${RED}Format: needs formatting${NC}" + EXIT_CODE=1 +else + echo -e "${GREEN}Format: OK${NC}" +fi + +if [ "$CRITICAL_ERRORS" -gt 0 ]; then + echo -e "${RED}Analysis: Critical violations (block merge)${NC}" + if [ "$EXIT_CODE" -eq 0 ]; then EXIT_CODE=2; else EXIT_CODE=3; fi +elif [ "$ADVISORY_WARNINGS" -gt 0 ]; then + echo -e "${YELLOW}Analysis: Major/Minor warnings (advisory)${NC}" +else + echo -e "${GREEN}Analysis: OK${NC}" +fi + +echo "" +if [ "$EXIT_CODE" -eq 0 ]; then + echo -e "${GREEN}${BOLD}✓ All checks passed!${NC}" +else + echo -e "${RED}${BOLD}✗ Validation failed (exit code: $EXIT_CODE)${NC}" +fi +echo "" +exit $EXIT_CODE diff --git a/code-standards/tests/__init__.py b/code-standards/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/code-standards/tests/test_compliance.py b/code-standards/tests/test_compliance.py new file mode 100644 index 000000000..e4c536dff --- /dev/null +++ b/code-standards/tests/test_compliance.py @@ -0,0 +1,230 @@ +"""Code Standards Compliance Tests (Python port). + +Verifies that the ruff / pyright configurations work correctly, that the +severity mapping is complete, and that the example files behave as documented. + +Python port of the framework's ``tests/test_compliance.py``. + +Requirements: + - ruff (available directly or via ``uv run ruff``) + - pytest + - PyYAML + +Run: pytest code-standards/tests/test_compliance.py -v +""" + +import shutil +import subprocess +from pathlib import Path + +import pytest + +# ============================================================================= +# Test Configuration +# ============================================================================= + +FRAMEWORK_ROOT = Path(__file__).parent.parent +EXAMPLES_DIR = FRAMEWORK_ROOT / "examples" +SCRIPTS_DIR = FRAMEWORK_ROOT / "scripts" +RUFF_CONFIG = FRAMEWORK_ROOT / "ruff.toml" + + +def ruff_cmd() -> list[str] | None: + """Return an invocation for ruff, or None if it is unavailable.""" + if shutil.which("ruff"): + return ["ruff"] + if shutil.which("uv"): + return ["uv", "run", "ruff"] + return None + + +requires_ruff = pytest.mark.skipif(ruff_cmd() is None, reason="ruff not installed") + + +# ============================================================================= +# Configuration File Tests +# ============================================================================= + + +class TestConfigurationFiles: + """Tests for configuration file existence and validity.""" + + def test_ruff_config_exists(self): + assert RUFF_CONFIG.exists(), "Missing ruff.toml configuration file" + + def test_pyright_config_exists(self): + assert (FRAMEWORK_ROOT / "pyrightconfig.json").exists(), "Missing pyrightconfig.json" + + def test_severity_mapping_exists(self): + assert (FRAMEWORK_ROOT / "rule-severity-mapping.yaml").exists(), ( + "Missing rule-severity-mapping.yaml" + ) + + def test_windsurf_rules_exist(self): + rules = FRAMEWORK_ROOT / "windsurf" / "python-safety-critical-rules.md" + assert rules.exists(), "Missing windsurf/python-safety-critical-rules.md" + + def test_validate_script_exists(self): + assert (SCRIPTS_DIR / "validate.sh").exists(), "Missing scripts/validate.sh" + + def test_validate_script_structure(self): + content = (SCRIPTS_DIR / "validate.sh").read_text() + assert "#!/bin/bash" in content, "Script should have bash shebang" + assert "ruff format" in content, "Script should use ruff format" + assert "ruff check" in content or "check --config" in content, "Script should use ruff check" + + +# ============================================================================= +# ruff Tests +# ============================================================================= + + +@requires_ruff +class TestRuff: + """Tests for the ruff configuration and example behavior.""" + + def test_config_is_valid(self): + """ruff should load the config and lint the compliant example cleanly.""" + result = subprocess.run( + [*ruff_cmd(), "check", "--config", str(RUFF_CONFIG), str(EXAMPLES_DIR / "compliant.py")], + capture_output=True, + text=True, + ) + assert result.returncode == 0, ( + f"compliant.py should pass ruff check:\n{result.stdout}\n{result.stderr}" + ) + + def test_compliant_example_is_formatted(self): + result = subprocess.run( + [ + *ruff_cmd(), + "format", + "--config", + str(RUFF_CONFIG), + "--check", + str(EXAMPLES_DIR / "compliant.py"), + ], + capture_output=True, + text=True, + ) + assert result.returncode == 0, f"compliant.py should be formatted:\n{result.stderr}" + + def test_violations_detected(self): + """violations.py must trigger lint findings.""" + result = subprocess.run( + [*ruff_cmd(), "check", "--config", str(RUFF_CONFIG), str(EXAMPLES_DIR / "violations.py")], + capture_output=True, + text=True, + ) + assert result.returncode != 0, "Expected violations.py to fail ruff check" + + def test_critical_violations_detected(self): + """The critical gate must flag the critical examples in violations.py.""" + critical_codes = ( + "E722,BLE001,S102,S301,S302,S307,S311,S324,S506," + "S602,S604,S605,S608,S609,SIM115,B006,B008,F821,F822,F823" + ) + result = subprocess.run( + [ + *ruff_cmd(), + "check", + "--config", + str(RUFF_CONFIG), + "--select", + critical_codes, + str(EXAMPLES_DIR / "violations.py"), + ], + capture_output=True, + text=True, + ) + assert result.returncode != 0, "Expected critical violations in violations.py" + + +# ============================================================================= +# Severity Mapping Tests +# ============================================================================= + + +class TestSeverityMapping: + """Tests for rule severity classification.""" + + @pytest.fixture + def severity_config(self): + yaml = pytest.importorskip("yaml") + with open(FRAMEWORK_ROOT / "rule-severity-mapping.yaml") as handle: + return yaml.safe_load(handle) + + def test_version_is_1_0(self, severity_config): + assert str(severity_config.get("version")) == "1.0", "Framework version should be 1.0" + + def test_has_all_severity_levels(self, severity_config): + levels = severity_config["severity_levels"] + assert "critical" in levels + assert "major" in levels + assert "minor" in levels + + def test_critical_has_rules(self, severity_config): + critical = severity_config["severity_levels"]["critical"] + assert len(critical["rules"]) > 0 + + def test_critical_rules_have_checks(self, severity_config): + critical = severity_config["severity_levels"]["critical"] + for rule in critical["rules"]: + assert "rule_id" in rule + assert "checks" in rule and len(rule["checks"]) > 0, ( + f"{rule.get('rule_id')} missing checks" + ) + + def test_rule_ids_are_unique(self, severity_config): + ids = [] + for level in severity_config["severity_levels"].values(): + for rule in level.get("rules", []): + ids.append(rule.get("rule_id")) + ids = [r for r in ids if r] + assert len(ids) == len(set(ids)), "Duplicate rule IDs found" + + def test_critical_rules_start_at_20(self, severity_config): + critical = severity_config["severity_levels"]["critical"] + ids = [r.get("rule_id", "") for r in critical.get("rules", [])] + assert any("Rule 2" in r for r in ids), "Critical rules should start at Rule 20" + + +# ============================================================================= +# Example / Documentation Tests +# ============================================================================= + + +class TestExampleFiles: + """Tests for example code files.""" + + def test_compliant_example_exists(self): + assert (EXAMPLES_DIR / "compliant.py").exists() + + def test_violations_example_exists(self): + assert (EXAMPLES_DIR / "violations.py").exists() + + def test_compliant_has_main(self): + content = (EXAMPLES_DIR / "compliant.py").read_text() + assert "def main" in content + + def test_violations_documents_rules(self): + content = (EXAMPLES_DIR / "violations.py").read_text() + assert "Rule 20" in content or "VIOLATION" in content + + +class TestDocumentation: + """Tests for documentation completeness.""" + + def test_readme_exists(self): + assert (FRAMEWORK_ROOT / "README.md").exists() + + def test_rule_reference_exists(self): + assert (FRAMEWORK_ROOT / "docs" / "rule-reference.md").exists() + + def test_readme_has_severity_info(self): + content = (FRAMEWORK_ROOT / "README.md").read_text() + assert "Critical" in content and "Major" in content and "Minor" in content + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/code-standards/vscode-settings.json.template b/code-standards/vscode-settings.json.template new file mode 100644 index 000000000..e691491c9 --- /dev/null +++ b/code-standards/vscode-settings.json.template @@ -0,0 +1,54 @@ +{ + // ========================================================================== + // VSCode/Windsurf Settings for the Python Code Standards Framework + // ========================================================================== + // Copy this file to .vscode/settings.json in your project (or merge these + // keys into your existing settings). Requires the "Ruff" (charliermarsh.ruff) + // and "Pylance"/"Pyright" extensions. + // ========================================================================== + + // --------------------------------------------------------------------------- + // Formatting (Rule 40) - ruff format, Black-compatible + // --------------------------------------------------------------------------- + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.ruff": "explicit", + "source.organizeImports.ruff": "explicit" + }, + "editor.tabSize": 4, + "editor.insertSpaces": true + }, + + // --------------------------------------------------------------------------- + // Ruff (Rules 20-46 static analysis) - point at the framework config + // --------------------------------------------------------------------------- + "ruff.configuration": "code-standards/ruff.toml", + "ruff.nativeServer": "on", + "ruff.lint.enable": true, + + // --------------------------------------------------------------------------- + // Pyright / Pylance (Rules 22, 25, 30, 32 - IDE intelligence, the .clangd analog) + // --------------------------------------------------------------------------- + "python.analysis.typeCheckingMode": "basic", + "python.analysis.diagnosticMode": "workspace", + "python.analysis.inlayHints.functionReturnTypes": true, + "python.analysis.inlayHints.variableTypes": true, + + // --------------------------------------------------------------------------- + // Testing + // --------------------------------------------------------------------------- + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + + // --------------------------------------------------------------------------- + // Exclude from Search/Watch + // --------------------------------------------------------------------------- + "files.watcherExclude": { + "**/.git/**": true, + "**/__pycache__/**": true, + "**/.venv/**": true, + "**/build/**": true + } +} diff --git a/code-standards/windsurf/python-safety-critical-rules.md b/code-standards/windsurf/python-safety-critical-rules.md new file mode 100644 index 000000000..9b28d6441 --- /dev/null +++ b/code-standards/windsurf/python-safety-critical-rules.md @@ -0,0 +1,363 @@ +--- +trigger: always_on +--- + +# Python Safety-Critical Code Standards + +> Copy this file to `.windsurf/rules/python-safety-critical-rules.md` in your project. + +## Overview + +When reviewing Python code, enforce these safety-critical rules and classify violations by severity. Report violations with their rule ID for traceability. This is the Python instantiation of the Code Standards Compliance Framework; rule IDs and severities match `rule-severity-mapping.yaml`, the ruff config, and the docs. + +## Severity Levels + +| Level | Icon | Action | Description | +|-------|------|--------|-------------| +| **Critical** | 🔴 | Block merge | Safety/security risk - runtime crashes, vulnerabilities, data corruption | +| **Major** | 🟡 | Require review | Quality issue - potential bugs, edge case failures | +| **Minor** | 🟢 | Warn only | Style issue - readability, maintainability | + +When you find a violation: (1) detect it, (2) classify by severity, (3) report it with the Rule ID (e.g. "🔴 Critical - Rule 20 violation"), (4) explain why the rule matters, (5) suggest a compliant fix. + +--- + +## Critical Rules (Block Merge) 🔴 + +### Rule 20: Do Not Silently Swallow Errors + +Never use a bare `except:` or a blind `except Exception: pass`. Catch specific exceptions and handle or re-raise them. + +```python +# ❌ VIOLATION - Critical (Rule 20) +try: + result = risky() +except: + pass # Failure is hidden! + +# ✅ COMPLIANT +try: + result = risky() +except ValueError as err: + logger.error("risky failed: %s", err) + raise +``` + +**Why:** Swallowed exceptions hide failures, leading to undefined behavior, silent data corruption, and undebuggable production incidents. + +--- + +### Rule 21: Prevent Injection / Unsafe Input Handling + +Never pass untrusted input into a shell, SQL string, or template. Use parameterized/argument-list APIs. + +```python +# ❌ VIOLATION - Critical (Rule 21) +subprocess.run(f"ls {user_input}", shell=True) + +# ✅ COMPLIANT +subprocess.run(["ls", user_input], shell=False) +``` + +**Why:** Injection is the #1 class of security vulnerabilities (CWE-78, CWE-89). + +--- + +### Rule 22: Guard Optional / None Values + +Validate values that may be `None` before accessing attributes, items, or calling them. + +```python +# ❌ VIOLATION - Critical (Rule 22) +value = data.get("key") +return value.strip() # AttributeError if key missing! + +# ✅ COMPLIANT +value = data.get("key") +if value is None: + return None +return value.strip() +``` + +**Why:** `None` access is the Python analog of null-pointer dereference; it crashes at runtime (AttributeError/TypeError). + +--- + +### Rule 23: Prevent Resource Leaks + +Always use a context manager (`with`) for files, sockets, and locks. + +```python +# ❌ VIOLATION - Critical (Rule 23) +f = open(path) +data = f.read() # Never closed on error! + +# ✅ COMPLIANT +with open(path) as f: + data = f.read() +``` + +**Why:** Leaked descriptors accumulate and crash long-running systems. + +--- + +### Rule 24: Avoid Dangerous Shared Mutable State + +Never use a mutable default argument (`[]`, `{}`, or a function call). + +```python +# ❌ VIOLATION - Critical (Rule 24) +def add(item, bucket=[]): + bucket.append(item) # Shared across ALL calls! + +# ✅ COMPLIANT +def add(item, bucket=None): + if bucket is None: + bucket = [] + bucket.append(item) +``` + +**Why:** Mutable defaults are evaluated once and shared, causing state-corruption bugs (the Python analog of use-after-free surprises). + +--- + +### Rule 25: No Undefined / Use-Before-Assignment + +Every name must be bound on all code paths before use. + +```python +# ❌ VIOLATION - Critical (Rule 25) +if condition: + count = 10 +return count # UnboundLocalError if condition is False! + +# ✅ COMPLIANT +count = 0 +if condition: + count = 10 +return count +``` + +**Why:** Unbound names raise NameError/UnboundLocalError at runtime. + +--- + +### Rule 26: Avoid Insecure APIs + +| Insecure | Secure Alternative | +|----------|-------------------| +| `eval()`, `exec()` | explicit parsing / `ast.literal_eval` | +| `pickle.loads(untrusted)` | `json.loads` | +| `yaml.load(x)` | `yaml.safe_load(x)` | +| `random` for secrets | `secrets` module | +| `hashlib.md5`/`sha1` for security | `hashlib.sha256` | + +```python +# ❌ VIOLATION - Critical (Rule 26) +token = random.random() + +# ✅ COMPLIANT +import secrets +token = secrets.token_hex(16) +``` + +--- + +## Major Rules (Requires Review) 🟡 + +### Rule 30: Avoid Unsafe Comparisons / Conversions + +```python +# ❌ VIOLATION - Major (Rule 30) +if type(x) == int: + ... +if name is "root": # identity check on a literal! + ... + +# ✅ COMPLIANT +if isinstance(x, int): + ... +if name == "root": + ... +``` + +--- + +### Rule 31: Consistent Signatures & Docstrings + +Docstring parameters must match the function signature (enforced by `darglint`). + +```python +# ❌ VIOLATION - Major (Rule 31) - docstring says `n`, signature says `count` +def f(count): + """Do it. + + Args: + n: the number. + """ +``` + +--- + +### Rule 32: No Redundant / Dead Code + +```python +# ❌ VIOLATION - Major (Rule 32) +result = compute() # assigned but never used +return other + +# ✅ COMPLIANT +return other +``` + +--- + +### Rule 33: Prevent Control-Flow Errors + +`break`/`continue`/`return` must appear in a valid context. + +```python +# ❌ VIOLATION - Major (Rule 33) +def f(): + x = 1 + break # break outside loop! +``` + +--- + +### Rule 34: Concurrency / Shared-State Safety + +Avoid shared mutable defaults and module-level mutable globals in concurrent code (see also Rule 24). + +--- + +### Rule 35: Avoid Performance Issues + +```python +# ❌ VIOLATION - Major (Rule 35) +out = [] +for x in items: + out.append(x * 2) + +# ✅ COMPLIANT +out = [x * 2 for x in items] +``` + +--- + +## Minor Rules (Style Warnings) 🟢 + +### Rule 40: Consistent Formatting + +Use `ruff format` (Black-compatible, 100-column). Enforced by `ruff.toml`. + +### Rule 41: Naming Conventions + +- **Functions / variables:** `snake_case` +- **Constants:** `UPPER_CASE` +- **Classes:** `PascalCase` + +```python +# ❌ VIOLATION - Minor (Rule 41) +def ProcessData(): ... +MyConstant = 100 + +# ✅ COMPLIANT +def process_data(): ... +MY_CONSTANT = 100 +``` + +### Rule 42: Explicit Comparisons + +```python +# ❌ VIOLATION - Minor (Rule 42) +if x == None: +if flag == True: + +# ✅ COMPLIANT +if x is None: +if flag: +``` + +### Rule 43: Simplify Boolean Expressions + +```python +# ❌ VIOLATION - Minor (Rule 43) +if flag == True: + +# ✅ COMPLIANT +if flag: +``` + +### Rule 44: Avoid Else After Return + +```python +# ❌ VIOLATION - Minor (Rule 44) +if error: + return -1 +else: + process() + +# ✅ COMPLIANT +if error: + return -1 +process() +``` + +### Rule 45: Use Modern Syntax + +```python +# ❌ VIOLATION - Minor (Rule 45) +d = dict() +msg = "%s" % name + +# ✅ COMPLIANT +d = {} +msg = f"{name}" +``` + +### Rule 46: Remove Unused Imports / Arguments + +```python +# ❌ VIOLATION - Minor (Rule 46) +import os + +def process(x, unused): + return x + +# ✅ COMPLIANT +def process(x): + return x +``` + +--- + +## Validation Commands + +```bash +# Check formatting +ruff format --config code-standards/ruff.toml --check src/ + +# Run static analysis +ruff check --config code-standards/ruff.toml src/ + +# Run validation script (format + tiered analysis) +./code-standards/scripts/validate.sh src/ +``` + +## Suppressing Warnings + +```python +result = risky_call() # noqa: BLE001 (explain why) +``` + +**Always add a comment explaining why suppression is necessary.** + +--- + +## References + +- [ruff rules](https://docs.astral.sh/ruff/rules/) +- [pyright configuration](https://microsoft.github.io/pyright/#/configuration) +- [CWE - Common Weakness Enumeration](https://cwe.mitre.org/) +- [OWASP Top 10](https://owasp.org/www-project-top-ten/)