From 17ab28e7e1e71f8a2b5fb4021ffad701c6002492 Mon Sep 17 00:00:00 2001 From: charles chen Date: Sat, 3 Oct 2026 10:00:54 +0300 Subject: [PATCH] fix: possible_duplicate skips the CR being planned and withdrawn items; release 0.50.1 Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 8 ++++++++ medharness/services/design_validation.py | 14 ++++++++++++-- pyproject.toml | 2 +- tests/integration/test_ai_stages_end_to_end.py | 17 +++++++++++++++++ uv.lock | 2 +- 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7c9d3c8..114a2b3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,14 @@ MedHarness follows [Semantic Versioning](https://semver.org/): ## [Unreleased] +## [0.50.1] — 2026-10-03 + +### Fixed + +- `possible_duplicate` no longer fires on the CR being planned (intake creates it, the model does + not) or against `cancelled` and `rejected` items, so re-running intake for an issue whose earlier + CRs were cancelled no longer warns that the new CR reads like the old one. + ## [0.50.0] — 2026-10-03 ### Added diff --git a/medharness/services/design_validation.py b/medharness/services/design_validation.py index c5653b6d..2974c7a0 100644 --- a/medharness/services/design_validation.py +++ b/medharness/services/design_validation.py @@ -592,8 +592,15 @@ def _check_impact_analysis(cr_item: dict | None, items: list[dict], config, return errors or impact_analysis_closure_errors(cr_item, items, config, changed_items) +WITHDRAWN_STATES = {"cancelled", "rejected"} + + def check_near_duplicates(dhf_path: Path, changed_items: dict[str, list[str]]) -> list[dict]: - """Warnings for created items that read like an existing item of the same type.""" + """Warnings for created items that read like an existing item of the same type. + + The CR being planned is created by intake, not by the model, and a withdrawn item is + not something to update instead, so neither is compared. + """ from medharness.services.impact import NEAR_DUPLICATE_RATIO, closest_same_type _api, _ = _load_api() @@ -605,9 +612,12 @@ def check_near_duplicates(dhf_path: Path, changed_items: dict[str, list[str]]) - return [] by_id = {it["id"]: it for it in items} created = set(changed_items.get("created", [])) + withdrawn = {it["id"] for it in items if str(it.get("status") or "") in WITHDRAWN_STATES} warnings = [] for uid in sorted(created): - closest = closest_same_type(by_id[uid], items, created) if uid in by_id else None + if uid not in by_id or by_id[uid].get("type") == "CR": + continue + closest = closest_same_type(by_id[uid], items, created | withdrawn) if closest and closest[1] >= NEAR_DUPLICATE_RATIO: warnings.append({ "code": "possible_duplicate", diff --git a/pyproject.toml b/pyproject.toml index 2a9c08f3..06471282 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "medharness" -version = "0.50.0" +version = "0.50.1" description = "Open-source design-controlled development infrastructure for medical device and SaMD teams — includes dhfkit" readme = "README.md" license = { text = "MIT" } diff --git a/tests/integration/test_ai_stages_end_to_end.py b/tests/integration/test_ai_stages_end_to_end.py index adda8e86..f7cef45b 100644 --- a/tests/integration/test_ai_stages_end_to_end.py +++ b/tests/integration/test_ai_stages_end_to_end.py @@ -340,6 +340,23 @@ def test_an_existing_item_that_gains_many_lines_is_a_warning(self, project: Path def test_a_one_line_change_to_an_existing_item_is_not(self, project: Path, tmp_path: Path) -> None: assert self._edit_swdd(tmp_path, "One changed line.") == [] + def _copy_of(self, uid: str, item_type: str, **extra) -> str: + item = json.loads(_run("item", "get", uid).stdout) + copy = {k: item[k] for k in ("title", "content", "category", "satisfies", "verification_method", + "verification_criteria", "description") if k in item} + return f"{MH} item create --type {item_type} --data " + _json({**copy, **extra}) + + def _duplicates(self, tmp_path: Path, commands: list[str]) -> list[dict]: + report = self._plan_and_run(tmp_path, commands + [self._cr()]) + return [w for w in report["warnings"] if w["code"] == "possible_duplicate"] + + def test_a_cr_that_reads_like_another_cr_is_not_a_possible_duplicate(self, project: Path, tmp_path: Path) -> None: + assert self._duplicates(tmp_path, [self._copy_of("CR-001", "CR")]) == [] + + def test_an_item_that_reads_like_a_cancelled_one_is_not_a_possible_duplicate(self, project: Path, tmp_path: Path) -> None: + cancel = f"{MH} item update SYS-001 --data " + _json({"status": "cancelled"}) + assert self._duplicates(tmp_path, [cancel, self._copy_of("SYS-001", "SYS")]) == [] + def test_a_rejected_cr_needs_no_record(self, project: Path, tmp_path: Path) -> None: reject = f"{MH} item update CR-001 --data " + _json( {"status": "rejected", "impact_assessment": "duplicate of CR-002"}) diff --git a/uv.lock b/uv.lock index 1098c723..dc5ebee6 100644 --- a/uv.lock +++ b/uv.lock @@ -508,7 +508,7 @@ wheels = [ [[package]] name = "medharness" -version = "0.50.0" +version = "0.50.1" source = { editable = "." } dependencies = [ { name = "click" },