Split out of #101, which fixed one instance of this and named the general case as separate work.
What happens
Every optional dependency except mcp is declared with a lower bound and no ceiling:
langchain-openai>=0.2 # openrouter, openai, novita, ollama
fastapi>=0.115 # server
uvicorn>=0.32 # server
real-ladybug>=0.15.3 # ladybug
anthropic>=0.40 # api
slack-bolt>=1.20 # slack
opentelemetry-api>=1.27 # otel
opentelemetry-sdk>=1.27
opentelemetry-exporter-otlp-proto-http>=1.27
Each is one major release away from the failure #101 documented: mcp>=1.2 resolved to 2.0.0 for anyone installing fresh, mcp.server.fastmcp no longer existed there, and grapharc[mcp] was broken on arrival.
Why nothing catches it
uv.lock pins the working versions, so the dev environment and every local pytest resolve to what already works. Only a resolver starting from pyproject.toml — that is, a real user running pip install grapharc[...] — sees the new major. The gap is structural, not an oversight: the lockfile's whole job is to keep development reproducible, and that is exactly what hides this.
CI's clean-environment wheel check is the one job that caught #101, and it caught it only because the broken import made a subpackage unwalkable. An extra whose SDK changed shape without breaking import would sail through.
What to consider
A scheduled job (weekly, plus workflow_dispatch) that resolves and installs from pyproject.toml with --upgrade rather than from the lockfile, then imports every subpackage the way ci.yml's build job already does. It must not gate pull requests — an upstream release is not a reason to redden someone's unrelated PR — so it should open or update an issue on failure instead.
Worth deciding alongside it: whether new extras get a ceiling by default. #101's pin is documented as deliberate (raising it means porting off FastMCP), which is the right shape for a bound — a ceiling with a reason attached, not a reflex.
Acceptance criteria
A resolve that ignores uv.lock and takes the newest release of every extra runs on a schedule, imports every subpackage from the built wheel, and reports a failure somewhere a maintainer will see it without it blocking unrelated pull requests.
Split out of #101, which fixed one instance of this and named the general case as separate work.
What happens
Every optional dependency except
mcpis declared with a lower bound and no ceiling:Each is one major release away from the failure #101 documented:
mcp>=1.2resolved to 2.0.0 for anyone installing fresh,mcp.server.fastmcpno longer existed there, andgrapharc[mcp]was broken on arrival.Why nothing catches it
uv.lockpins the working versions, so the dev environment and every localpytestresolve to what already works. Only a resolver starting frompyproject.toml— that is, a real user runningpip install grapharc[...]— sees the new major. The gap is structural, not an oversight: the lockfile's whole job is to keep development reproducible, and that is exactly what hides this.CI's clean-environment wheel check is the one job that caught #101, and it caught it only because the broken import made a subpackage unwalkable. An extra whose SDK changed shape without breaking
importwould sail through.What to consider
A scheduled job (weekly, plus
workflow_dispatch) that resolves and installs frompyproject.tomlwith--upgraderather than from the lockfile, then imports every subpackage the wayci.yml's build job already does. It must not gate pull requests — an upstream release is not a reason to redden someone's unrelated PR — so it should open or update an issue on failure instead.Worth deciding alongside it: whether new extras get a ceiling by default. #101's pin is documented as deliberate (raising it means porting off FastMCP), which is the right shape for a bound — a ceiling with a reason attached, not a reflex.
Acceptance criteria
A resolve that ignores
uv.lockand takes the newest release of every extra runs on a schedule, imports every subpackage from the built wheel, and reports a failure somewhere a maintainer will see it without it blocking unrelated pull requests.