Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .ai/stack-profiles/diffrat-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pytest
ruff format --check src tests
ruff check .
mypy .
bandit -r src/diffrat/checks.py src/diffrat/review_quality.py src/diffrat/scoring.py
python -m diffrat --help
diffrat --help
```
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/validate-workflow-contracts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,12 @@ jobs:
- name: Mypy
run: mypy .

- name: Bandit
run: >
bandit -r
src/diffrat/checks.py
src/diffrat/review_quality.py
src/diffrat/scoring.py

- name: Pytest
run: pytest tests/ -q
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- `--check` bandit false positives on Diffrat itself (`subprocess` import/run in
`checks.py`; pillar id `"maintainable"` in `review_quality.py`)
- Track `.ai/ideas/implemented/` docs referenced by `decisions.md`

### Changed
Expand All @@ -18,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`./scripts/setup-ai-workflow.sh` first
- CI runs `ruff format --check src tests`, `ruff check .`, and `mypy .` in
addition to pytest (matches README / stack-profile quality gates)
- CI / `[dev]` include `bandit` on `--check` dogfood modules (`checks.py`,
`review_quality.py`, `scoring.py`)
- One-shot `ruff format` on `src/` and `tests/`

## [1.1.1] - 2026-08-09
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ pytest
ruff format --check src tests
ruff check .
mypy .
bandit -r src/diffrat/checks.py src/diffrat/review_quality.py src/diffrat/scoring.py
```

## Architecture and context
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Issues = "https://github.com/szymoniwacz/diffrat/issues"
Changelog = "https://github.com/szymoniwacz/diffrat/blob/main/CHANGELOG.md"

[project.optional-dependencies]
dev = ["pytest>=8.0", "mypy>=1.8", "ruff>=0.4"]
dev = ["pytest>=8.0", "mypy>=1.8", "ruff>=0.4", "bandit>=1.7"]

[project.scripts]
diffrat = "diffrat.__main__:main"
Expand Down
4 changes: 2 additions & 2 deletions src/diffrat/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import shlex
import shutil
import subprocess
import subprocess # nosec B404
import sys
from dataclasses import dataclass
from pathlib import PurePosixPath
Expand Down Expand Up @@ -254,7 +254,7 @@ def run_checks(
)
)
continue
completed = subprocess.run(
completed = subprocess.run( # nosec B603
list(spec.argv),
cwd=cwd,
capture_output=True,
Expand Down
39 changes: 21 additions & 18 deletions src/diffrat/review_quality.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
PillarId = Literal["understand", "focused", "maintainable"]
PillarStatus = Literal["ok", "warn", "risk"]

_DEFAULT_PILLAR: PillarId = "maintainable"
# Pillar id label (JSON/API contract); not a credential.
PILLAR_MAINTAINABLE: PillarId = "maintainable" # nosec B105

_DEFAULT_PILLAR: PillarId = PILLAR_MAINTAINABLE


@dataclass(frozen=True, slots=True)
Expand Down Expand Up @@ -43,7 +46,7 @@ class ReviewQualityPillarResult:
label="One thing well",
),
ReviewQualityPillar(
id="maintainable",
id=PILLAR_MAINTAINABLE,
label="Safe to change in six months",
),
)
Expand All @@ -66,22 +69,22 @@ class ReviewQualityPillarResult:
"wip_commits": "focused",
"mixed_concerns": "focused",
# maintainable — safety, tests, dependencies, fragile patterns
"security_sensitive_paths": "maintainable",
"ci_workflow_paths": "maintainable",
"possible_secret": "maintainable",
"dangerous_call": "maintainable",
"config_or_deps": "maintainable",
"suspicious_constant_change": "maintainable",
"tests_touched": "maintainable",
"source_without_tests": "maintainable",
"source_heavy_without_tests": "maintainable",
"ci_without_tests": "maintainable",
"missing_test_file": "maintainable",
"lockfile_without_manifest": "maintainable",
"manifest_without_lockfile": "maintainable",
"debug_leftover": "maintainable",
"broad_exception": "maintainable",
"hardcoded_url_or_ip": "maintainable",
"security_sensitive_paths": PILLAR_MAINTAINABLE,
"ci_workflow_paths": PILLAR_MAINTAINABLE,
"possible_secret": PILLAR_MAINTAINABLE,
"dangerous_call": PILLAR_MAINTAINABLE,
"config_or_deps": PILLAR_MAINTAINABLE,
"suspicious_constant_change": PILLAR_MAINTAINABLE,
"tests_touched": PILLAR_MAINTAINABLE,
"source_without_tests": PILLAR_MAINTAINABLE,
"source_heavy_without_tests": PILLAR_MAINTAINABLE,
"ci_without_tests": PILLAR_MAINTAINABLE,
"missing_test_file": PILLAR_MAINTAINABLE,
"lockfile_without_manifest": PILLAR_MAINTAINABLE,
"manifest_without_lockfile": PILLAR_MAINTAINABLE,
"debug_leftover": PILLAR_MAINTAINABLE,
"broad_exception": PILLAR_MAINTAINABLE,
"hardcoded_url_or_ip": PILLAR_MAINTAINABLE,
}


Expand Down
28 changes: 28 additions & 0 deletions tests/test_bandit_self.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Dogfood: bandit stays clean on modules that --check commonly scans."""

from __future__ import annotations

import shutil
import subprocess
from pathlib import Path

import pytest

_REPO_ROOT = Path(__file__).resolve().parents[1]
_BANDIT_TARGETS = (
"src/diffrat/checks.py",
"src/diffrat/review_quality.py",
"src/diffrat/scoring.py",
)


@pytest.mark.skipif(shutil.which("bandit") is None, reason="bandit not installed")
def test_bandit_clean_on_check_dogfood_modules() -> None:
completed = subprocess.run(
["bandit", "-r", *_BANDIT_TARGETS],
cwd=_REPO_ROOT,
capture_output=True,
text=True,
check=False,
)
assert completed.returncode == 0, completed.stdout + completed.stderr
Loading