Repository navigation
bug: uv.lock cannot be regenerated because the root workspace resolution is unsatisfiable when git tags are present #1724
Description
Describe the bug
The root project cannot be resolved from scratch. The committed uv.lock keeps working only because uv reuses it as long as it is self-consistent; the moment anything forces a fresh resolution — adding or upgrading a dependency, or editing a dependency specifier in any of the editable integrations — the resolution is unsatisfiable, and uv lock, the usual remedy, fails for the same reason. The root lock is effectively frozen.
Root cause is a self-dependency cycle created by the version computed from git tags:
- The root project is versioned by
hatch-vcs([tool.hatch.version] source = "vcs"), so in any checkout that has the release tags its version is1.0.1.dev<N>. - Two editable integrations that the root project depends on still cap the dependency below 1.x:
integrations/langchain/pyproject.toml→powercontext[client]>=0.0.2,<1integrations/opendal/pyproject.toml→powercontext[client]>=0.0.3.dev0,<1- (
integrations/langgraph/pyproject.tomlhas no upper bound, so it is unaffected)
- The root
pyproject.tomlpulls all three into the root resolution via[dependency-groups] dev/notebooksand[tool.uv.sources]editable paths.
That yields powercontext:dev → powercontext-langchain → powercontext[client]<1, i.e. the project requires itself at an incompatible version. uv reports it as a name-shadowing problem: "The package powercontext-langchain depends on the package powercontext but the name is shadowed by your project."
Steps to reproduce
On a normal (non-shallow) clone of master (1e90376a), i.e. with tags present. Confirm the version that the tag-derived build produces:
git describe --tags
# powercontext-v1.0.0-103-g... -> the project version is 1.0.1.dev103
Step 1 — the lock-consistent fast path hides the problem
uv lock --check
# Resolved 254 packages in 26ms (exit 0 - the lock is reused; the version is never materialized)
Step 2 — forcing a full resolution fails
uv lock --upgrade --dry-run
# × No solution found when resolving dependencies for split (markers: python_full_version >= '3.12'):
# ╰─▶ ... powercontext:dev depends on itself at an incompatible version (powercontext[client]>=0.0.2,<1)
# ... your project's requirements are unsatisfiable.
# hint: The package `powercontext-langchain` depends on the package `powercontext`
# but the name is shadowed by your project.
Step 3 — the everyday path fails as soon as the lock is stale
Change any specifier - for example integrations/opendal/pyproject.toml <1 to <2 - without regenerating the lock, then:
uv run ruff --version
# × No solution found ... (same self-dependency error)
Restoring <1 makes the same command work again.
Expected behavior
A fresh resolution of the root workspace should succeed, so that uv.lock can be regenerated whenever dependencies change, and developers can add or upgrade dependencies normally.
Actual behavior
A fresh resolution is unsatisfiable whenever the project version is >= 1.0.0 (any checkout with tags), and uv lock cannot repair a stale lock — the two failure modes deadlock each other. Full output:
uv lock --upgrade --dry-run
# × No solution found when resolving dependencies for split (markers: python_full_version >= '3.12'):
# ╰─▶ ... powercontext:dev depends on itself at an incompatible version (powercontext[client]>=0.0.2,<1)
# ... your project's requirements are unsatisfiable.
# hint: The package `powercontext-langchain` depends on the package `powercontext`
# but the name is shadowed by your project.
Why CI mostly hides this. The failure depends on whether the checkout has tags, because that decides the version the root project resolves to:
| Checkout | Computed version | Satisfies <1? |
Fresh resolution |
|---|---|---|---|
With tags (fetch-depth: 0, any normal clone) |
1.0.1.dev103 |
no | fails |
Tag-less shallow clone (default actions/checkout) |
0.1.dev1 |
yes | succeeds |
Measured by building the wheel in both checkouts: powercontext-1.0.1.dev103+ga2f82381e vs powercontext-0.1.dev1+ga2f82381e.
The validate job in .github/workflows/e2e-harness.yml uses fetch-depth: 0, so it is the one CI job that sees version 1.x. It runs the root-level uv run ruff check e2e/bub from make harness-check, which forces a re-resolution and therefore fails for any pull request that touches a dependency specifier. Jobs using the default shallow checkout (including make check and the opendal-package job) compute 0.1.dev1, satisfy <1, and stay green — which is why the trap is easy to miss.
uv lock --locked / uv lock --check do not catch this: on a self-consistent lock they take the lock-reuse path and pass even on the unsatisfiable graph (verified with uv lock --check; in #1717 the quality job's uv lock --locked also stayed green on a stale lock).
Impact
- Any developer working from a full clone (the normal case) cannot add, remove, or upgrade a dependency:
uv lockfails. - Any pull request that changes a dependency specifier, in the root project or in any of the three integrations, turns the
validatejob red unless it also relaxes the offending<1bound in the same commit. - The only way to regenerate the lock is to relax the
<1bounds first, which drags unrelated version-policy changes into whatever PR happens to need a lock refresh. This is exactly what happened while preparing feat(sources): persist immutable evidence declarations #1717: the lock had to be regenerated, which forced the integration bounds to<2in the same commit, and the author reverted their own fix because it looked like an out-of-scope version change.
Suggested fix
- Relax the integration upper bounds so they admit the 1.x line (
integrations/langchain→>=0.0.2,<2,integrations/opendal→>=0.0.3.dev0,<2;langgraphneeds nothing), and regenerateuv.lockin the same commit. The connectors are already validated against the current checkout by their own CI jobs (they build with--with-editable .), so<1looks like a publishing-time leftover rather than a real compatibility boundary. - Or decouple the integrations from the root resolution: drop the three packages from the root
[dependency-groups]and[tool.uv.sources]so each integration owns its own lock (their CI already builds them standalone). This removes the cycle structurally, at the cost of no longer linting/type-checking/testing them through the root environment. - Add a guard regardless of which fix is chosen: run a from-scratch resolution in CI, in a checkout with tags — e.g.
uv lock --upgrade --dry-run, or deleting the lock in a scratch copy and re-locking.uv lock --lockedis not sufficient, since it passes on the broken graph.
Additional context
Found while resolving the src/powercontext/builtin/persistence/memory.py merge conflict for #1717. Not caused by that PR: the offending powercontext<1 bound lives in integrations/langchain/pyproject.toml, which #1717 does not touch, and the failure reproduces on a clean upstream/master worktree.
Environment
- PowerContext:
master@1e90376a(reproduced), and the rebased branch of feat(sources): persist immutable evidence declarations #1717 - uv:
0.12.3locally; also reproduced on CI with uv0.10.12(thevalidatejob of feat(sources): persist immutable evidence declarations #1717) - Python: 3.13.5 and 3.11 (Windows 11); CI jobs run on
ubuntu-latest
Are you willing to submit a PR to fix this bug?
- Yes, I would like to submit a PR.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
fix(uv): stop the integrations from constraining powercontext's version
fix(uv): stop the integrations from constraining powercontext's versi…