Skip to content

MCP server: publish real per-tool JSON Schemas (#506) - #528

Merged
kgdunn merged 5 commits into
mainfrom
claude/process-improve-library-issues-hr7xxk-mcp
Aug 29, 2026
Merged

MCP server: publish real per-tool JSON Schemas (#506)#528
kgdunn merged 5 commits into
mainfrom
claude/process-improve-library-issues-hr7xxk-mcp

Conversation

@kgdunn

@kgdunn kgdunn commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fixes MCP server publishes no per-tool JSON Schema: every tool appears to clients as (**kwargs) #506: the MCP server registered every @tool_spec tool from an async def handler(**kwargs) closure and assigned __annotations__ / __defaults__ afterwards, which cannot synthesise a real signature, so all tools were published to MCP clients with an empty inputSchema. Tools are now registered as explicit mcp.server.mcpserver.tools.Tool objects whose parameters field, published verbatim by list_tools(), is exactly the registry's spec["input_schema"] from get_tool_specs(). Types, required vs optional, enums, bounds, descriptions, and anyOf unions (int | None, Literal[...]) all survive; nothing defaults to string.
  • The server now targets the MCP 2.x SDK (MCPServer, renamed from 1.x's FastMCP). The existing mcp>=1.0 pin already resolved to 2.x, under which the old from mcp.server.fastmcp import FastMCP failed at import (the tests silently skipped, and this was mypy's one standing error, now gone). The mcp extra pins mcp>=2.0. Argument validation is unchanged and stays in execute_tool_call, which validates against each tool's real pydantic input model (SEC-15); the hand-built FuncMetadata passes arguments through untouched.
  • mcp_server.py is removed from the .coveragerc omit list; the new tests bring it to 100% line and branch coverage, so the 92% gate is unaffected.
  • Drive-by from the tracking issue Tracking: minor findings from the 2026-08 repo-wide audit (triage list) #513: the concurrent-dispatch test asserted wall-clock time (elapsed < 1.7 * _SLEEP) and could flake on loaded CI runners; it now proves the overlap with a threading.Barrier both in-flight calls must reach, which a serialised dispatch can never satisfy.

Version is bumped to 1.73.2 (PATCH), with CITATION.cff and CHANGELOG.md updated in the same commit. The version assumes the earlier queued PRs (through 1.73.1) merge first.

Test plan

  • New test test_published_schemas_match_registry_exactly drives the real registration path (create_server() then list_tools()) and asserts, for every registered tool, that the published inputSchema and description equal get_tool_specs()'s entries.
  • New test test_optional_and_union_parameters_survive asserts anyOf unions and required lists reach the published schemas (guards against regressing to signature introspection).
  • New test test_call_tool_dispatches_raw_arguments exercises the real call_tool() path end to end.
  • Handler error paths (curated ToolSafetyError payload, generic non-leaking error, safe-mode routing, non-dict results) and main() covered; mcp_server.py at 100% coverage.
  • uv run pytest tests/test_mcp_server.py tests/test_tool_spec.py --no-cov -q: 87 passed. Requires the mcp extra (uv sync --dev --all-extras); the tests skip without it via pytest.importorskip("mcp.server.mcpserver").
  • Full suite: 2704 passed locally, 94% total coverage (gate is 92%). The only 2 failures are network-dependent dataset downloads (test_oildoe_loads, test_distillateflow_loads) blocked by this sandbox's proxy (openmv.net returns 403); they are unrelated to this change.
  • uv run ruff check . and uv run ruff format --check . both pass.
  • uv run mypy src/process_improve: no issues (previously 1 error in this module).

Checklist

  • Version bumped in pyproject.toml (PATCH for fixes/docs/config, MINOR for new features)
  • Tests added or updated where relevant
  • ruff check . passes
  • CHANGELOG.md updated

Generated by Claude Code

The MCP server built every tool from an async (**kwargs) closure and
assigned __annotations__ / __defaults__ afterwards, which cannot add
parameters to a function that has none. Every registered tool was
therefore published to MCP clients with an empty inputSchema: no
parameter names, types, required/optional split, enums, or bounds.

Registration now constructs explicit Tool objects whose `parameters`
field, the schema the server publishes verbatim, is exactly the
registry's spec["input_schema"] from get_tool_specs(). Argument
validation stays where it always lived: execute_tool_call validates
against each tool's real pydantic input model, so the hand-built
FuncMetadata passes arguments through unchanged.

The server now targets the MCP 2.x SDK (mcp.server.mcpserver.MCPServer,
renamed from 1.x's FastMCP); the installed `mcp>=1.0` pin already
resolved to 2.x, under which the old `from mcp.server.fastmcp import
FastMCP` failed at import (and was mypy's one standing error). The
`mcp` extra now pins `mcp>=2.0`, and the module is no longer omitted
from coverage measurement (100% covered by the new tests, which also
compare every published schema against the registry through the real
list_tools() path).

Drive-by from #513: the concurrent-dispatch test asserted on wall-clock
elapsed time and could flake on loaded CI runners; it now proves the
overlap with a barrier both in-flight calls must reach.

Version 1.73.2; CITATION.cff and CHANGELOG.md updated in step.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU
kgdunn pushed a commit that referenced this pull request Aug 29, 2026
The typecheck gate is red on main itself: the unpinned mcp extra now
resolves to mcp 2.x, where FastMCP was renamed MCPServer, so mypy fails
on mcp_server.py on every branch. This ports the migrated server and the
mcp>=2.0 floor verbatim from #528; the diff no-ops once #528 merges.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU
kgdunn pushed a commit that referenced this pull request Aug 29, 2026
The typecheck gate is red on main itself: the unpinned mcp extra now
resolves to mcp 2.x, where FastMCP was renamed MCPServer, so mypy fails
on mcp_server.py on every branch. This ports the migrated server and the
mcp>=2.0 floor verbatim from #528; the diff no-ops once #528 merges.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU
kgdunn pushed a commit that referenced this pull request Aug 29, 2026
The typecheck gate is red on main itself: the unpinned mcp extra now
resolves to mcp 2.x, where FastMCP was renamed MCPServer, so mypy fails
on mcp_server.py on every branch. This ports the migrated server and the
mcp>=2.0 floor verbatim from #528; the diff no-ops once #528 merges.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU
kgdunn pushed a commit that referenced this pull request Aug 29, 2026
The typecheck gate is red on main itself: the unpinned mcp extra now
resolves to mcp 2.x, where FastMCP was renamed MCPServer, so mypy fails
on mcp_server.py on every branch. This ports the migrated server and the
mcp>=2.0 floor verbatim from #528; the diff no-ops once #528 merges.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU
@codecov

codecov Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.42105% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/process_improve/experiments/optimal.py 69.23% 2 Missing and 2 partials ⚠️
src/process_improve/experiments/designs_optimal.py 66.66% 0 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

kgdunn added a commit that referenced this pull request Aug 29, 2026
…riteria search the design class (#496-#499) (#524)

* chore: start OMARS design generation fixes (#496-#499)

* fix(experiments): make OMARS n_runs the total run count and selection criteria exact where enumerable

n_runs now counts the centre runs (issue #496): the design returned for
generate_omars(n_runs=n, center_runs=c) has exactly n rows, and
n_runs - center_runs must be a positive even number.

The a_optimal, d_efficiency, and min_second_order_correlation criteria
(and the D axis of the default dominance rule) previously post-hoc ranked
whatever the binary randomized-objective ILP multistart surfaced, which
cannot even represent designs that repeat a half-run; the exhaustive
optima in issues #497, #498 and #499 need such repetition. The search now
enumerates every feasible half-design multiset (counts per sign class)
up to four factors at moderate sizes, scores the count vectors in
vectorised batches without materialising designs, and picks the winner
exactly; larger problems keep the multistart, reported honestly as
search_mode="multistart". Regression tests pin all tractable cells of
the three issue tables to their exhaustive optima.

A design with a constant second-order column now scores inf on the
selection correlation metric instead of having the column skipped, so
min_second_order_correlation no longer favours degenerate designs.

Fixes #496. Fixes #497. Fixes #498. Fixes #499.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* chore: bump version to 1.72.0 with CITATION and CHANGELOG in sync

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* fix: port the mcp 2.x migration from #528 so typecheck is green

The typecheck gate is red on main itself: the unpinned mcp extra now
resolves to mcp 2.x, where FastMCP was renamed MCPServer, so mypy fails
on mcp_server.py on every branch. This ports the migrated server and the
mcp>=2.0 floor verbatim from #528; the diff no-ops once #528 merges.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

---------

Co-authored-by: Claude <noreply@anthropic.com>
…e-library-issues-hr7xxk-mcp

# Conflicts:
#	CHANGELOG.md
#	CITATION.cff
#	pyproject.toml
kgdunn added a commit that referenced this pull request Aug 29, 2026
…#527)

* chore: start sklearn clone-contract fix (#505)

* fix(multivariate): stop fit() mutating constructor parameters, restoring the clone contract

PCA, PLS, and OPLS wrote the resolved component count (and PLS the
resolved missing-data settings) back onto their constructor parameters
inside fit(). sklearn's contract requires __init__ arguments to stay
verbatim, because get_params/clone/GridSearchCV treat them as the
requested configuration: PLS.cross_validate clones the fitted parent, so
every resample silently inherited the clamped component count and the
mutated missing-data settings, and PCA(n_components=None) became a
concrete int after one fit, freezing the first dataset's clamp.

The resolved count is now the fitted attribute n_components_, set only
in fit(); the parameter is untouched. Internal post-fit readers (the T2
limit mixin, ellipse_coordinates, spe_limit, the plot pre-checks, PLS
prediction intervals) read n_components_, MBPCA/MBPLS expose the same
fitted attribute for those shared helpers, and OPLS maps the old
attribute name to a rename message. Clamping logic and its
SpecificationWarning are unchanged.

Fixes #505.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* chore: bump version to 1.73.0 with CITATION and CHANGELOG in sync

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* fix: port the mcp 2.x migration from #528 so typecheck is green

The typecheck gate is red on main itself: the unpinned mcp extra now
resolves to mcp 2.x, where FastMCP was renamed MCPServer, so mypy fails
on mcp_server.py on every branch. This ports the migrated server and the
mcp>=2.0 floor verbatim from #528; the diff no-ops once #528 merges.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* test: cover the _fitted_n_components fallback branches

Codecov flagged the TPLS-parent delegation, the plain-integer fallback,
and the unfitted-model raise as uncovered on the patch; pin all four
resolution paths directly.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

---------

Co-authored-by: Claude <noreply@anthropic.com>
…e-library-issues-hr7xxk-mcp

# Conflicts:
#	CHANGELOG.md
#	CITATION.cff
#	pyproject.toml
kgdunn added a commit that referenced this pull request Aug 29, 2026
… (#526)

* Make Plotly annotation tests assert the rendered structure; stop theme leak

The four TestPlotlyAnnotationRendering tests asserted only that the figure
dict has a "data" key, which is true of every Plotly figure, so they passed
whether or not the annotation rendered at all. They now pin layout.shapes
(count, x0/x1 vs y0/y1, axis refs, fillcolor) and layout.annotations, and
the skipped-band test compares against a band-free baseline.

test_set_theme_changes_default restored the package DEFAULT_THEME instead
of the value in effect before the test, mutating plotly.io.templates.default
for every later test in the worker; it now restores the captured prior value
via monkeypatch. The background-colour test's docstring claimed each theme
has a distinct background while asserting two identical white values; it now
pins all four values without the distinctness claim.

Closes #512.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* Fix ECharts adapter silent-wrong-output defects; raise on unimplemented members

Annotations now attach to a series of the panel they belong to: the
single-panel and multi-panel paths share one _attach_annotations helper,
and a panel with annotations but no layers gets an empty carrier series
bound to its own axes instead of painting the previous panel (the old
code attached to the cumulative all_series[-1]).

Missing data fields raise KeyError instead of silently plotting 0, in
_paired_data and _wireframe_series, matching the Plotly adapter's direct
row[field] lookups. A ragged z_matrix in _heatmap_series raises ValueError
naming the expected shape instead of filling missing cells with 0.

Per-point style lists (colors, error_y) whose length differs from the data
raise ValueError naming the layer and the style key; the three previously
non-strict zips are now strict=True, matching visualization/tools.py.

MarkType.area and AnnotationType.label are declared in types.py but were
implemented in neither adapter; both adapters now raise NotImplementedError
for them instead of silently falling back to a scatter trace or dropping
the annotation.

Closes #509.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* Bump version to 1.73.1; add changelog entry for #509 and #512

Version assumes the queued PRs carrying 1.72.x/1.73.0 merge first.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* fix: port the mcp 2.x migration from #528 so typecheck is green

The typecheck gate is red on main itself: the unpinned mcp extra now
resolves to mcp 2.x, where FastMCP was renamed MCPServer, so mypy fails
on mcp_server.py on every branch. This ports the migrated server and the
mcp>=2.0 floor verbatim from #528; the diff no-ops once #528 merges.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

---------

Co-authored-by: Claude <noreply@anthropic.com>
claude added 2 commits August 29, 2026 07:29
…e-library-issues-hr7xxk-mcp

# Conflicts:
#	CHANGELOG.md
#	CITATION.cff
#	pyproject.toml
test_budget_clamped_to_minimum_model_size failed intermittently on this
PR's CI (macOS, assert 3 >= 4) in code this PR does not touch. Root
cause and fix are in #531 (a k+1 budget floor on the pyoptex path, a
greedy run-count completion in point_exchange, and a singular-design
scoring guard); ported verbatim here so this PR's CI is deterministic.
No-ops once #531 merges.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

kgdunn commented Aug 29, 2026

Copy link
Copy Markdown
Owner Author

The macOS failure on this head was test_budget_clamped_to_minimum_model_size (assert 3 >= 4), in DOE dispatch code this PR does not touch. It's an intermittent flake in the pyoptex path (the documented k + 1 budget floor was only implemented in the point-exchange fallback, and point_exchange itself could occasionally fall short of the requested run count on a numerically singular starting design). The full root-cause fix is PR #531; it's ported verbatim onto this branch so CI is deterministic here in any merge order, and the ported hunk no-ops once #531 merges.


Generated by Claude Code

@kgdunn
kgdunn merged commit 9eb5645 into main Aug 29, 2026
11 checks passed
@kgdunn
kgdunn deleted the claude/process-improve-library-issues-hr7xxk-mcp branch August 29, 2026 07:44
kgdunn added a commit that referenced this pull request Aug 29, 2026
…least-privilege jobs (#507) (#525)

* fix(release): close the dispatch, SBOM, and Makefile holes in the publish pipeline (#507)

The ENG-21 gate promised that a typo in pyproject.toml on main cannot
ship a release, but three paths around the gate remained. This closes
them:

- publish.yml: the version guard now also runs on workflow_dispatch,
  which becomes a republish-only path: the checked-out commit must
  already carry the v<version> tag for the version in pyproject.toml.
- publish.yml: the build job additionally asserts CHANGELOG.md has a
  matching "## [X.Y.Z]" heading (mirroring the extraction regex), and
  the notes-extraction step fails loudly instead of silently falling
  back to auto-generated notes.
- publish.yml: gh release create passes --target with the SHA of the
  checked-out commit (HEAD, not GITHUB_SHA, since a dispatch run may
  check out inputs.ref), so the release tag lands on the published
  commit rather than the default branch head.
- publish.yml: the SBOM is generated from a fresh venv containing only
  the built wheel and its runtime closure, with pip/setuptools/wheel
  removed; cyclonedx-bom runs from a separate tooling venv so build
  tooling no longer ships as runtime components.
- publish.yml: permissions are per-job. The build job, which executes
  repo code via PEP 517 hooks, holds only contents: read; id-token,
  contents, and attestations write scopes stay on the publish job.
- publish.yml: pypa/gh-action-pypi-publish is pinned to the commit SHA
  that release/v1 resolves to (v1.14.2) instead of the mutable branch.
- Makefile: the release target no longer builds or publishes; it prints
  the tag-gated release instructions and exits nonzero.

Version 1.73.3; CITATION.cff and CHANGELOG.md updated in step.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* fix: port the mcp 2.x migration from #528 so typecheck is green

The typecheck gate is red on main itself: the unpinned mcp extra now
resolves to mcp 2.x, where FastMCP was renamed MCPServer, so mypy fails
on mcp_server.py on every branch. This ports the migrated server and the
mcp>=2.0 floor verbatim from #528; the diff no-ops once #528 merges.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* fix: port the point_exchange/pyoptex-budget deflake from #531

test_budget_clamped_to_minimum_model_size fails intermittently on any
PR's CI (macOS, assert 3 >= 4) in code this PR does not touch. Root
cause and fix are in #531; ported verbatim so this PR's CI is
deterministic. No-ops once #531 merges.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

---------

Co-authored-by: Claude <noreply@anthropic.com>
kgdunn added a commit that referenced this pull request Aug 29, 2026
…at can fail (#508, #510, #511) (#529)

* fix(experiments): bound the remote dataset fetch with an explicit timeout

pd.read_csv(url) carries no timeout, so distillateflow() / oildoe()
against a black-holing host blocked the caller indefinitely (#508).
_read_remote_csv now fetches with urllib.request.urlopen(url,
timeout=...) into a buffer and hands that to pandas. The default of
30 s lives on the config settings singleton as dataset_fetch_timeout,
overridable via PROCESS_IMPROVE_DATASET_FETCH_TIMEOUT or a caller
argument. Timeouts and URL errors still surface as the documented
RuntimeError naming the URL. An on-disk cache stays out of scope.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* test(perf): replace non-asserting benchmarks with deterministic ENG-18 assertions

The tests in tests/perf/ contained no assertions, had no stored
baseline, and pytest-benchmark disables itself under the default
-n auto, so a 100x slowdown passed green while the docstrings claimed
regression tracking (#511). Option 2 from the issue: assert the
cost-shape properties the ENG-18 lazy-frame work actually needs
protected. The public DataFrame views are built exactly once and then
cached (identity plus a counted single build), pickling excludes the
cache and the views rebuild correctly, repeated transform / predict /
diagnose calls cause zero lazy-frame rebuilds, and check_random_state
passes a Generator through by identity while resolving an int seed to
a fresh generator with bit-identical draws. Verified the new tests
fail when the _LazyFrame cache write is removed. CONTRIBUTING.md's
performance-regression policy now describes this honestly; the ENG-15
wall-clock benchmark CI job stays planned.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* test(experiments): skip the live openmv.net tests on the wrapped RuntimeError

The loaders now wrap network failures (including timeouts) in the
module's documented RuntimeError, so the offline skip helper must catch
that too instead of failing when openmv.net is unreachable.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* fix: port the mcp 2.x migration from #528 so typecheck is green

The typecheck gate is red on main itself: the unpinned mcp dependency
resolves to mcp 2.x, where FastMCP was renamed MCPServer, so mypy fails
on src/process_improve/mcp_server.py. This ports the #528 fix verbatim
(mcp_server.py plus the mcp>=2.0 pin in both pyproject.toml
occurrences) so this PR's CI can pass on its own. It becomes a no-op
once #528 merges; this PR's own version bump and CHANGELOG entries for
#508/#510/#511 are in a separate commit.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* test: make the ENG-29 test tiers real with strict markers and measured slow marks

The tiers were documented but unused: -m 'not slow' deselected almost
nothing and -m 'not dataset' still hit the network (#510).

- pytest.ini addopts gains --strict-markers, so a typo'd marker is a
  collection error instead of a silent warning.
- test_pca_foods in tests/test_multivariate.py, the one test fetching
  from openmv.net, now carries @pytest.mark.dataset; a tests/ sweep for
  urlopen / openmv / http found no other unmarked network fetches.
- 42 tests measured at >= 5 s with --durations on this runner now carry
  @pytest.mark.slow (threshold is 2 s per CONTRIBUTING.md; the margin
  keeps the marks honest on faster machines). The two end-to-end tests
  crossing library boundaries (sensory pipeline, sklearn halving grid
  search) also carry @pytest.mark.integration.
- Wall clock: full suite 6:15, -m 'not slow and not dataset' 1:32 on
  the same runner.
- Both CI test jobs run plain pytest with no -m filter, so every tier
  still executes in CI.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* chore: bump version to 1.73.4 with citation and changelog in sync

PATCH bump for the #508/#510/#511 test-hygiene fixes. CITATION.cff
carries the same version and today's date-released; CHANGELOG.md gains
the 1.73.4 section and updated link footer. The version assumes the
earlier queued 1.73.x PRs merge first.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* test: bind both accesses to names in the loadings cache-identity assertion

CodeQL flags a self-comparison; two named reads assert the same
cache-identity property without the identical-values pattern.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* fix: port the optimal-design budget floor from #531 so the flaky clamp test is deterministic

test_budget_clamped_to_minimum_model_size fails intermittently because
the pyoptex path never implemented the documented k + 1 budget floor;
ported verbatim from #531, no-ops once it merges.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* fix: port the point_exchange run-count completion from #531

The dispatcher budget floor alone was not sufficient: the shortfall
happens inside point_exchange, which could end below number_points on
1 to 2% of unseeded runs. Ported verbatim from #531; no-ops once it
merges.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* fix: port the point_exchange singular-design scoring guard from #531

Completes the earlier port: non-finite scores from a numerically
singular X'X now read as unacceptable rather than unbeatable, which was
the underlying freeze behind the run-count shortfall. No-ops once #531
merges.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

---------

Co-authored-by: Claude <noreply@anthropic.com>
kgdunn added a commit that referenced this pull request Aug 29, 2026
…ic multiblock starts, one TPLS T2 (#502, #503, #504) (#530)

* Make NIPALS convergence scale-relative across PCA, PLS, MBPCA and MBPLS

terminate_check compared the raw norm of the change between successive
score vectors against an absolute tolerance, so whether the loop
converged depended on the magnitude of the data: large-magnitude X
burned every md_max_iter iteration, tiny X converged instantly. The
criterion is now the relative form already used by TPLS's
_has_converged: the norm of the change divided by the norm of the
current vector, floored via _nz so a fully-deflated all-zero score
vector cannot divide by zero. The signature and the md_tol settings key
are unchanged; md_tol is now interpreted as a relative tolerance.

The MBPCA and MBPLS inner loops get the same relative criterion, and
their absolute default tolerances at machine precision (eps**(9/10) and
eps**(6/7)), which as relative tolerances sit below the floating-point
oscillation floor, become the shared epsqrt default used by PCA, PLS
and TPLS.

In the same loops, the hard-coded np.random.default_rng(0) start for
the MBPCA super-score and the MBPLS u vector is replaced by a
deterministic seed from the column with the largest sum of squares,
the way single-block PCA / PLS already seed (#195): no RNG remains in
these fit paths, satisfying the reproducibility contract (the
determinism tests for #503 follow in the next commit). Fitted
components are unchanged up to the existing sign convention.

Fixes #504.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* Add determinism tests for the multiblock deterministic NIPALS start

MBPCA and MBPLS previously seeded their NIPALS super-score / u
initialisation from a hard-coded np.random.default_rng(0), violating
the reproducibility contract in docs/development/reproducibility.rst,
and exposed no random_state. The previous commit removed the RNG from
both fit paths in favour of a deterministic seed from the column with
the largest sum of squares, matching single-block PCA / PLS (#195), so
no random_state parameter is needed at all. These tests pin the
contract: repeated fits are bit-identical, and no non-convergence
warning fires on well-conditioned data. Fitted components on the
reference and oracle suites are unchanged up to the existing sign
convention.

Fixes #503.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* Compute one Hotelling's T2 in TPLS, shared by fit() and diagnose()

TPLS.fit() computed a full-Mahalanobis T2 over the super-scores with
the covariance divided by N, shape (n_obs, 1), while diagnose()
computed the cumulative diagonal form, shape (n_obs, n_components):
two different statistics under one name, compared against one limit.
Both now use the cumulative per-component form sum_a (t_a / s_a)^2
already used by PCA and PLS, which is what the class docstring
promised, matches the sibling estimators, and extends to new
observations without the training covariance. The score standard
deviations (scaling_factor_for_scores, which also feeds
ellipse_coordinates) use the unbiased N - 1 divisor, the estimator the
F-distribution form in hotellings_t2_limit is derived for; the
previous N divisor inflated T2 by N / (N - 1) and drew score ellipses
slightly too small.

The pinned test values change accordingly: the training super-scores
are orthogonal (normalized off-diagonal cross-products ~1e-17), so the
old full-Mahalanobis values equal the diagonal form with the N
divisor, and the new pins are exactly the old ones times
(N - 1) / N = 104 / 105. A new test asserts fit() and diagnose()
return the same T2, at the same shape, for the same rows.

Fixes #502.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* Bump version to 1.74.0 and add the changelog entry for #502/#503/#504

MINOR bump: all three fixes change numerical results or iteration
behaviour, because the previous values were wrong. CITATION.cff is kept
in sync in the same commit. The version assumes the queued sibling PRs
land first in order.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* Appease ruff: rename a shadowing loop variable and apply formatting

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* fix: port the mcp 2.x migration from #528 so typecheck is green

The typecheck gate is red on main itself: the unpinned mcp dependency
resolves to mcp 2.x, where FastMCP was renamed MCPServer, so mypy fails
on src/process_improve/mcp_server.py. This ports the #528 fix verbatim
(mcp_server.py plus the two mcp>=2.0 requirement bumps) and no-ops once
#528 merges. The version bump and changelog entries for #528 are
deliberately not taken here.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* Always run the first NIPALS iteration; relax one kernel-vs-NIPALS rtol

Two CI follow-ups on the scale-relative convergence rework (#504).

The PCA / PLS loops prime their convergence check with a seed offset of
+1.0. Under the relative criterion that offset can be negligible for a
large-magnitude seed column (a fuzzed Y entry of ~1.9e154), so the loop
reported convergence before its first iteration: PLS then raised
UnboundLocalError for the never-assigned t_a through the fit_pls tool
boundary, and PCA would have silently returned the unrefined seed as
the score with an all-zero loading. Both loops now force the first
iteration with an explicit itern == 0 check, restoring the pre-rework
behaviour on such inputs, and a deterministic (non-hypothesis)
regression test pins the fuzz payload.

test_adaptive_pls_initial_matches_batch_ldpe compared kernel-PLS and
NIPALS beta coefficients at the assert_allclose default rtol of 1e-7;
the relative stopping rule now ends the batch fit once successive score
iterations agree to epsqrt (~1.49e-8) relative accuracy, and the
agreement on this fixture sits at 1.07e-7 relative on one of 70
elements. Both solvers are converged to their own criteria, so that one
assertion is relaxed to rtol=1e-6 with an explanatory comment; nothing
else is loosened.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* fix: port the point_exchange/pyoptex-budget deflake from #531

test_budget_clamped_to_minimum_model_size fails intermittently on any
PR's CI (macOS, assert 3 >= 4) in code this PR does not touch. Root
cause and fix are in #531; ported verbatim so this PR's CI is
deterministic. No-ops once #531 merges.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

---------

Co-authored-by: Claude <noreply@anthropic.com>
kgdunn added a commit that referenced this pull request Aug 29, 2026
…s CI) (#531)

* fix(experiments): floor the optimal-design budget at k + 1 on the pyoptex path

The point-exchange fallback already raises a budget below k + 1 to k + 1
(the smallest run count that can estimate the main-effects model), and
the tests document that clamp as the contract. The pyoptex path passed
the budget through unclamped, and pyoptex handles an infeasible run
count unpredictably, which made
test_budget_clamped_to_minimum_model_size fail intermittently across
unrelated PRs. Apply the same floor in the d-, i-, and a-optimal
dispatchers, except when the requested budget cannot even hold the
fixed_runs, so that validation still raises on the requested value.

Part of the DOE cluster tracked in #513.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* chore: bump version to 1.74.1, sync CITATION and CHANGELOG, mcp>=2.0 floor

The mcp floor accompanies the mcp 2.x server port from #528 included in
the previous commit; both no-op once #528 merges.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* fix(experiments): point_exchange always returns the requested run count

The exchange loop grows the design only when an addition improves the
criterion, and a candidate consumed by a replacement is never considered
for addition, so an unlucky shuffle ended below number_points on roughly
1 to 2% of unseeded runs; this is the actual mechanism behind the
intermittent 'assert 3 >= 4' in the D-optimal dispatch test (the
dispatcher-level budget floor in the previous commit is necessary for the
contract but not sufficient, because the shortfall happened inside
point_exchange itself). Complete the design greedily with the remaining
candidates that best preserve D-optimality; measured 500/500 correct
sizes after the fix versus 492/500 before.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* test: pin seeds that exercise the point_exchange completion path

Seeds 122, 146, 199, 319, and 338 ended with 3 rows under the pre-fix
algorithm, so they cover the greedy completion loop that codecov flagged
as untested; the low seeds keep the ordinary path covered.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

* fix(experiments): score numerically singular designs as unacceptable in point_exchange

optimization_function guarded only the exactly singular case via
LinAlgError; a numerically singular X'X passes np.linalg.inv and its
garbage inverse can carry an exactly zero determinant, scoring
log|det| = -inf. Under the lower-is-better convention that ranked the
singular design as unbeatable, so no replacement or addition could ever
improve on it: this froze the exchange loop (the underlying mechanism of
the run-count shortfall) and let the new completion step pick a
rank-deficient row (the pinned regression seeds caught it via a
non-finite d_opt). Map any non-finite score to +inf, matching the
existing convention for the exactly singular case. Verified 300/300
unseeded runs and all pinned seeds now return full-rank, correctly sized
designs with finite scores.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019C3XXbJkuSYH9fMryLqNcU

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP server publishes no per-tool JSON Schema: every tool appears to clients as (**kwargs)

2 participants