Skip to content
Open
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
3 changes: 3 additions & 0 deletions desloppify/engine/_work_queue/ranking.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from desloppify.engine._work_queue.synthetic import subjective_strict_scores
from desloppify.engine._work_queue.types import WorkQueueItem
from desloppify.engine.planning.helpers import CONFIDENCE_ORDER
from desloppify.engine.policy.zones import should_skip_detector_in_zone

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -140,6 +141,8 @@ def build_issue_items(
continue
if not status_matches(issue.get("status", "open"), status_filter):
continue
if should_skip_detector_in_zone(issue.get("zone"), issue.get("detector")):
continue
if chronic and not (
issue.get("status") == "open" and issue.get("reopen_count", 0) >= 2
):
Expand Down
36 changes: 33 additions & 3 deletions desloppify/engine/planning/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,22 @@
from pathlib import Path

from desloppify.base.discovery.file_paths import rel
from desloppify.base.output.terminal import colorize
from desloppify.base.discovery.paths import get_project_root
from desloppify.base.output.terminal import colorize
from desloppify.engine.planning.helpers import is_subjective_phase
from desloppify.engine.policy.zones import ZONE_POLICIES, FileZoneMap
from desloppify.engine.policy.zones import (
ZONE_POLICIES,
FileZoneMap,
should_skip_issue,
)
from desloppify.languages.framework import (
clear_review_phase_prefetch,
DetectorPhase,
LangConfig,
LangRun,
auto_detect_lang,
available_langs,
capability_report,
clear_review_phase_prefetch,
get_lang,
make_lang_run,
prewarm_review_phase_detectors,
Expand Down Expand Up @@ -121,6 +125,31 @@ def _stamp_issue_context(issues: list[Issue], lang: LangRun) -> None:
issue["confidence"] = "low"


def _filter_zone_skipped_issues(issues: list[Issue], lang: LangRun) -> list[Issue]:
"""Apply zone policy once to every normalized phase result.

Individual phases often filter their raw entries, but raw entry shapes
differ (for example, smell findings group matches across files). A final
normalized-issue gate prevents a phase omission from admitting generated,
vendor, or otherwise policy-skipped findings into persistent state.
"""
if lang.zone_map is None:
return issues

filtered: list[Issue] = []
for issue in issues:
filepath = issue.get("file")
detector = issue.get("detector")
if (
isinstance(filepath, str)
and isinstance(detector, str)
and should_skip_issue(lang.zone_map, filepath, detector)
):
continue
filtered.append(issue)
return filtered


def _generate_issues_from_lang(
path: Path,
lang: LangRun,
Expand All @@ -137,6 +166,7 @@ def _generate_issues_from_lang(
issues, all_potentials = _run_phases(path, lang, phases)
finally:
clear_review_phase_prefetch(lang)
issues = _filter_zone_skipped_issues(issues, lang)
_stamp_issue_context(issues, lang)
_stderr(f"\n Total: {len(issues)} issues")
return issues, all_potentials
Expand Down
22 changes: 16 additions & 6 deletions desloppify/engine/policy/zones.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,18 +293,28 @@ def adjust_potential(zone_map, total: int) -> int:
return max(total - zone_map.non_production_count(), 0)


def should_skip_issue(zone_map, filepath: str, detector: str) -> bool:
"""Check if a issue should be skipped based on zone policy.
def should_skip_detector_in_zone(zone: object, detector: object) -> bool:
"""Return whether a detector is excluded by an issue's stored zone.

Returns True if the file's zone policy says to skip this detector.
Queue consumers only have the zone stamped onto an issue, not the source
``FileZoneMap`` used during scanning. Keep their policy decision aligned
with phase-time filtering rather than treating every non-production zone
as non-actionable.
"""
if zone_map is None:
normalized_zone = normalize_zone(zone)
if normalized_zone is None or not isinstance(detector, str):
return False
zone = zone_map.get(filepath)
policy = ZONE_POLICIES.get(zone)
policy = ZONE_POLICIES.get(normalized_zone)
return policy is not None and detector in policy.skip_detectors


def should_skip_issue(zone_map, filepath: str, detector: str) -> bool:
"""Check whether a file's zone policy skips its detector."""
if zone_map is None:
return False
return should_skip_detector_in_zone(zone_map.get(filepath), detector)


def filter_entries(
zone_map, entries: list[dict], detector: str, file_key: str = "file"
) -> list[dict]:
Expand Down
102 changes: 101 additions & 1 deletion desloppify/tests/commands/test_queue_count_consistency.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from desloppify.engine._plan.policy.subjective import compute_subjective_visibility
from desloppify.engine._state.merge_issues import verify_disappeared
from desloppify.engine.policy.zones import FileZoneMap, Zone, ZoneRule

# ---------------------------------------------------------------------------
# Helpers
Expand Down Expand Up @@ -171,6 +172,37 @@ def test_out_of_scope_verification_adds_to_resolved_detectors(self):
)
assert "smells" in detectors

def test_generated_zone_issue_auto_resolves_when_absent(self):
"""A scan clears historical issues that current zone policy excludes."""
generated_file = "client/migrations/versions/revision.py"
existing = {
"generated-smells": {
"id": "generated-smells",
"status": "open",
"file": generated_file,
"detector": "smells",
},
}
zone_map = FileZoneMap(
[generated_file], [ZoneRule(Zone.GENERATED, ["/migrations/"])]
)

resolved, _lang, out_of_scope, detectors = verify_disappeared(
existing,
current_ids=set(),
suspect_detectors=set(),
now="2026-08-01T00:00:00+00:00",
lang=None,
scan_path=".",
zone_map=zone_map,
)

assert resolved == 1
assert out_of_scope == 0
assert detectors == {"smells"}
assert existing["generated-smells"]["status"] == "auto_resolved"
assert "zone policy now skips smells" in existing["generated-smells"]["note"]


# ---------------------------------------------------------------------------
# Fix 2: queue counting functions pass scan_path
Expand Down Expand Up @@ -238,6 +270,74 @@ def test_queue_count_no_scan_path_returns_all(self):
)
assert result["total"] == 2

def test_queue_hides_historical_zone_policy_skipped_issues(self):
"""Stored generated findings cannot re-enter the active queue."""
from desloppify.engine._work_queue.core import (
QueueBuildOptions,
build_work_queue,
)

state: dict = {
"issues": {
"generated-smells": {
"id": "generated-smells",
"detector": "smells",
"status": "open",
"file": "client/migrations/versions/revision.py",
"zone": "generated",
"tier": 1,
"confidence": "high",
"summary": "historical generated smell",
"detail": {},
},
"generated-structural": {
"id": "generated-structural",
"detector": "structural",
"status": "open",
"file": "client/migrations/versions/revision.py",
"zone": "generated",
"tier": 1,
"confidence": "high",
"summary": "historical generated structural finding",
"detail": {},
},
"test-unused": {
"id": "test-unused",
"detector": "unused",
"status": "open",
"file": "tests/test_live.py",
"zone": "test",
"tier": 1,
"confidence": "high",
"summary": "allowed test-zone detector",
"detail": {},
},
"production-smells": {
"id": "production-smells",
"detector": "smells",
"status": "open",
"file": "src/live.py",
"zone": "production",
"tier": 1,
"confidence": "high",
"summary": "production finding",
"detail": {},
},
},
"scan_count": 5,
}

result = build_work_queue(
state,
options=QueueBuildOptions(status="open", count=None),
)

assert {item["id"] for item in result["items"]} == {
"test-unused",
"production-smells",
}
assert result["total"] == 2

def test_explicit_scan_path_overrides_state(self):
"""Explicit scan_path on QueueBuildOptions overrides state value."""
from desloppify.engine._work_queue.core import (
Expand Down Expand Up @@ -772,11 +872,11 @@ class TestQueueGuardScanPath:
def test_queue_guard_respects_scan_path_from_state(self):
"""_check_queue_order_guard uses build_work_queue which auto-reads
scan_path from state, so out-of-scope items don't appear in the queue."""
from desloppify.app.commands.resolve.queue_guard import _check_queue_order_guard
from desloppify.app.commands.resolve.plan_load import (
DegradedPlanWarningState,
ResolvePlanAccess,
)
from desloppify.app.commands.resolve.queue_guard import _check_queue_order_guard

state = {
"issues": {
Expand Down
69 changes: 68 additions & 1 deletion desloppify/tests/plan/test_plan_modules_direct.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
import pytest

import desloppify.engine._state.filtering as filtering_mod
from desloppify.engine._work_queue.core import QueueBuildOptions
import desloppify.engine.planning.helpers as plan_common_mod
import desloppify.engine.planning.queue_policy as queue_policy_mod
import desloppify.engine.planning.scan as plan_scan_mod
import desloppify.engine.planning.select as plan_select_mod
from desloppify.engine._work_queue.core import QueueBuildOptions
from desloppify.engine.policy.zones import FileZoneMap, Zone, ZoneRule


class _Phase:
Expand Down Expand Up @@ -119,6 +120,72 @@ def test_generate_issues_from_lang_clears_prefetch_on_phase_error(monkeypatch):
assert calls == ["prime", "clear"]


def test_generate_issues_from_lang_filters_zone_skipped_phase_output(monkeypatch):
"""A final gate catches generated findings leaked by an individual phase."""
generated_file = "client/migrations/versions/revision.py"
production_file = "src/live.py"
test_file = "tests/test_live.py"
lang = SimpleNamespace(
phases=[],
name="python",
zone_map=FileZoneMap(
[generated_file, production_file, test_file],
[
ZoneRule(Zone.GENERATED, ["/migrations/"]),
ZoneRule(Zone.TEST, ["/tests/"]),
],
),
)

monkeypatch.setattr(plan_scan_mod, "_build_zone_map", lambda *_a, **_k: None)
monkeypatch.setattr(plan_scan_mod, "_select_phases", lambda *_a, **_k: [])
monkeypatch.setattr(
plan_scan_mod,
"_run_phases",
lambda *_a, **_k: (
[
{
"id": "generated-smells",
"file": generated_file,
"detector": "smells",
},
{
"id": "generated-structural",
"file": generated_file,
"detector": "structural",
},
{
"id": "production-smells",
"file": production_file,
"detector": "smells",
},
{
"id": "allowed-test-unused",
"file": test_file,
"detector": "unused",
},
],
{"smells": 2, "structural": 1, "unused": 1},
),
)
monkeypatch.setattr(
plan_scan_mod, "prewarm_review_phase_detectors", lambda *_a, **_k: None
)
monkeypatch.setattr(
plan_scan_mod, "clear_review_phase_prefetch", lambda *_a, **_k: None
)

issues, potentials = plan_scan_mod._generate_issues_from_lang(Path("."), lang)

assert [issue["id"] for issue in issues] == [
"production-smells",
"allowed-test-unused",
]
assert [issue["zone"] for issue in issues] == ["production", "test"]
assert all(issue["lang"] == "python" for issue in issues)
assert potentials == {"smells": 2, "structural": 1, "unused": 1}


def test_resolve_lang_prefers_explicit_and_fallbacks(monkeypatch):
explicit = object()
assert plan_scan_mod._resolve_lang(explicit, Path(".")) is explicit
Expand Down