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
13 changes: 12 additions & 1 deletion desloppify/engine/_state/merge_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ def verify_disappeared(
Returns (resolved_count, skipped_other_lang, resolved_out_of_scope, changed_detectors).
Queue-tracked work stays user-controlled unless the detector is known to
have run in the current scan or the source file no longer exists. Manually
resolved items can be marked as scan-verified when they remain absent.
resolved items can be marked as scan-verified when they remain absent
from a detector confirmed to have run in this scan.
"""
resolved = skipped_other_lang = resolved_out_of_scope = 0
resolved_detectors: set[str] = set()
Expand All @@ -123,6 +124,16 @@ def verify_disappeared(
if previous.get("detector", "unknown") in suspect_detectors:
continue

# Review imports and partial scans do not corroborate dispositions for
# detectors that did not run. Suspect detection alone is insufficient:
# it counts open issues, and a detector may have only resolved issues.
# Apply this before scope verification, which also records scan evidence.
if previous_status != "open" and (
confirmed_detectors is None
or previous.get("detector", "unknown") not in confirmed_detectors
):
continue

if scan_path and scan_path != ".":
prefix = scan_path.rstrip("/") + "/"
if (
Expand Down
2 changes: 2 additions & 0 deletions desloppify/tests/commands/test_queue_count_consistency.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def test_out_of_scope_fixed_issues_get_scan_verification(self):
now="2026-03-01T00:00:00+00:00",
lang=None,
scan_path="src",
confirmed_detectors={"smells"},
)
attestation = existing["f1"]["resolution_attestation"]
assert existing["f1"]["status"] == "fixed"
Expand Down Expand Up @@ -168,6 +169,7 @@ def test_out_of_scope_verification_adds_to_resolved_detectors(self):
now="2026-01-01T00:00:00+00:00",
lang=None,
scan_path="src",
confirmed_detectors={"smells"},
)
assert "smells" in detectors

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,7 @@ def test_auto_resolved_detector_marks_its_dimensions_stale(self):
state["work_items"]["structural::big.py::large_file"]["status"] = "fixed"

# Second scan: structural issue absent → scan-verified, detector changed
merge_scan(state, [], MergeScanOptions(force_resolve=True))
merge_scan(state, [], MergeScanOptions(potentials={"structural": 1}))

dc = state["subjective_assessments"]["design_coherence"]
assert dc["needs_review_refresh"] is True
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Review imports leave mechanical dispositions and fresh assessments intact."""

from copy import deepcopy

import pytest

from desloppify.intelligence.review import import_holistic_issues, import_review_issues
from desloppify.state import ensure_state_defaults
from desloppify.tests.state.test_state import _make_raw_issue


@pytest.mark.parametrize("import_review", [import_holistic_issues, import_review_issues])
@pytest.mark.parametrize("status", ["fixed", "wontfix", "false_positive"])
def test_review_import_does_not_verify_mechanical_issue(
import_review, status, empty_state, tmp_path
):
issue = _make_raw_issue(
"structural::a.ts::large_file", detector="structural", file="a.ts", status=status,
lang="typescript",
)
issue["resolution_attestation"] = {
"kind": "manual", "text": "reviewed", "scan_verified": False,
}
empty_state["work_items"][issue["id"]] = issue
ensure_state_defaults(empty_state)
before = deepcopy(issue)

diff = import_review(
{"issues": [], "assessments": {"design_coherence": 81}},
empty_state, "typescript", project_root=tmp_path,
)

assert diff["auto_resolved"] == 0
assert issue == before
assessment = empty_state["subjective_assessments"]["design_coherence"]
assert assessment["score"] == 81
assert not assessment.get("needs_review_refresh")
assert "stale_since" not in assessment
66 changes: 66 additions & 0 deletions desloppify/tests/state/test_manual_scan_verification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""A review import is not evidence that a mechanical detector ran (#809)."""

from copy import deepcopy

import pytest

from desloppify.engine._state.merge_issues import verify_disappeared
from desloppify.state import MergeScanOptions, empty_state, merge_scan
from desloppify.tests.state.test_state import _make_raw_issue


@pytest.mark.parametrize("status", ["fixed", "wontfix", "false_positive"])
@pytest.mark.parametrize("scan_path", [".", "other"])
@pytest.mark.parametrize("confirmed_detectors", [None, set(), {"review"}])
def test_manual_resolution_requires_detector_evidence(
status, scan_path, confirmed_detectors
):
issue = _make_raw_issue(
"smells::src/a.py::item", detector="smells", file="src/a.py", status=status
)
issue["note"] = "Manual disposition"
issue["resolution_attestation"] = {
"kind": "manual", "text": "reviewed", "scan_verified": False,
}
existing = {issue["id"]: issue}
before = deepcopy(existing)

result = verify_disappeared(
existing, set(), set(), "2026-03-14T00:00:00+00:00",
lang=None, scan_path=scan_path, confirmed_detectors=confirmed_detectors,
)

assert result == (0, 0, 0, set())
assert existing == before


@pytest.mark.parametrize("status", ["fixed", "wontfix", "false_positive"])
@pytest.mark.parametrize("evidence", ["potentials", "output"])
def test_manual_resolution_is_verified_when_detector_ran(status, evidence):
state = empty_state()
issue = _make_raw_issue(
"smells::a.py::item", detector="smells", file="a.py", status=status
)
state["work_items"][issue["id"]] = issue
output = []
options = MergeScanOptions(potentials={"smells": 10})
if evidence == "output":
output = [_make_raw_issue("smells::b.py::item", detector="smells", file="b.py")]
options = MergeScanOptions()

diff = merge_scan(state, output, options)

assert diff["auto_resolved"] == 1
assert issue["status"] == status
assert issue["resolution_attestation"]["scan_verified"] is True


def test_force_resolve_does_not_invent_detector_evidence():
state = empty_state()
issue = _make_raw_issue("smells::a.py::item", detector="smells", status="fixed")
state["work_items"][issue["id"]] = issue

diff = merge_scan(state, [], MergeScanOptions(force_resolve=True, potentials={"review": 1}))

assert diff["auto_resolved"] == 0
assert "resolution_attestation" not in issue
2 changes: 1 addition & 1 deletion desloppify/tests/state/test_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ def test_missing_fixed_issue_gets_scan_verified(self):
}
st["issues"]["det::a.py::fn"] = old

diff = merge_scan(st, [], MergeScanOptions(lang="python", force_resolve=True))
diff = merge_scan(st, [], MergeScanOptions(lang="python", potentials={"det": 1}))
assert diff["auto_resolved"] == 1
assert st["issues"]["det::a.py::fn"]["status"] == "fixed"
assert st["issues"]["det::a.py::fn"]["resolution_attestation"]["scan_verified"] is True
Expand Down