claude-dev-env 1.69.2 → 1.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,6 +16,7 @@ from autoconverge_report_constants.render_report_constants import (
16
16
  CAUSE_MUTED_STYLE,
17
17
  DEFAULT_FINDING_CATEGORY,
18
18
  DEFAULT_FINDING_SEVERITY,
19
+ DEFAULT_ISSUE_ICON,
19
20
  GITHUB_PR_URL_TEMPLATE,
20
21
  HTML_DOCTYPE,
21
22
  HTML_HEAD_TEMPLATE,
@@ -30,6 +31,7 @@ from autoconverge_report_constants.render_report_constants import (
30
31
  ISSUE_CLASS_FIELD_PLAINNAME,
31
32
  ISSUE_CLASS_FIELD_SEVERITY,
32
33
  ISSUE_CLASS_FIELD_STATUS,
34
+ ISSUE_ICON_BY_CATEGORY,
33
35
  JOURNAL_SIBLING_SUBAGENTS,
34
36
  JOURNAL_SIBLING_WORKFLOWS,
35
37
  LABEL_CONVERGENCE_SUMMARY,
@@ -43,6 +45,9 @@ from autoconverge_report_constants.render_report_constants import (
43
45
  SCENE_FIELD_CONDITION,
44
46
  SCENE_FIELD_RESULT,
45
47
  SCENE_FIELD_TRIGGER,
48
+ SCORECARD_LABEL_CAUGHT,
49
+ SCORECARD_LABEL_REMAINING,
50
+ SCORECARD_LABEL_ROUNDS,
46
51
  SEVERITY_SORT_RANK,
47
52
  SHORT_SHA_LENGTH,
48
53
  STATUS_LABEL_BY_VALUE,
@@ -456,6 +461,30 @@ def _render_verdict_banner(
456
461
  )
457
462
 
458
463
 
464
+ def _render_scorecard(run_data: RunData, round_count: int) -> str:
465
+ """Return the at-a-glance scorecard: findings caught, rounds, and zero remaining.
466
+
467
+ Args:
468
+ run_data: Aggregated metrics from the journal.
469
+ round_count: Total number of convergence rounds.
470
+
471
+ Returns:
472
+ An HTML .scorecard grid of three .stat tiles; the remaining tile is marked
473
+ .good and reads zero, since the report renders only on a converged run.
474
+ """
475
+ remaining_count = 0
476
+ return (
477
+ '<div class="scorecard">'
478
+ f'<div class="stat"><div class="stat-num">{run_data.total_finding_count}</div>'
479
+ f'<div class="stat-label">{SCORECARD_LABEL_CAUGHT}</div></div>'
480
+ f'<div class="stat"><div class="stat-num">{round_count}</div>'
481
+ f'<div class="stat-label">{SCORECARD_LABEL_ROUNDS}</div></div>'
482
+ f'<div class="stat good"><div class="stat-num">{remaining_count}</div>'
483
+ f'<div class="stat-label">{SCORECARD_LABEL_REMAINING}</div></div>'
484
+ "</div>"
485
+ )
486
+
487
+
459
488
  def _render_scene_row(scene: dict, is_problem: bool) -> str:
460
489
  """Return one .scene row plus its caption for a problem-or-fix scene.
461
490
 
@@ -642,20 +671,26 @@ def _issue_class_sort_key(issue_class: dict) -> tuple[int, int]:
642
671
 
643
672
 
644
673
  def _render_issue_class_heading(issue_class: dict) -> str:
645
- """Return the per-class heading with the plain bug name and an occurrence count.
674
+ """Return the per-class heading with a category icon, plain name, and count.
646
675
 
647
676
  Args:
648
677
  issue_class: One issue-class dict from the summary.
649
678
 
650
679
  Returns:
651
- An HTML .bug-head block with the plain bug name and a finding-count chip.
680
+ An HTML .bug-head block: a category icon and the plain bug name grouped in
681
+ a .bug-title span, plus a finding-count chip.
652
682
  """
653
683
  plain_name = html.escape(str(issue_class.get(ISSUE_CLASS_FIELD_PLAINNAME, "")))
654
684
  count = _coerce_count(issue_class.get(ISSUE_CLASS_FIELD_COUNT, 0))
655
685
  count_phrase = html.escape(_pluralize(count, "finding", "findings"))
686
+ category = str(issue_class.get(ISSUE_CLASS_FIELD_CATEGORY, CATEGORY_BUG))
687
+ icon = ISSUE_ICON_BY_CATEGORY.get(category, DEFAULT_ISSUE_ICON)
656
688
  return (
657
689
  '<div class="bug-head">'
690
+ '<span class="bug-title">'
691
+ f'<span class="bug-icon">{icon}</span>'
658
692
  f'<span class="bug-name">{plain_name}</span>'
693
+ "</span>"
659
694
  f'<span class="bug-count">{count_phrase}</span>'
660
695
  "</div>"
661
696
  )
@@ -792,9 +827,10 @@ def _render_summary_body(
792
827
  final_sha_short: First eight characters of the final commit sha.
793
828
 
794
829
  Returns:
795
- An HTML body fragment: verdict banner, problem/fix cards, then a single
796
- caught section that opens with a run-stats lead line and holds the
797
- issue-class before/after panels, followed by the collapsed appendix.
830
+ An HTML body fragment: verdict banner, an at-a-glance scorecard,
831
+ problem/fix cards, then a single caught section that opens with a
832
+ run-stats lead line and holds the issue-class before/after panels,
833
+ followed by the collapsed appendix.
798
834
  """
799
835
  convergence_summary = run_data.convergence_summary
800
836
  if convergence_summary is None:
@@ -803,12 +839,14 @@ def _render_summary_body(
803
839
  verdict_banner = _render_verdict_banner(
804
840
  convergence_summary, run_data, final_sha_short
805
841
  )
842
+ scorecard = _render_scorecard(run_data, round_count)
806
843
  pf_grid = _render_pf_grid(convergence_summary)
807
844
  caught_lead = _render_caught_lead(convergence_summary, run_data, round_count)
808
845
  issue_panels = _render_issue_class_panels(convergence_summary)
809
846
  appendix = _render_appendix(run_data.all_distinct_findings)
810
847
  return (
811
848
  f"{verdict_banner}"
849
+ f"{scorecard}"
812
850
  f"<h2>What this PR does</h2>{pf_grid}"
813
851
  f"<h2>What was caught &mdash; and how it looked</h2>{caught_lead}{issue_panels}"
814
852
  f"{appendix}"
@@ -138,6 +138,49 @@ def test_cli_renders_verdict_banner_with_python_computed_vsub(tmp_path: Path) ->
138
138
  assert "final commit 7c2f420c" in html_content
139
139
 
140
140
 
141
+ def test_cli_renders_scorecard_with_caught_rounds_and_zero_remaining(
142
+ tmp_path: Path,
143
+ ) -> None:
144
+ """Should render an at-a-glance scorecard: findings caught, rounds, and zero left."""
145
+ out_path = tmp_path / "report-scorecard.html"
146
+
147
+ completed = _render_cli(FIXTURE_JOURNAL, out_path)
148
+ assert completed.returncode == 0, f"CLI failed:\n{completed.stderr}"
149
+
150
+ html_content = out_path.read_text(encoding="utf-8")
151
+ assert 'class="scorecard"' in html_content
152
+ assert 'class="stat good"' in html_content
153
+ assert f'<div class="stat-num">{EXPECTED_TOTAL_FINDINGS}</div>' in html_content
154
+ assert f'<div class="stat-num">{EXPECTED_ROUND_COUNT}</div>' in html_content
155
+ assert '<div class="stat-num">0</div>' in html_content
156
+ assert ">caught</div>" in html_content
157
+ assert ">rounds</div>" in html_content
158
+ assert ">left</div>" in html_content
159
+
160
+
161
+ def test_render_issue_class_heading_carries_a_category_icon() -> None:
162
+ """Should prefix the plain name with the icon matching the issue category."""
163
+ bug_heading = render_report._render_issue_class_heading(
164
+ {"plainName": "A plain symptom", "count": 2, "category": "bug"}
165
+ )
166
+ assert 'class="bug-title"' in bug_heading
167
+ assert 'class="bug-icon"' in bug_heading
168
+ assert render_report.ISSUE_ICON_BY_CATEGORY["bug"] in bug_heading
169
+
170
+ standard_heading = render_report._render_issue_class_heading(
171
+ {"plainName": "A standard symptom", "count": 1, "category": "code-standard"}
172
+ )
173
+ assert render_report.ISSUE_ICON_BY_CATEGORY["code-standard"] in standard_heading
174
+
175
+
176
+ def test_render_issue_class_heading_uses_default_icon_for_unknown_category() -> None:
177
+ """Should fall back to the default icon when the category has no mapped icon."""
178
+ heading = render_report._render_issue_class_heading(
179
+ {"plainName": "An unmapped symptom", "count": 1, "category": "mystery"}
180
+ )
181
+ assert render_report.DEFAULT_ISSUE_ICON in heading
182
+
183
+
141
184
  def test_cli_renders_problem_and_fix_scene_cards(tmp_path: Path) -> None:
142
185
  """Should draw problem and fix scene cards with trigger, result, and caption."""
143
186
  out_path = tmp_path / "report-scenes.html"