Skip to content

Commit 12a8b72

Browse files
vosesoftclaude
andcommitted
Release v0.3.0-alpha.19: resolve var_ids before pulling samples
Fixes bug #23 discovered while end-to-end testing alpha.18 on a real workbook. After a fresh run_simulation, get_sensitivity_ranking returned empty on the first call and full ranking on the immediate retry. Diagnostic: output lookup succeeded, output samples loaded, then every input lookup against the same handle returned None. Contract finding about MRService.dll: MRLIB_GetModelData appears to leave the handle in a state where subsequent MRLIB_GetModelVarID calls return None. The call sequence within one open handle must be all GetModelVarID calls first, then all GetModelData calls. Interleaving is unsafe. Fix: every reader that interleaves lookups with sample fetches now resolves all var_ids first, then pulls samples. Touches: - ResultsReader.get_sensitivity_ranking - ResultsReader.get_simulation_results - ResultsReader.get_correlation_matrix get_samples (single name, single fetch) is fine as-is. Verified live: first sensitivity-ranking call on NPV_of_a_capital _investment complete.xlsx now returns 6 driver entries (top driver Market growth, r = +0.72) instead of an empty list. 399 unit tests still pass — the regression test is the integration smoke run since the bug only manifests against the real DLL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 99940c8 commit 12a8b72

6 files changed

Lines changed: 62 additions & 15 deletions

File tree

CHANGELOG.md

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

55
## [Unreleased]
66

7+
## [0.3.0-alpha.19] — 2026-05-22
8+
9+
Fixes the bug-#23 lookup-after-samples regression discovered while end-to-end testing alpha.18 against a real workbook: `get_sensitivity_ranking` returned empty on the first call after `run_simulation`, then worked on the second identical call. The diagnostic trace was unambiguous — the output looked up fine, its samples loaded, then every input lookup against the same handle returned None.
10+
11+
### Fixed
12+
13+
- **MRLIB_GetModelData poisons subsequent MRLIB_GetModelVarID calls on the same handle.** Resolution: every reader that interleaves name lookups with sample fetches now resolves ALL var_ids first, THEN pulls samples. Applies to:
14+
- `ResultsReader.get_sensitivity_ranking` — was failing on the first call after a fresh simulation (output looked up, output samples loaded, all inputs then refused to resolve). Now: output lookup → all input lookups → all sample fetches → ranking.
15+
- `ResultsReader.get_simulation_results` — same risk on multi-output calls. Same fix.
16+
- `ResultsReader.get_correlation_matrix` — same risk on multi-name correlation requests. Same fix.
17+
- `ResultsReader.get_samples` (single name, single fetch — no change needed).
18+
19+
This is a *contract* finding about MRService.dll: the call sequence within one open handle must be all `GetModelVarID` calls first, then all `GetModelData` calls. Inverting them or interleaving is unsafe. Worth flagging upstream to the ModelRisk SDK team — and worth knowing for any future readers that touch the same surface.
20+
21+
### Why this matters end-to-end
22+
23+
Without alpha.19, the user's first sensitivity-ranking call after a sim returned silently empty. The LLM would tell them "no drivers detected" — completely wrong on a model that clearly has Spearman correlations up to +0.72. After alpha.19 the first call works correctly. Verified live against the `NPV_of_a_capital_investment complete.xlsx` workbook: 6 driver entries returned, top driver Market growth (r = +0.72), bottom three in noise territory.
24+
25+
### Tests
26+
27+
399 unit tests still pass — the bug only manifests against the real DLL, so the regression test is the integration smoke run.
28+
729
## [0.3.0-alpha.18] — 2026-05-22
830

