diff --git a/changes/325.fix b/changes/325.fix
new file mode 100644
index 0000000..cec83f8
--- /dev/null
+++ b/changes/325.fix
@@ -0,0 +1 @@
+Phase status dots in the HTML report now reflect the structured verdict for verify and review phases. A verify phase with verdict ``FAIL`` shows a red dot even when its execution status is ``completed``; a review phase with verdict ``REWORK`` shows a yellow dot; and ``approve``/``pass``/``pass-with-follow-ups`` show a green dot. Hard execution failures (``failed``, ``skipped``, ``superseded``) still take priority over any verdict.
diff --git a/src/raki/report/templates/report.html.j2 b/src/raki/report/templates/report.html.j2
index 59f4a73..100284d 100644
--- a/src/raki/report/templates/report.html.j2
+++ b/src/raki/report/templates/report.html.j2
@@ -1381,7 +1381,29 @@
{% if sorted_phases %}
{% for phase in sorted_phases %}
- {% set dot_class = 'rework' if phase.status == 'completed' and phase.generation > 1 else phase.status %}
+ {# Compute dot_class: failed/skipped/superseded status always wins; for verify/review
+ phases with structured output, use verdict to pick the color; otherwise fall back to
+ the existing gen>1 → rework logic. #}
+ {% if phase.status in ('failed', 'skipped', 'superseded') %}
+ {% set dot_class = phase.status %}
+ {% elif phase.output_structured and phase.output_structured is mapping
+ and phase.output_structured.verdict is defined
+ and phase.name == 'verify' %}
+ {% set dot_class = 'completed' if phase.output_structured.verdict | lower == 'pass' else 'failed' %}
+ {% elif phase.output_structured and phase.output_structured is mapping
+ and phase.output_structured.verdict is defined
+ and phase.name == 'review' %}
+ {% set verdict_lc = phase.output_structured.verdict | lower %}
+ {% if verdict_lc in ('approve', 'pass', 'pass-with-follow-ups') %}
+ {% set dot_class = 'completed' %}
+ {% elif verdict_lc == 'rework' %}
+ {% set dot_class = 'rework' %}
+ {% else %}
+ {% set dot_class = 'failed' %}
+ {% endif %}
+ {% else %}
+ {% set dot_class = 'rework' if phase.status == 'completed' and phase.generation > 1 else phase.status %}
+ {% endif %}
{{ phase.name }} (gen {{ phase.generation }})
diff --git a/tests/test_report_html.py b/tests/test_report_html.py
index a7df144..fb7d90e 100644
--- a/tests/test_report_html.py
+++ b/tests/test_report_html.py
@@ -2934,6 +2934,7 @@ def _make_report_with_phases(self, phases: list):
generation=ph["generation"],
status=ph["status"],
output="done",
+ output_structured=ph.get("output_structured"),
)
for ph in phases
]
@@ -3025,6 +3026,257 @@ def test_superseded_phase_css_rule_defined(self, tmp_path: Path) -> None:
# The CSS selector must be defined so the dot is actually styled.
assert ".phase-status-superseded" in content
+ # --- Ticket #325: dot color should reflect verdict, not execution status ---
+
+ def test_verify_pass_verdict_completed_gives_green_dot(self, tmp_path: Path) -> None:
+ """verify phase with verdict=PASS and status=completed should get green dot."""
+ from raki.report.html_report import write_html_report
+
+ report = self._make_report_with_phases(
+ [
+ {
+ "name": "verify",
+ "generation": 1,
+ "status": "completed",
+ "output_structured": {"verdict": "PASS"},
+ }
+ ]
+ )
+ output = tmp_path / "report.html"
+ write_html_report(report, output, include_sessions=True)
+ content = output.read_text()
+ assert 'class="phase-status phase-status-completed"' in content
+ assert 'class="phase-status phase-status-failed"' not in content
+ assert 'class="phase-status phase-status-rework"' not in content
+
+ def test_verify_fail_verdict_completed_gives_red_dot(self, tmp_path: Path) -> None:
+ """verify phase with verdict=FAIL and status=completed should get red dot."""
+ from raki.report.html_report import write_html_report
+
+ report = self._make_report_with_phases(
+ [
+ {
+ "name": "verify",
+ "generation": 1,
+ "status": "completed",
+ "output_structured": {"verdict": "FAIL"},
+ }
+ ]
+ )
+ output = tmp_path / "report.html"
+ write_html_report(report, output, include_sessions=True)
+ content = output.read_text()
+ assert 'class="phase-status phase-status-failed"' in content
+ assert 'class="phase-status phase-status-completed"' not in content
+ assert 'class="phase-status phase-status-rework"' not in content
+
+ def test_verify_pass_verdict_but_failed_status_gives_red_dot(self, tmp_path: Path) -> None:
+ """failed status takes priority over PASS verdict for verify phase."""
+ from raki.report.html_report import write_html_report
+
+ report = self._make_report_with_phases(
+ [
+ {
+ "name": "verify",
+ "generation": 1,
+ "status": "failed",
+ "output_structured": {"verdict": "PASS"},
+ }
+ ]
+ )
+ output = tmp_path / "report.html"
+ write_html_report(report, output, include_sessions=True)
+ content = output.read_text()
+ assert 'class="phase-status phase-status-failed"' in content
+ assert 'class="phase-status phase-status-completed"' not in content
+
+ def test_verify_pass_verdict_but_skipped_status_gives_skipped_dot(self, tmp_path: Path) -> None:
+ """skipped status takes priority over PASS verdict for verify phase."""
+ from raki.report.html_report import write_html_report
+
+ report = self._make_report_with_phases(
+ [
+ {
+ "name": "verify",
+ "generation": 1,
+ "status": "skipped",
+ "output_structured": {"verdict": "PASS"},
+ }
+ ]
+ )
+ output = tmp_path / "report.html"
+ write_html_report(report, output, include_sessions=True)
+ content = output.read_text()
+ assert 'class="phase-status phase-status-skipped"' in content
+ assert 'class="phase-status phase-status-completed"' not in content
+
+ def test_verify_pass_verdict_but_superseded_status_gives_superseded_dot(
+ self, tmp_path: Path
+ ) -> None:
+ """superseded status takes priority over PASS verdict for verify phase."""
+ from raki.report.html_report import write_html_report
+
+ report = self._make_report_with_phases(
+ [
+ {
+ "name": "verify",
+ "generation": 1,
+ "status": "superseded",
+ "output_structured": {"verdict": "PASS"},
+ }
+ ]
+ )
+ output = tmp_path / "report.html"
+ write_html_report(report, output, include_sessions=True)
+ content = output.read_text()
+ assert 'class="phase-status phase-status-superseded"' in content
+ assert 'class="phase-status phase-status-completed"' not in content
+
+ def test_review_approve_verdict_gives_green_dot(self, tmp_path: Path) -> None:
+ """review phase with verdict=approve and status=completed should get green dot."""
+ from raki.report.html_report import write_html_report
+
+ report = self._make_report_with_phases(
+ [
+ {
+ "name": "review",
+ "generation": 1,
+ "status": "completed",
+ "output_structured": {"verdict": "approve"},
+ }
+ ]
+ )
+ output = tmp_path / "report.html"
+ write_html_report(report, output, include_sessions=True)
+ content = output.read_text()
+ assert 'class="phase-status phase-status-completed"' in content
+ assert 'class="phase-status phase-status-failed"' not in content
+ assert 'class="phase-status phase-status-rework"' not in content
+
+ def test_review_pass_verdict_gives_green_dot(self, tmp_path: Path) -> None:
+ """review phase with verdict=pass and status=completed should get green dot."""
+ from raki.report.html_report import write_html_report
+
+ report = self._make_report_with_phases(
+ [
+ {
+ "name": "review",
+ "generation": 1,
+ "status": "completed",
+ "output_structured": {"verdict": "pass"},
+ }
+ ]
+ )
+ output = tmp_path / "report.html"
+ write_html_report(report, output, include_sessions=True)
+ content = output.read_text()
+ assert 'class="phase-status phase-status-completed"' in content
+ assert 'class="phase-status phase-status-failed"' not in content
+ assert 'class="phase-status phase-status-rework"' not in content
+
+ def test_review_pass_with_follow_ups_verdict_gives_green_dot(self, tmp_path: Path) -> None:
+ """review phase with verdict=pass-with-follow-ups should get green dot."""
+ from raki.report.html_report import write_html_report
+
+ report = self._make_report_with_phases(
+ [
+ {
+ "name": "review",
+ "generation": 1,
+ "status": "completed",
+ "output_structured": {"verdict": "pass-with-follow-ups"},
+ }
+ ]
+ )
+ output = tmp_path / "report.html"
+ write_html_report(report, output, include_sessions=True)
+ content = output.read_text()
+ assert 'class="phase-status phase-status-completed"' in content
+ assert 'class="phase-status phase-status-failed"' not in content
+ assert 'class="phase-status phase-status-rework"' not in content
+
+ def test_review_rework_verdict_gives_yellow_dot(self, tmp_path: Path) -> None:
+ """review phase with verdict=REWORK and status=completed should get yellow rework dot."""
+ from raki.report.html_report import write_html_report
+
+ report = self._make_report_with_phases(
+ [
+ {
+ "name": "review",
+ "generation": 1,
+ "status": "completed",
+ "output_structured": {"verdict": "REWORK"},
+ }
+ ]
+ )
+ output = tmp_path / "report.html"
+ write_html_report(report, output, include_sessions=True)
+ content = output.read_text()
+ assert 'class="phase-status phase-status-rework"' in content
+ assert 'class="phase-status phase-status-completed"' not in content
+ assert 'class="phase-status phase-status-failed"' not in content
+
+ def test_review_fail_verdict_gives_red_dot(self, tmp_path: Path) -> None:
+ """review phase with verdict=FAIL and status=completed should get red dot."""
+ from raki.report.html_report import write_html_report
+
+ report = self._make_report_with_phases(
+ [
+ {
+ "name": "review",
+ "generation": 1,
+ "status": "completed",
+ "output_structured": {"verdict": "FAIL"},
+ }
+ ]
+ )
+ output = tmp_path / "report.html"
+ write_html_report(report, output, include_sessions=True)
+ content = output.read_text()
+ assert 'class="phase-status phase-status-failed"' in content
+ assert 'class="phase-status phase-status-completed"' not in content
+ assert 'class="phase-status phase-status-rework"' not in content
+
+ def test_review_rework_verdict_but_failed_status_gives_red_dot(self, tmp_path: Path) -> None:
+ """failed status takes priority over REWORK verdict for review phase."""
+ from raki.report.html_report import write_html_report
+
+ report = self._make_report_with_phases(
+ [
+ {
+ "name": "review",
+ "generation": 1,
+ "status": "failed",
+ "output_structured": {"verdict": "REWORK"},
+ }
+ ]
+ )
+ output = tmp_path / "report.html"
+ write_html_report(report, output, include_sessions=True)
+ content = output.read_text()
+ assert 'class="phase-status phase-status-failed"' in content
+ assert 'class="phase-status phase-status-rework"' not in content
+
+ def test_non_verify_review_phase_uses_generation_logic(self, tmp_path: Path) -> None:
+ """implement phase with gen=1 should still use existing green/rework logic."""
+ from raki.report.html_report import write_html_report
+
+ report = self._make_report_with_phases(
+ [
+ {
+ "name": "implement",
+ "generation": 2,
+ "status": "completed",
+ "output_structured": None,
+ }
+ ]
+ )
+ output = tmp_path / "report.html"
+ write_html_report(report, output, include_sessions=True)
+ content = output.read_text()
+ assert 'class="phase-status phase-status-rework"' in content
+ assert 'class="phase-status phase-status-completed"' not in content
+
# --- Ticket #250: Structured drill-down sections ---