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
22 changes: 16 additions & 6 deletions desloppify/engine/planning/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@
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.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 @@ -54,7 +54,9 @@ def _resolve_lang(
return get_lang(detected)


def _build_zone_map(path: Path, lang: LangRun, zone_overrides: dict[str, str] | None) -> None:
def _build_zone_map(
path: Path, lang: LangRun, zone_overrides: dict[str, str] | None
) -> None:
if not (lang.zone_rules and lang.file_finder):
return

Expand All @@ -77,7 +79,9 @@ def _build_zone_map(path: Path, lang: LangRun, zone_overrides: dict[str, str] |
_stderr(f" Not available: {', '.join(missing)}")


def _select_phases(lang: LangRun, *, include_slow: bool, profile: str) -> list[DetectorPhase]:
def _select_phases(
lang: LangRun, *, include_slow: bool, profile: str
) -> list[DetectorPhase]:
active_profile = profile if profile in {"objective", "full", "ci"} else "full"
phases = lang.phases
if not include_slow or active_profile == "ci":
Expand All @@ -87,7 +91,9 @@ def _select_phases(lang: LangRun, *, include_slow: bool, profile: str) -> list[D
return phases


def _run_phases(path: Path, lang: LangRun, phases: list[DetectorPhase]) -> tuple[list[Issue], dict[str, int]]:
def _run_phases(
path: Path, lang: LangRun, phases: list[DetectorPhase]
) -> tuple[list[Issue], dict[str, int]]:
issues: list[Issue] = []
all_potentials: dict[str, int] = {}

Expand All @@ -114,7 +120,11 @@ def _stamp_issue_context(issues: list[Issue], lang: LangRun) -> None:
if lang.zone_map is None:
continue

zone = lang.zone_map.get(issue.get("file", ""))
file_path = issue.get("file", "")
if issue.get("detector") == "flat_dirs":
zone = lang.zone_map.get_directory(file_path)
else:
zone = lang.zone_map.get(file_path)
issue["zone"] = zone.value
policy = zone_policies.get(zone) if zone_policies else None
if policy and issue.get("detector") in policy.downgrade_detectors:
Expand Down
20 changes: 20 additions & 0 deletions desloppify/engine/policy/zones.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from collections.abc import Callable
from dataclasses import dataclass, field
from enum import Enum
from pathlib import PurePath

from desloppify.base.output.fallbacks import log_best_effort_failure
from desloppify.engine.policy.zones_data import (
Expand Down Expand Up @@ -194,6 +195,25 @@ def get(self, path: str) -> Zone:

return Zone.PRODUCTION

def get_directory(self, path: str) -> Zone:
"""Get the common zone for classified files beneath a directory."""
rel_path = path
if self._rel_fn is not None:
try:
rel_path = self._rel_fn(path)
except (OSError, TypeError, ValueError):
pass

directory = PurePath(rel_path)
zones = {
zone
for file_path, zone in self._rel_map.items()
if PurePath(file_path).is_relative_to(directory)
}
if len(zones) == 1:
return zones.pop()
return Zone.PRODUCTION

def exclude(self, files: list[str], *zones: Zone) -> list[str]:
"""Return files NOT in the given zones."""
zone_set = set(zones)
Expand Down
18 changes: 17 additions & 1 deletion desloppify/tests/detectors/test_zones.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,23 @@ def test_get_unknown_file_defaults_production(self, zone_map):
"""File not in the map returns PRODUCTION."""
assert zone_map.get("unknown/file.py") == Zone.PRODUCTION

def test_get_directory_returns_common_descendant_zone(self):
"""A directory inherits the zone shared by all classified descendants."""
files = ["auth_test.go", "internal/runner/runner.go"]
overrides = {file_path: "test" for file_path in files}
zone_map = FileZoneMap(files, [], overrides=overrides)

assert zone_map.get_directory(".") == Zone.TEST
assert zone_map.get_directory("internal") == Zone.TEST

def test_get_directory_defaults_production_for_mixed_or_unknown_directory(self):
"""Mixed and unknown directories retain conservative production scoring."""
files = ["src/app.py", "tests/test_app.py"]
zone_map = FileZoneMap(files, COMMON_ZONE_RULES)

assert zone_map.get_directory(".") == Zone.PRODUCTION
assert zone_map.get_directory("missing") == Zone.PRODUCTION

def test_exclude_zones(self, zone_map, sample_files):
"""exclude() removes files in specified zones."""
result = zone_map.exclude(sample_files, Zone.TEST, Zone.VENDOR)
Expand Down Expand Up @@ -569,4 +586,3 @@ def test_every_zone_has_policy(self):


# ── adjust_potential() ───────────────────────────────────────

54 changes: 52 additions & 2 deletions desloppify/tests/plan/test_plan_modules_direct.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
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._scoring.detection import detector_pass_rate
from desloppify.engine._work_queue.core import QueueBuildOptions
from desloppify.engine.policy.zones import FileZoneMap


class _Phase:
Expand Down Expand Up @@ -65,6 +67,52 @@ def test_select_phases_and_run_phases_behavior():
assert potentials == {"fast": 1, "slow": 2, "review": 3}


def test_stamp_issue_context_excludes_all_test_flat_directory_from_score():
files = ["auth_test.go", "internal/runner/runner.go"]
overrides = {file_path: "test" for file_path in files}
zone_map = FileZoneMap(files, [], overrides=overrides)
issue = filtering_mod.make_issue(
"flat_dirs",
".",
"",
tier=3,
confidence="medium",
summary="Directory overload",
)
lang = SimpleNamespace(name="go", zone_map=zone_map)

plan_scan_mod._stamp_issue_context([issue], lang)

assert issue["zone"] == "test"
assert detector_pass_rate("flat_dirs", {issue["id"]: issue}, 5) == (
1.0,
0,
0.0,
)


def test_stamp_issue_context_keeps_production_flat_directory_scored():
zone_map = FileZoneMap(["app.go", "internal/service.go"], [])
issue = filtering_mod.make_issue(
"flat_dirs",
".",
"",
tier=3,
confidence="medium",
summary="Directory overload",
)
lang = SimpleNamespace(name="go", zone_map=zone_map)

plan_scan_mod._stamp_issue_context([issue], lang)

assert issue["zone"] == "production"
assert detector_pass_rate("flat_dirs", {issue["id"]: issue}, 5) == (
0.86,
1,
0.7,
)


def test_generate_issues_from_lang_primes_and_clears_review_prefetch(monkeypatch):
calls: list[str] = []
lang = SimpleNamespace(phases=[], zone_map=None, name="python")
Expand Down Expand Up @@ -124,7 +172,9 @@ def test_resolve_lang_prefers_explicit_and_fallbacks(monkeypatch):
assert plan_scan_mod._resolve_lang(explicit, Path(".")) is explicit

monkeypatch.setattr(plan_scan_mod, "auto_detect_lang", lambda _root: None)
monkeypatch.setattr(plan_scan_mod, "available_langs", lambda: ["python", "typescript"])
monkeypatch.setattr(
plan_scan_mod, "available_langs", lambda: ["python", "typescript"]
)
monkeypatch.setattr(plan_scan_mod, "get_lang", lambda name: f"cfg:{name}")
resolved = plan_scan_mod._resolve_lang(None, Path("."))
assert resolved == "cfg:python"
Expand Down