Skip to content

Commit c0cdc09

Browse files
vosesoftclaude
andcommitted
Release v0.3.0-alpha.22: drivers report layout consistency
Alpha.20 polished build_executive_report (narrow gutters at A/M, content in B-L) but I missed applying the same change to build_drivers_report. Live screenshot review caught the asymmetry — exec sheet had the polished gutter pattern, drivers sheet still had labels and tornado flush against column A. Fix: DriversReportBuilder now uses the same B-shifted layout — title band B:L, KEY FINDINGS at B4, tornado chart shifted right ~16pt to align with column B, driver-ranking table moved from G:J to H:K (post mid-gutter), HOW TO READ and RECOMMENDED ACTIONS narrative sections shifted to B with merges extended to L. 399 tests pass. 6 drivers-report tests updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ebb663b commit c0cdc09

8 files changed

Lines changed: 107 additions & 80 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ All notable changes to ModelRisk MCP. Follows [Keep a Changelog](https://keepach
44

55
## [Unreleased]
66

7+
## [0.3.0-alpha.22] — 2026-05-22
8+
9+
Layout consistency fix: alpha.20 polished `build_executive_report`'s layout (narrow gutters at A/M, content in B–L) but I missed applying the same change to `build_drivers_report`. Live screenshot review caught the asymmetry — exec sheet had the polished gutter pattern, drivers sheet still had labels and tornado flush against column A.
10+
11+
### Fixed
12+
13+
- **DriversReportBuilder now uses the same B-shifted layout as ExecutiveReportBuilder.** Title band B:L (was A:J), KEY FINDINGS at B4 (was A4), bullets in B (was A), tornado chart shifted right by ~16pt to align with column B, driver-ranking table moved from G:J to H:K (sits after the mid-gutter at column G), HOW TO READ THIS CHART and RECOMMENDED ACTIONS narrative sections shifted to B with merge ranges expanded to L. Recommendations rows put labels in B and values in C (was A/B); the value cell merges B-merged-to-L. Now both reports look like siblings.
14+
15+
### Tests
16+
17+
399 unit tests pass. 6 drivers-report tests updated to assert against the new column positions (B-shifted findings, H-K driver table, C-shifted recommendations).
18+
719
## [0.3.0-alpha.21] — 2026-05-22
820

921
Hotfix for a regression introduced by alpha.16's `_ModelRiskReports` helper sheet: the second run of `build_executive_report` (and `build_drivers_report`) failed with `Move method of Worksheet class failed` on real Excel.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "modelrisk-mcp"
3-
version = "0.3.0a21"
3+
version = "0.3.0a22"
44
description = "Open MCP server bridging Anthropic Claude (and any MCP-compatible client) with the ModelRisk Excel add-in."
55
readme = "README.md"
66
requires-python = ">=3.11"

server.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
"name": "io.github.vosesoftware/modelrisk-mcp",
44
"title": "ModelRisk",
55
"description": "Read, build, fit, and run Monte Carlo risk models in Excel through Vose Software's ModelRisk.",
6-
"version": "0.3.0a21",
6+
"version": "0.3.0a22",
77
"packages": [
88
{
99
"registryType": "pypi",
1010
"identifier": "modelrisk-mcp",
11-
"version": "0.3.0a21",
11+
"version": "0.3.0a22",
1212
"transport": {
1313
"type": "stdio"
1414
}

src/modelrisk_mcp/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.3.0a21"
1+
__version__ = "0.3.0a22"

src/modelrisk_mcp/bridge/reports.py

Lines changed: 66 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -986,10 +986,17 @@ def build(
986986

987987
@staticmethod
988988
def _set_column_widths(sheet: Any) -> None:
989+
"""alpha.22 layout: narrow gutters at A and M, content in B-L.
990+
Driver table lives at H-K (was G-J). Match the executive
991+
report's gutter pattern so the two reports feel like siblings."""
989992
widths = {
990-
"A": 24, "B": 14, "C": 14, "D": 14, "E": 14,
991-
"F": 4,
992-
"G": 20, "H": 12, "I": 12, "J": 12,
993+
"A": 2, # left gutter
994+
"B": 24, # findings + recommendation labels
995+
"C": 14, "D": 14, "E": 14, "F": 14,
996+
"G": 4, # mid gutter
997+
"H": 22, # driver-table Input column
998+
"I": 14, "J": 14, "K": 14, "L": 14,
999+
"M": 2, # right gutter
9931000
}
9941001
for col, w in widths.items():
9951002
try:
@@ -999,42 +1006,43 @@ def _set_column_widths(sheet: Any) -> None:
9991006

10001007
@staticmethod
10011008
def _write_title_band(sheet: Any, title: str, subtitle: str) -> None:
1002-
sheet.range("A1").value = title
1003-
sheet.range("A2").value = subtitle
1009+
sheet.range("B1").value = title
1010+
sheet.range("B2").value = subtitle
10041011
try:
1005-
sheet.range("A1:J1").merge()
1006-
sheet.range("A2:J2").merge()
1007-
band = sheet.range("A1:J2")
1012+
# Band spans B:L; A and M stay as narrow gutters.
1013+
sheet.range("B1:L1").merge()
1014+
sheet.range("B2:L2").merge()
1015+
band = sheet.range("B1:L2")
10081016
band.api.Interior.Color = _COLOR_TITLE_BG
10091017
band.api.Font.Color = _COLOR_TITLE_FG
1010-
sheet.range("A1").api.Font.Size = 18
1011-
sheet.range("A1").api.Font.Bold = True
1012-
sheet.range("A2").api.Font.Size = 11
1013-
sheet.range("A1").api.HorizontalAlignment = -4108
1014-
sheet.range("A2").api.HorizontalAlignment = -4108
1015-
sheet.range("A1").row_height = 28
1016-
sheet.range("A2").row_height = 18
1018+
sheet.range("B1").api.Font.Size = 18
1019+
sheet.range("B1").api.Font.Bold = True
1020+
sheet.range("B2").api.Font.Size = 11
1021+
sheet.range("B1").api.HorizontalAlignment = -4108
1022+
sheet.range("B2").api.HorizontalAlignment = -4108
1023+
sheet.range("B1").row_height = 28
1024+
sheet.range("B2").row_height = 18
10171025
except Exception:
10181026
pass
10191027

10201028
@staticmethod
10211029
def _write_findings(sheet: Any, findings: list[str]) -> None:
1022-
sheet.range(f"A{DriversReportBuilder.FINDINGS_HEADER_ROW}").value = (
1030+
sheet.range(f"B{DriversReportBuilder.FINDINGS_HEADER_ROW}").value = (
10231031
"KEY FINDINGS"
10241032
)
10251033
try:
10261034
r = DriversReportBuilder.FINDINGS_HEADER_ROW
1027-
sheet.range(f"A{r}:J{r}").api.Font.Bold = True
1028-
sheet.range(f"A{r}").api.Font.Size = 12
1029-
sheet.range(f"A{r}").api.Font.Color = _rgb(100, 100, 100)
1035+
sheet.range(f"B{r}:L{r}").api.Font.Bold = True
1036+
sheet.range(f"B{r}").api.Font.Size = 12
1037+
sheet.range(f"B{r}").api.Font.Color = _rgb(100, 100, 100)
10301038
except Exception:
10311039
pass
10321040
for i, finding in enumerate(findings, start=DriversReportBuilder.FINDINGS_FIRST_ROW):
1033-
sheet.range(f"A{i}").value = f"• {finding}"
1041+
sheet.range(f"B{i}").value = f"• {finding}"
10341042
try:
1035-
sheet.range(f"A{i}:J{i}").merge()
1036-
sheet.range(f"A{i}").api.WrapText = True
1037-
sheet.range(f"A{i}").row_height = 28
1043+
sheet.range(f"B{i}:L{i}").merge()
1044+
sheet.range(f"B{i}").api.WrapText = True
1045+
sheet.range(f"B{i}").row_height = 28
10381046
except Exception:
10391047
pass
10401048

@@ -1070,51 +1078,54 @@ def _write_tornado_chart(
10701078
helper_anchor_col=anchor,
10711079
driver_count=len(entries),
10721080
title=f"What moves {output_name}",
1073-
left=10, top=210, width=420, height=320,
1081+
# Shifted right ~16pt post-alpha.22 to align with the
1082+
# column-B content start (A is now the gutter).
1083+
left=26, top=210, width=440, height=320,
10741084
)
10751085
except Exception:
10761086
pass
10771087

10781088
@staticmethod
10791089
def _write_driver_table(sheet: Any, entries: list[Any]) -> None:
1080-
# Header
1090+
# alpha.22 layout: table moves from G:J to H:K so it lives
1091+
# past the mid-gutter at column G.
10811092
header_row = DriversReportBuilder.TABLE_HEADER_ROW
10821093
headers = ["Input", "Correlation (r)", "|r|", "Variance share"]
10831094
for i, label in enumerate(headers):
1084-
col = chr(ord("G") + i)
1095+
col = chr(ord("H") + i)
10851096
sheet.range(f"{col}{header_row}").value = label
10861097
try:
1087-
sheet.range(f"G{header_row}:J{header_row}").api.Font.Bold = True
1098+
sheet.range(f"H{header_row}:K{header_row}").api.Font.Bold = True
10881099
sheet.range(
1089-
f"G{header_row}:J{header_row}"
1100+
f"H{header_row}:K{header_row}"
10901101
).api.Interior.Color = _COLOR_BAND_LIGHT
10911102
except Exception:
10921103
pass
10931104

10941105
for idx, e in enumerate(entries, start=DriversReportBuilder.TABLE_DATA_ROW):
1095-
sheet.range(f"G{idx}").value = e.input_name
1096-
sheet.range(f"H{idx}").value = e.correlation
1097-
sheet.range(f"I{idx}").value = abs(e.correlation)
1098-
sheet.range(f"J{idx}").value = _variance_share(e.correlation)
1106+
sheet.range(f"H{idx}").value = e.input_name
1107+
sheet.range(f"I{idx}").value = e.correlation
1108+
sheet.range(f"J{idx}").value = abs(e.correlation)
1109+
sheet.range(f"K{idx}").value = _variance_share(e.correlation)
10991110
try:
1100-
sheet.range(f"H{idx}").api.NumberFormat = "0.000"
11011111
sheet.range(f"I{idx}").api.NumberFormat = "0.000"
1102-
sheet.range(f"J{idx}").api.NumberFormat = "0.0%"
1112+
sheet.range(f"J{idx}").api.NumberFormat = "0.000"
1113+
sheet.range(f"K{idx}").api.NumberFormat = "0.0%"
11031114
# Color the |r| cell by strength tier.
11041115
col_strength = _driver_strength_color(abs(e.correlation))
1105-
sheet.range(f"I{idx}").api.Font.Color = col_strength
1106-
sheet.range(f"I{idx}").api.Font.Bold = True
1116+
sheet.range(f"J{idx}").api.Font.Color = col_strength
1117+
sheet.range(f"J{idx}").api.Font.Bold = True
11071118
except Exception:
11081119
pass
11091120

11101121
@staticmethod
11111122
def _write_chart_explanation(sheet: Any) -> None:
11121123
row = DriversReportBuilder.EXPLAIN_HEADER_ROW
1113-
sheet.range(f"A{row}").value = "HOW TO READ THIS CHART"
1124+
sheet.range(f"B{row}").value = "HOW TO READ THIS CHART"
11141125
try:
1115-
sheet.range(f"A{row}").api.Font.Bold = True
1116-
sheet.range(f"A{row}").api.Font.Size = 12
1117-
sheet.range(f"A{row}").api.Font.Color = _rgb(100, 100, 100)
1126+
sheet.range(f"B{row}").api.Font.Bold = True
1127+
sheet.range(f"B{row}").api.Font.Size = 12
1128+
sheet.range(f"B{row}").api.Font.Color = _rgb(100, 100, 100)
11181129
except Exception:
11191130
pass
11201131
paragraphs = [
@@ -1138,12 +1149,12 @@ def _write_chart_explanation(sheet: Any) -> None:
11381149
),
11391150
]
11401151
for i, paragraph in enumerate(paragraphs, start=row + 1):
1141-
sheet.range(f"A{i}").value = paragraph
1152+
sheet.range(f"B{i}").value = paragraph
11421153
try:
1143-
sheet.range(f"A{i}:J{i}").merge()
1144-
sheet.range(f"A{i}").api.WrapText = True
1145-
sheet.range(f"A{i}").row_height = 32
1146-
sheet.range(f"A{i}").api.VerticalAlignment = -4160 # xlTop
1154+
sheet.range(f"B{i}:L{i}").merge()
1155+
sheet.range(f"B{i}").api.WrapText = True
1156+
sheet.range(f"B{i}").row_height = 32
1157+
sheet.range(f"B{i}").api.VerticalAlignment = -4160 # xlTop
11471158
except Exception:
11481159
pass
11491160

@@ -1152,11 +1163,11 @@ def _write_recommendations(
11521163
sheet: Any, recommendations: dict[str, list[str]],
11531164
) -> None:
11541165
row = DriversReportBuilder.RECOMMEND_HEADER_ROW
1155-
sheet.range(f"A{row}").value = "RECOMMENDED ACTIONS"
1166+
sheet.range(f"B{row}").value = "RECOMMENDED ACTIONS"
11561167
try:
1157-
sheet.range(f"A{row}").api.Font.Bold = True
1158-
sheet.range(f"A{row}").api.Font.Size = 12
1159-
sheet.range(f"A{row}").api.Font.Color = _rgb(100, 100, 100)
1168+
sheet.range(f"B{row}").api.Font.Bold = True
1169+
sheet.range(f"B{row}").api.Font.Size = 12
1170+
sheet.range(f"B{row}").api.Font.Color = _rgb(100, 100, 100)
11601171
except Exception:
11611172
pass
11621173
tier_rows = [
@@ -1165,16 +1176,16 @@ def _write_recommendations(
11651176
("Can be deprioritised:", "deprioritise", _COLOR_DRIVER_WEAK),
11661177
]
11671178
for i, (label, key, color) in enumerate(tier_rows, start=row + 1):
1168-
sheet.range(f"A{i}").value = label
1179+
sheet.range(f"B{i}").value = label
11691180
inputs = recommendations.get(key, [])
11701181
value = ", ".join(inputs) if inputs else "(none)"
1171-
sheet.range(f"B{i}").value = value
1182+
sheet.range(f"C{i}").value = value
11721183
try:
1173-
sheet.range(f"A{i}").api.Font.Bold = True
1174-
sheet.range(f"B{i}").api.Font.Color = color
11751184
sheet.range(f"B{i}").api.Font.Bold = True
1176-
sheet.range(f"B{i}:J{i}").merge()
1177-
sheet.range(f"B{i}").api.WrapText = True
1185+
sheet.range(f"C{i}").api.Font.Color = color
1186+
sheet.range(f"C{i}").api.Font.Bold = True
1187+
sheet.range(f"C{i}:L{i}").merge()
1188+
sheet.range(f"C{i}").api.WrapText = True
11781189
except Exception:
11791190
pass
11801191

tests/unit/test_reports.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -690,9 +690,11 @@ def test_builds_sheet_with_title_and_findings(self) -> None:
690690
)
691691
assert isinstance(result, DriversReportResult)
692692
sheet = book.sheets["Drivers"]
693-
assert "Profit" in sheet.cells["A1"]
694-
assert "5,000 iterations" in sheet.cells["A2"]
695-
assert sheet.cells["A4"] == "KEY FINDINGS"
693+
# alpha.22 layout: drivers report shifted to B-column gutter
694+
# pattern (matches the executive report).
695+
assert "Profit" in sheet.cells["B1"]
696+
assert "5,000 iterations" in sheet.cells["B2"]
697+
assert sheet.cells["B4"] == "KEY FINDINGS"
696698
assert result.drivers_analyzed == 3
697699
assert result.top_driver == "WidgetCost"
698700
assert result.top_correlation == -0.72
@@ -713,7 +715,7 @@ def test_findings_name_top_driver_with_direction(self) -> None:
713715
sheet = book.sheets["Drivers"]
714716
# First finding should mention the driver name + that it
715717
# LOWERS the output (negative correlation).
716-
first_finding_text = sheet.cells.get("A5", "") or ""
718+
first_finding_text = sheet.cells.get("B5", "") or ""
717719
assert "WidgetCost" in first_finding_text
718720
assert "lowers" in first_finding_text
719721
assert "Profit" in first_finding_text
@@ -733,7 +735,7 @@ def test_findings_mention_top_n_variance_share(self) -> None:
733735
iterations=1000,
734736
)
735737
sheet = book.sheets["Drivers"]
736-
coverage_finding = sheet.cells.get("A6", "") or ""
738+
coverage_finding = sheet.cells.get("B6", "") or ""
737739
assert "top 3" in coverage_finding.lower() or "top-3" in coverage_finding.lower() or "top " in coverage_finding.lower()
738740

739741
def test_driver_table_populated(self) -> None:
@@ -750,16 +752,16 @@ def test_driver_table_populated(self) -> None:
750752
iterations=1000,
751753
)
752754
sheet = book.sheets["Drivers"]
753-
# Header
754-
assert sheet.cells["G11"] == "Input"
755-
assert sheet.cells["H11"] == "Correlation (r)"
756-
assert sheet.cells["I11"] == "|r|"
757-
assert sheet.cells["J11"] == "Variance share"
755+
# Header — alpha.22 shifted driver table from G:J to H:K.
756+
assert sheet.cells["H11"] == "Input"
757+
assert sheet.cells["I11"] == "Correlation (r)"
758+
assert sheet.cells["J11"] == "|r|"
759+
assert sheet.cells["K11"] == "Variance share"
758760
# Data row 1 (strongest)
759-
assert sheet.cells["G12"] == "A"
760-
assert sheet.cells["H12"] == -0.7
761-
assert abs(sheet.cells["I12"] - 0.7) < 1e-9
762-
assert abs(sheet.cells["J12"] - 0.49) < 1e-9 # r² = 0.49
761+
assert sheet.cells["H12"] == "A"
762+
assert sheet.cells["I12"] == -0.7
763+
assert abs(sheet.cells["J12"] - 0.7) < 1e-9
764+
assert abs(sheet.cells["K12"] - 0.49) < 1e-9 # r² = 0.49
763765

764766
def test_recommendations_tier_by_correlation_strength(self) -> None:
765767
book = _FakeBook()
@@ -784,10 +786,12 @@ def test_recommendations_tier_by_correlation_strength(self) -> None:
784786
focus_row = DriversReportBuilder.RECOMMEND_HEADER_ROW + 1
785787
monitor_row = focus_row + 1
786788
deprioritise_row = focus_row + 2
787-
assert "StrongDriver" in sheet.cells[f"B{focus_row}"]
788-
assert "StrongDriver2" in sheet.cells[f"B{focus_row}"]
789-
assert "ModerateDriver" in sheet.cells[f"B{monitor_row}"]
790-
assert "WeakDriver" in sheet.cells[f"B{deprioritise_row}"]
789+
# alpha.22: recommendations values shifted from B → C
790+
# (label in B, value in C).
791+
assert "StrongDriver" in sheet.cells[f"C{focus_row}"]
792+
assert "StrongDriver2" in sheet.cells[f"C{focus_row}"]
793+
assert "ModerateDriver" in sheet.cells[f"C{monitor_row}"]
794+
assert "WeakDriver" in sheet.cells[f"C{deprioritise_row}"]
791795

792796
def test_empty_sensitivity_doesnt_crash(self) -> None:
793797
book = _FakeBook()
@@ -804,7 +808,7 @@ def test_empty_sensitivity_doesnt_crash(self) -> None:
804808
assert result.top_driver is None
805809
# A finding still gets written, explaining the empty result.
806810
sheet = book.sheets["Drivers"]
807-
first_finding = sheet.cells.get("A5", "")
811+
first_finding = sheet.cells.get("B5", "")
808812
assert "No drivers" in first_finding
809813

810814
def test_concentration_label(self) -> None:

tests/unit/test_server_boot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66

77
def test_version_is_set() -> None:
8-
assert __version__ == "0.3.0a21"
8+
assert __version__ == "0.3.0a22"
99

1010

1111
def test_server_name() -> None:

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)