From ca3cb74244fc9f190316c0d16a1b10fd4f4e8aa5 Mon Sep 17 00:00:00 2001 From: Szymon Iwacz Date: Sun, 9 Aug 2026 12:12:21 +0200 Subject: [PATCH 1/3] Fix --check bandit argv and __init__ mapping Use a single bandit -r with multiple paths, and skip package __init__.py when mapping to pytest targets. --- src/diffrat/checks.py | 10 ++++----- tests/test_checks.py | 48 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/diffrat/checks.py b/src/diffrat/checks.py index 7de01b3..cb26fb9 100644 --- a/src/diffrat/checks.py +++ b/src/diffrat/checks.py @@ -102,6 +102,8 @@ def _map_path_to_pytest_target(path: str) -> str | None: parts = posix.parts if is_src_package_path(path): + if posix.name == "__init__.py": + return None if posix.suffix == ".py": return f"tests/test_{posix.stem}.py" return "tests" @@ -281,12 +283,8 @@ def _format_subprocess_output(stdout: str, stderr: str) -> str: def _bandit_display_command(targets: list[str]) -> str: - flags = " ".join(f"-r {target}" for target in targets) - return f"{_BANDIT_COMMAND} {flags}" + return f"{_BANDIT_COMMAND} -r {' '.join(targets)}" def _bandit_argv(executable: str, targets: list[str]) -> tuple[str, ...]: - argv: list[str] = [executable] - for target in targets: - argv.extend(["-r", target]) - return tuple(argv) + return (executable, "-r", *targets) diff --git a/tests/test_checks.py b/tests/test_checks.py index 25caace..c5870d5 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -12,6 +12,8 @@ ) from diffrat.checks import ( CheckSpec, + _bandit_argv, + _bandit_display_command, bandit_targets_for_paths, is_pip_audit_dependency_path, mypy_targets_for_paths, @@ -131,6 +133,15 @@ def test_pytest_targets_maps_conftest_to_tests_directory() -> None: assert pytest_targets_for_paths(["tests/conftest.py"]) == ["tests"] +def test_pytest_targets_skips_package_init() -> None: + assert pytest_targets_for_paths(["src/diffrat/__init__.py"]) == [] + + +def test_pytest_targets_skips_init_among_other_modules() -> None: + paths = ["src/diffrat/__init__.py", "src/diffrat/review.py"] + assert pytest_targets_for_paths(paths) == ["tests/test_review.py"] + + def test_mypy_targets_maps_source_modules() -> None: assert mypy_targets_for_paths(["src/diffrat/review.py"]) == ["src/diffrat/review.py"] @@ -385,6 +396,43 @@ def test_plan_checks_selects_bandit_when_on_path() -> None: assert specs[2].skip_reason is None +def test_bandit_command_uses_single_r_flag_for_multiple_targets() -> None: + targets = ["src/diffrat/__init__.py", "src/diffrat/analysis.py", "src/diffrat/scoring.py"] + assert _bandit_display_command(targets) == ( + "bandit -r src/diffrat/__init__.py src/diffrat/analysis.py src/diffrat/scoring.py" + ) + assert _bandit_argv("/usr/bin/bandit", targets) == ( + "/usr/bin/bandit", + "-r", + "src/diffrat/__init__.py", + "src/diffrat/analysis.py", + "src/diffrat/scoring.py", + ) + + +def test_plan_checks_bandit_multi_file_single_r_flag() -> None: + summary = DiffSummary( + files=( + FileChange(path="src/diffrat/review.py", additions=1, deletions=0, binary=False), + FileChange(path="src/diffrat/scoring.py", additions=1, deletions=0, binary=False), + ) + ) + + with patch("diffrat.checks.shutil.which", return_value="/usr/bin/bandit"): + specs = plan_checks(summary) + + bandit_spec = next(spec for spec in specs if spec.code == "bandit") + assert bandit_spec.display_command == ( + "bandit -r src/diffrat/review.py src/diffrat/scoring.py" + ) + assert bandit_spec.argv == ( + "/usr/bin/bandit", + "-r", + "src/diffrat/review.py", + "src/diffrat/scoring.py", + ) + + def test_plan_checks_skips_bandit_when_missing() -> None: summary = DiffSummary( files=( From 03d4fe8ca9f7c95f0758085d3cd5641f16fc1a1e Mon Sep 17 00:00:00 2001 From: Szymon Iwacz Date: Sun, 9 Aug 2026 12:12:21 +0200 Subject: [PATCH 2/3] Tighten possible_secret entropy heuristics Ignore code and prose string fragments so f-string message pieces no longer trigger high-entropy secret hints. --- src/diffrat/content_hints.py | 3 +++ tests/test_content_hints.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/diffrat/content_hints.py b/src/diffrat/content_hints.py index 085e739..6e03ca4 100644 --- a/src/diffrat/content_hints.py +++ b/src/diffrat/content_hints.py @@ -258,6 +258,9 @@ def _is_high_entropy_literal(value: str) -> bool: return False if "://" in value or "|" in value or value.startswith("^") or value.endswith("$"): return False + # Reject code/prose fragments (f-strings, expressions), keep token-like secrets. + if re.search(r"[\s{}()*/\\]", value): + return False if not re.search(r"[A-Za-z]", value) or not re.search(r"\d", value): return False counts = Counter(value) diff --git a/tests/test_content_hints.py b/tests/test_content_hints.py index c92d802..f0c89bb 100644 --- a/tests/test_content_hints.py +++ b/tests/test_content_hints.py @@ -200,6 +200,24 @@ def test_content_hints_possible_secret_negative() -> None: assert content_focus_risk_hints(content) == [] +def test_content_hints_possible_secret_rejects_fstring_code_fragment() -> None: + content = _single_file_content( + "src/diffrat/analysis.py", + 'f"additions ({source_additions * 100 // summary.total_additions}% "', + ) + + assert content_focus_risk_hints(content) == [] + + +def test_content_hints_possible_secret_high_entropy_token() -> None: + token = "a7Kx9Qm2Vp4Ln8Rw0Yz3Bc6Hd1Jf5Tg" + content = _single_file_content("src/diffrat/auth.py", f'token_blob = "{token}"') + + hints = content_focus_risk_hints(content) + assert len(hints) == 1 + assert hints[0].code == "possible_secret" + + def test_content_hints_debug_leftover_positive() -> None: content = _single_file_content("src/diffrat/review.py", "print(result)") From ce098fc330f43f16d9d1eb23f922adcbfa9343c5 Mon Sep 17 00:00:00 2001 From: Szymon Iwacz Date: Sun, 9 Aug 2026 12:12:21 +0200 Subject: [PATCH 3/3] Bump to 1.1.1 and sync feedback docs Record the check and hint fixes in the changelog and update the external feedback checklist for --brief and Review quality. --- CHANGELOG.md | 18 +++++++++++++++++- README.md | 2 +- docs/feedback-checklist.md | 13 ++++++++++--- pyproject.toml | 2 +- src/diffrat/__init__.py | 2 +- 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 812f36a..e3152d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.1.1] - 2026-08-09 + +### Fixed + +- `--check` bandit invocation uses a single `-r` with multiple paths (repeated + `-r` flags broke bandit CLI) +- `--check` no longer maps package `__init__.py` to a non-existent + `tests/test___init__.py` +- `possible_secret` high-entropy detection ignores code/prose string fragments + (e.g. f-string message pieces with braces and operators) + +### Changed + +- `docs/feedback-checklist.md` updated for 1.1.1 (`--brief`, Review quality) + ## [1.1.0] - 2026-08-08 ### Added @@ -91,6 +106,7 @@ project name, CLI command, and import package match (`diffrat`). PyPI held a `.diffrat.toml`, and `DIFFRAT_LLM_*` (old `NUMBAT_*` / `[tool.numbat]` no longer read) -[Unreleased]: https://github.com/szymoniwacz/diffrat/compare/v1.1.0...HEAD +[Unreleased]: https://github.com/szymoniwacz/diffrat/compare/v1.1.1...HEAD +[1.1.1]: https://github.com/szymoniwacz/diffrat/compare/v1.1.0...v1.1.1 [1.1.0]: https://github.com/szymoniwacz/diffrat/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/szymoniwacz/diffrat/releases/tag/v1.0.0 diff --git a/README.md b/README.md index 5be9ea3..c8008a1 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,7 @@ is empty while `changes.limits` remains. `--brief` works with `--staged`, ## Status -**1.1.0** is the current release on PyPI as +**1.1.1** is the current release on PyPI as [`diffrat`](https://pypi.org/project/diffrat/) (formerly developed as Numbat; see D-008). **1.0.0** was the first product release: diff --git a/docs/feedback-checklist.md b/docs/feedback-checklist.md index 536c63f..01a80e1 100644 --- a/docs/feedback-checklist.md +++ b/docs/feedback-checklist.md @@ -1,4 +1,4 @@ -# Feedback checklist (1.0.0) +# Feedback checklist (1.1.1) Short session for external testers and dogfood. Goal: feedback on the **CLI product**, not on private AI workflow tooling. @@ -14,6 +14,8 @@ pip install diffrat diffrat --version ``` +Expect `diffrat 1.1.1` (or newer). + From source: ```bash @@ -46,8 +48,12 @@ diffrat review --staged # Branch vs base diffrat review --base main +# Triage-first (omit Changes hunks); note Review quality pillars after Summary +diffrat review --base main --brief + # JSON for scripting diffrat review --base main --json +diffrat review --base main --brief --json # Optional gates / deep view diffrat review --base main --fail-on=possible_secret,docs_touched @@ -72,8 +78,9 @@ diffrat review --base main 1. What was confusing or unexpected in Setup or the first successful report? 2. Which Focus/Risk hints felt useful? Which felt like false positives or noise? 3. Was Review order / risk scoring helpful for deciding what to look at first? -4. Did `--json` fit how you would script or gate a review? -5. What is the one thing you most wanted that is missing? +4. Did the Review quality pillars (understand / one thing well / maintainable) help? +5. Did `--json` fit how you would script or gate a review? +6. What is the one thing you most wanted that is missing? ## What not to evaluate diff --git a/pyproject.toml b/pyproject.toml index 1c953c0..327738d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "diffrat" -version = "1.1.0" +version = "1.1.1" description = "Local CLI for diff and PR review assistance" readme = "README.md" requires-python = ">=3.11" diff --git a/src/diffrat/__init__.py b/src/diffrat/__init__.py index 27ec655..d6fe9f7 100644 --- a/src/diffrat/__init__.py +++ b/src/diffrat/__init__.py @@ -1,3 +1,3 @@ """Diffrat — local CLI for diff and PR review assistance.""" -__version__ = "1.1.0" +__version__ = "1.1.1"