931
Targeted experiment for the empty-`.vmrs` blocker surfaced by alpha.17's post-condition verification. The bridge correctly detected that `VoseStartSimulCustom12 + VoseGetDataSZ12` was producing `.vmrs` files with zero registered outputs — sim ran, file existed, but no variable metadata. Ribbon-driven simulations on the same workbook worked fine, suggesting the ribbon path threads an option the headless XLL path skipped.

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.0a18"
3+
version = "0.3.0a19"
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.0a18",
6+
"version": "0.3.0a19",
77
"packages": [
88
{
99
"registryType": "pypi",
1010
"identifier": "modelrisk-mcp",
11-
"version": "0.3.0a18",
11+
"version": "0.3.0a19",
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.0a18"
1+
__version__ = "0.3.0a19"

src/modelrisk_mcp/bridge/results.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,18 @@ def get_simulation_results(
124124
# this fallback just won't return anything sensible here.
125125
# Use the read_vmrs tool with explicit output_names for now.
126126
return []
127+
# Bug #23 (alpha.19): MRLIB_GetModelData appears to put the
128+
# handle into a state where subsequent MRLIB_GetModelVarID
129+
# calls return None — so we resolve EVERY name to a var_id
130+
# before pulling any samples. Without this, a fresh-from-
131+
# simulation .vmrs would silently lose all but the first
132+
# output (lookup → samples → lookup → fails → continue → ...).
133+
resolved: list[tuple[str, int]] = []
127134
for name in wanted:
128135
var_id = self._lookup_var_id(handle, name)
129-
if var_id is None:
130-
continue
136+
if var_id is not None:
137+
resolved.append((name, var_id))
138+
for name, var_id in resolved:
131139
samples = handle.get_samples(var_id)
132140
if not samples:
133141
continue
@@ -159,12 +167,18 @@ def get_correlation_matrix(
159167
return CorrelationMatrix()
160168
name_list = list(names)
161169
with self._mrservice.open_vmrs(vmrs) as handle:
162-
arrays: list[np.ndarray] = []
163-
ordered: list[str] = []
170+
# Bug #23 (alpha.19): resolve every name to a var_id BEFORE
171+
# touching get_samples. See get_simulation_results for the
172+
# full backstory — MRLIB_GetModelData poisons subsequent
173+
# MRLIB_GetModelVarID lookups on the same handle.
174+
resolved: list[tuple[str, int]] = []
164175
for name in name_list:
165176
var_id = self._lookup_var_id(handle, name)
166-
if var_id is None:
167-
continue
177+
if var_id is not None:
178+
resolved.append((name, var_id))
179+
arrays: list[np.ndarray] = []
180+
ordered: list[str] = []
181+
for name, var_id in resolved:
168182
samples = handle.get_samples(var_id)
169183
if not samples:
170184
continue
@@ -250,17 +264,28 @@ def get_sensitivity_ranking(
250264
if vmrs is None:
251265
raise SimulationFailedError("No .vmrs available.")
252266
with self._mrservice.open_vmrs(vmrs) as handle:
267+
# Bug #23 (alpha.19): resolve EVERY name to a var_id before
268+
# touching get_samples. MRLIB_GetModelData appears to leave
269+
# the handle in a state where subsequent
270+
# MRLIB_GetModelVarID calls return None — first observed
271+
# on a fresh-from-simulation .vmrs where the output looked
272+
# up fine, its samples loaded, then every input lookup
273+
# returned None and the sensitivity ranking came back
274+
# empty. Resolving all IDs first sidesteps the issue.
253275
out_id = self._lookup_var_id(handle, output_name)
254276
if out_id is None:
255277
raise SimulationFailedError(
256278
f"Output {output_name!r} not found in .vmrs."
257279
)
258-
out_samples = np.asarray(handle.get_samples(out_id), dtype=float)
259-
entries: list[SensitivityEntry] = []
280+
resolved_inputs: list[tuple[str, int]] = []
260281
for in_name in input_names:
261282
in_id = self._lookup_var_id(handle, in_name)
262-
if in_id is None:
263-
continue
283+
if in_id is not None:
284+
resolved_inputs.append((in_name, in_id))
285+
286+
out_samples = np.asarray(handle.get_samples(out_id), dtype=float)
287+
entries: list[SensitivityEntry] = []
288+
for in_name, in_id in resolved_inputs:
264289
in_samples = np.asarray(handle.get_samples(in_id), dtype=float)
265290
n = min(in_samples.size, out_samples.size)
266291
if n < 2:

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.0a18"
8+
assert __version__ == "0.3.0a19"
99

1010

1111
def test_server_name() -> None:

0 commit comments

Comments
 (0)