Skip to content
Merged
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
10 changes: 5 additions & 5 deletions .github/workflows/ci-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,12 @@ jobs:
run: |
export PYTHONPATH="${PYTHONPATH}:${PWD}"
python -c "
from medharness.workflows.init import _scaffold_dhf
from medharness.scaffold import scaffold_dhf
from pathlib import Path
import tempfile
with tempfile.TemporaryDirectory() as tmp:
dhf_dir = Path(tmp) / 'test-dhf'
_scaffold_dhf(dhf_dir)
scaffold_dhf(dhf_dir)
assert (dhf_dir / 'DHF' / 'config' / 'global.yaml').exists()
assert not (dhf_dir / 'DHF' / 'config' / 'doc_types').exists() # defaults are inherited
assert (dhf_dir / 'DHF' / 'items').exists()
Expand Down Expand Up @@ -152,13 +152,13 @@ jobs:
run: |
export PYTHONPATH="${PYTHONPATH}:${PWD}"
python -c "
from medharness.workflows.init import _scaffold_dhf, _replace_placeholders
from medharness.scaffold import scaffold_dhf, replace_placeholders
from pathlib import Path
import tempfile, subprocess, sys
with tempfile.TemporaryDirectory() as tmp:
dhf_dir = Path(tmp) / 'starter-dhf'
_scaffold_dhf(dhf_dir)
_replace_placeholders(dhf_dir, 'Test Project')
scaffold_dhf(dhf_dir)
replace_placeholders(dhf_dir, 'Test Project')
dhf = str(dhf_dir / 'DHF')
subprocess.run([sys.executable, '-m', 'medharness', '--dhf', dhf, 'verify', 'dhf'], check=True)
# The starter items say 'Replace with your own': strict refuses them, so they cannot ship.
Expand Down
17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ MedHarness follows [Semantic Versioning](https://semver.org/):

## [Unreleased]

## [0.54.1] — 2026-10-04

### Fixed

- **The code review's fix pass now carries the issues it must fix.** The prompt said "the review above",
which only a model resuming the session could see; a model from another provider (no sessions) was
told to fix nothing in particular. The issues are in the prompt.
- The design review's "already checked" note no longer mentions a spec file that no stage writes.

### Changed

- `medharness/workflows/` is gone: `init.py` is `medharness/scaffold.py` (its test helpers lose the
underscore: `scaffold_dhf`, `replace_placeholders`) and `cr_state.py` is `medharness/services/cr_state.py`.
Nothing in it was a workflow. No behaviour change.

## [0.54.0] — 2026-10-04

### Changed
Expand Down Expand Up @@ -2976,7 +2991,7 @@ a compliance record is the defect class that matters most.

### Internal

- `dhfkit/tests/test_document_generation.py` imported `medharness.workflows.init`,
- `dhfkit/tests/test_document_generation.py` imported `medharness.scaffold`,
breaking the `dhfkit` → `medharness` boundary and the ability to run dhfkit's
suite standalone. It now builds its fixture from the bundled templates
directly. A check over the whole package confirms no such import remains.
Expand Down
6 changes: 3 additions & 3 deletions dhfkit/tests/test_cr_closure_criteria.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import pytest

import dhfkit.api as api
from medharness.workflows.init import _replace_placeholders, _scaffold_dhf
from medharness.scaffold import replace_placeholders, scaffold_dhf

CLOSURE = {
"implementation_notes": "Toolbar updated.",
Expand All @@ -31,8 +31,8 @@

@pytest.fixture
def cr(tmp_path: Path) -> tuple[Path, str]:
_scaffold_dhf(tmp_path)
_replace_placeholders(tmp_path, "Closure")
scaffold_dhf(tmp_path)
replace_placeholders(tmp_path, "Closure")
dhf = tmp_path / "DHF"
for state in ("design", "develop"):
api.transition_item(dhf, "CR-001", state)
Expand Down
14 changes: 7 additions & 7 deletions dhfkit/tests/test_dangling_covers_the_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from dhfkit.item_store import ItemStore
from dhfkit.traceability import find_dangling_links
from dhfkit.models.config import ProjectConfig
from medharness.workflows.init import _replace_placeholders, _scaffold_dhf
from medharness.scaffold import replace_placeholders, scaffold_dhf

TEMPLATES = Path(__file__).resolve().parents[1] / "templates" / "config" / "doc_types"

Expand All @@ -51,8 +51,8 @@ def test_the_scan_found_fields() -> None:

@pytest.mark.parametrize("field,code", FIELDS, ids=[f for f, _ in FIELDS])
def test_a_dangling_link_is_detected(field: str, code: str, tmp_path: Path) -> None:
_scaffold_dhf(tmp_path)
_replace_placeholders(tmp_path, "Dangling")
scaffold_dhf(tmp_path)
replace_placeholders(tmp_path, "Dangling")
dhf = tmp_path / "DHF"

target = next(((dhf / "items").rglob(f"{code}-*.yaml")), None)
Expand All @@ -78,15 +78,15 @@ def test_a_dangling_link_is_detected(field: str, code: str, tmp_path: Path) -> N

class TestTheFieldSetComesFromTheSchema:
def test_config_reports_every_declared_relationship(self, tmp_path: Path) -> None:
_scaffold_dhf(tmp_path)
_replace_placeholders(tmp_path, "Derived")
scaffold_dhf(tmp_path)
replace_placeholders(tmp_path, "Derived")
config = ProjectConfig.load(tmp_path / "DHF" / "config")
assert config.relationship_fields() == {f for f, _ in FIELDS}

def test_a_project_defined_field_is_picked_up(self, tmp_path: Path) -> None:
"""The point of deriving: a field this package has never heard of."""
_scaffold_dhf(tmp_path)
_replace_placeholders(tmp_path, "Custom")
scaffold_dhf(tmp_path)
replace_placeholders(tmp_path, "Custom")
dhf = tmp_path / "DHF"
from dhfkit.paths import DEFAULT_CONFIG_DIR

Expand Down
6 changes: 3 additions & 3 deletions dhfkit/tests/test_duplicate_item_ids.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@

import dhfkit.api as api
from dhfkit.item_store import ItemStore
from medharness.workflows.init import _replace_placeholders, _scaffold_dhf
from medharness.scaffold import replace_placeholders, scaffold_dhf


@pytest.fixture
def dhf(tmp_path: Path) -> Path:
_scaffold_dhf(tmp_path)
_replace_placeholders(tmp_path, "Duplicates")
scaffold_dhf(tmp_path)
replace_placeholders(tmp_path, "Duplicates")
return tmp_path / "DHF"


Expand Down
10 changes: 5 additions & 5 deletions dhfkit/tests/test_ids_are_never_reused.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import pytest

import dhfkit.api as api
from medharness.workflows.init import _replace_placeholders, _scaffold_dhf
from medharness.scaffold import replace_placeholders, scaffold_dhf


def _git(root: Path, *args: str) -> None:
Expand All @@ -31,8 +31,8 @@ def _commit(root: Path, message: str) -> None:

@pytest.fixture
def repo(tmp_path: Path) -> Path:
_scaffold_dhf(tmp_path)
_replace_placeholders(tmp_path, "Reuse")
scaffold_dhf(tmp_path)
replace_placeholders(tmp_path, "Reuse")
_git(tmp_path, "init", "-q")
_commit(tmp_path, "scaffold")
return tmp_path
Expand Down Expand Up @@ -81,8 +81,8 @@ def test_ids_are_still_sequential_without_deletions(self, repo: Path) -> None:
class TestOutsideGit:
def test_a_dhf_with_no_repository_still_creates_items(self, tmp_path: Path) -> None:
"""History is unavailable there; behave as before rather than fail."""
_scaffold_dhf(tmp_path)
_replace_placeholders(tmp_path, "NoGit")
scaffold_dhf(tmp_path)
replace_placeholders(tmp_path, "NoGit")
dhf = tmp_path / "DHF"
assert _new_srs(dhf, "a").startswith("SRS-")
assert _new_srs(dhf, "b").startswith("SRS-")
10 changes: 5 additions & 5 deletions dhfkit/tests/test_item_writes_are_atomic.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@

import dhfkit.api as api
from dhfkit.item_store import ItemStore
from medharness.workflows.init import _replace_placeholders, _scaffold_dhf
from medharness.scaffold import replace_placeholders, scaffold_dhf


@pytest.fixture
def dhf(tmp_path: Path) -> Path:
_scaffold_dhf(tmp_path)
_replace_placeholders(tmp_path, "Atomic")
scaffold_dhf(tmp_path)
replace_placeholders(tmp_path, "Atomic")
return tmp_path / "DHF"


Expand Down Expand Up @@ -86,8 +86,8 @@ class TestDhfkitDoesNotCommit:
"""

def _repo(self, tmp_path: Path) -> Path:
_scaffold_dhf(tmp_path)
_replace_placeholders(tmp_path, "Labels")
scaffold_dhf(tmp_path)
replace_placeholders(tmp_path, "Labels")
for args in (["init", "-q"], ["add", "-A"],
["-c", "user.email=t@e", "-c", "user.name=t",
"commit", "-qm", "scaffold"]):
Expand Down
18 changes: 9 additions & 9 deletions dhfkit/tests/test_lifecycle_states_resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import yaml

import dhfkit.api as api
from medharness.workflows.init import _replace_placeholders, _scaffold_dhf
from medharness.scaffold import replace_placeholders, scaffold_dhf

TEMPLATES = Path(__file__).resolve().parents[1] / "templates" / "config"

Expand Down Expand Up @@ -59,8 +59,8 @@ class TestTheScaffoldedCRCanReachCompleted:

def test_the_full_path(self, tmp_path: Path) -> None:
"""Closing needs the record that closure is supposed to mean."""
_scaffold_dhf(tmp_path)
_replace_placeholders(tmp_path, "Lifecycle")
scaffold_dhf(tmp_path)
replace_placeholders(tmp_path, "Lifecycle")
dhf = tmp_path / "DHF"
for state in ("design", "develop"):
api.transition_item(dhf, "CR-001", state)
Expand All @@ -83,22 +83,22 @@ def test_updating_a_field_does_not_undo_the_lifecycle(self, tmp_path: Path) -> N
Filling in a CR's required fields sent it back to `new`, so the criteria
above could never be satisfied — the act of qualifying disqualified it.
"""
_scaffold_dhf(tmp_path)
_replace_placeholders(tmp_path, "Lifecycle")
scaffold_dhf(tmp_path)
replace_placeholders(tmp_path, "Lifecycle")
dhf = tmp_path / "DHF"
api.transition_item(dhf, "CR-001", "design")
api.update_item(dhf, "CR-001", {"implementation_notes": "x"})
assert api.get_item(dhf, "CR-001")["status"] == "design"

def test_an_undefined_state_is_still_refused(self, tmp_path: Path) -> None:
"""Widening the state list must not make the machine permissive."""
_scaffold_dhf(tmp_path)
_replace_placeholders(tmp_path, "Lifecycle")
scaffold_dhf(tmp_path)
replace_placeholders(tmp_path, "Lifecycle")
with pytest.raises(ValueError):
api.transition_item(tmp_path / "DHF", "CR-001", "nonexistent")

def test_skipping_a_state_is_still_refused(self, tmp_path: Path) -> None:
_scaffold_dhf(tmp_path)
_replace_placeholders(tmp_path, "Lifecycle")
scaffold_dhf(tmp_path)
replace_placeholders(tmp_path, "Lifecycle")
with pytest.raises(ValueError):
api.transition_item(tmp_path / "DHF", "CR-001", "completed")
2 changes: 1 addition & 1 deletion dhfkit/tests/test_network_calls_have_timeouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def _text_io_without_encoding() -> list[tuple[str, int, str]]:


def test_the_encoding_scan_sees_real_calls() -> None:
text = (ROOT / "medharness" / "workflows" / "init.py").read_text(encoding="utf-8")
text = (ROOT / "medharness" / "scaffold.py").read_text(encoding="utf-8")
assert "read_text(" in text, "the probe file no longer does text IO"


Expand Down
6 changes: 3 additions & 3 deletions dhfkit/tests/test_stale_global_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@
import yaml

import dhfkit.api as api
from medharness.workflows.init import _replace_placeholders, _scaffold_dhf
from medharness.scaffold import replace_placeholders, scaffold_dhf


@pytest.fixture
def aged(tmp_path: Path) -> Path:
"""A DHF whose own lifecycle override predates the states its doc types use."""
from dhfkit.paths import DEFAULT_CONFIG_DIR

_scaffold_dhf(tmp_path)
_replace_placeholders(tmp_path, "Aged")
scaffold_dhf(tmp_path)
replace_placeholders(tmp_path, "Aged")
dhf = tmp_path / "DHF"
config = dhf / "config" / "global.yaml"
data = yaml.safe_load(config.read_text())
Expand Down
4 changes: 2 additions & 2 deletions medharness/cli/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def change_plan(ctx: click.Context, cr_id: str, pr_number: int | None, since_ref
pushes to that PR, revising when a reviewer asked for changes.
"""
from medharness.services.cr_generation import generate_dhf # noqa: PLC0415
from medharness.workflows.cr_state import assert_cr_active # noqa: PLC0415
from medharness.services.cr_state import assert_cr_active # noqa: PLC0415
dhf: Path = ctx.obj["dhf"]
try:
store = open_store(ctx.obj["dhf"])
Expand Down Expand Up @@ -167,7 +167,7 @@ def change_implement(ctx: click.Context, cr_id: str, pr_number: int | None,
pushes to that PR, revising when a reviewer asked for changes.
"""
from medharness.services.cr_generation import generate_code # noqa: PLC0415
from medharness.workflows.cr_state import assert_cr_active # noqa: PLC0415
from medharness.services.cr_state import assert_cr_active # noqa: PLC0415
dhf: Path = ctx.obj["dhf"]
try:
store = open_store(ctx.obj["dhf"])
Expand Down
2 changes: 1 addition & 1 deletion medharness/cli/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ def init_cmd() -> None:
Takes no prompts — the project name is derived from the directory name.
Existing files are not overwritten.
"""
from medharness.workflows.init import run_init
from medharness.scaffold import run_init
click.echo(json.dumps(run_init()))
8 changes: 4 additions & 4 deletions medharness/workflows/init.py → medharness/scaffold.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
# DHF scaffold
# ---------------------------------------------------------------------------

def _scaffold_dhf(project_dir: Path) -> None:
def scaffold_dhf(project_dir: Path) -> None:
"""Create DHF structure inside project_dir from bundled templates."""
project_dir.mkdir(parents=True, exist_ok=True)

Expand Down Expand Up @@ -93,7 +93,7 @@ def _substitutable_files(project_dir: Path):
yield Path(dirpath) / filename


def _replace_placeholders(project_dir: Path, project_name: str) -> None:
def replace_placeholders(project_dir: Path, project_name: str) -> None:
"""Substitute template placeholders in scaffolded content."""
try:
medharness_version = pkg_version("medharness")
Expand Down Expand Up @@ -252,8 +252,8 @@ def say(text: str = "", **style) -> None:

before = {f for f in project_dir.rglob("*") if f.is_file()}
steps = [
("Scaffold DHF structure", lambda: (_scaffold_dhf(project_dir),
_replace_placeholders(project_dir, project_name))),
("Scaffold DHF structure", lambda: (scaffold_dhf(project_dir),
replace_placeholders(project_dir, project_name))),
("Write AGENTS.md and CLAUDE.md", lambda: _write_agent_files(project_dir, project_name)),
("Write .gitignore", lambda: _write_gitignore(project_dir)),
]
Expand Down
6 changes: 3 additions & 3 deletions medharness/services/cr_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,11 +345,11 @@ def generate_code(
if code_review_verdict != "needs_revision" or review_cycle >= _MAX_CODE_REVIEW_CYCLES:
break

issues = "\n".join(f"- {issue}" for issue in review_data["issues"])
run.call(
f"run_code_fix_{review_cycle}",
f"The code review for {cr_id} found issues. "
f"Fix each issue flagged in the review above — modify only the affected files. "
f"Do not make unrelated changes.",
f"The code review for {cr_id} found issues:\n{issues}\n\n"
f"Fix each one — modify only the affected files. Do not make unrelated changes.",
develop_llm, critical=False,
)

Expand Down
File renamed without changes.
5 changes: 2 additions & 3 deletions medharness/services/stage_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,8 @@ def _augment_review_prompt(base: str, errors: list[dict]) -> str:
if not errors:
return base + (
"\n\n## Deterministic Checks (already passed)\n\n"
"Schema, traceability, and the presence of all spec `affected_items` "
"(or required `@links:` test annotations) have been verified "
"mechanically. Do not re-derive them — focus on judgment questions "
"Schema, traceability, and the CR's recorded `affected_items` "
"have been verified mechanically. Do not re-derive them — focus on judgment questions "
"that a script cannot answer."
)
residual = "\n".join(f"- {e.get('field', '?')}: {e.get('issue', '')}" for e in errors)
Expand Down
1 change: 0 additions & 1 deletion medharness/workflows/__init__.py

This file was deleted.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "medharness"
version = "0.54.0"
version = "0.54.1"
description = "Open-source design-controlled development infrastructure for medical device and SaMD teams — includes dhfkit"
readme = "README.md"
license = { text = "MIT" }
Expand Down
Loading
Loading