Skip to content

bug: uv.lock cannot be regenerated because the root workspace resolution is unsatisfiable when git tags are present #1724

Closed

Description

@jiannnnyyyyy

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 is 1.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,<1
    • integrations/opendal/pyproject.toml → powercontext[client]>=0.0.3.dev0,<1
    • (integrations/langgraph/pyproject.toml has no upper bound, so it is unaffected)
  • The root pyproject.toml pulls all three into the root resolution via [dependency-groups] dev / notebooks and [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 lock fails.
  • Any pull request that changes a dependency specifier, in the root project or in any of the three integrations, turns the validate job red unless it also relaxes the offending <1 bound in the same commit.
  • The only way to regenerate the lock is to relax the <1 bounds 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 <2 in the same commit, and the author reverted their own fix because it looked like an out-of-scope version change.

Suggested fix

  1. 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; langgraph needs nothing), and regenerate uv.lock in the same commit. The connectors are already validated against the current checkout by their own CI jobs (they build with --with-editable .), so <1 looks like a publishing-time leftover rather than a real compatibility boundary.
  2. 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.
  3. 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 --locked is 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

Are you willing to submit a PR to fix this bug?

  • Yes, I would like to submit a PR.

Activity

  1. moved this from Todo to In progress in PowerContexton Sep 23, 2026
  2. added a commit that references this issue on Sep 23, 2026
    bac95f6
  3. added a commit that references this issue on Sep 23, 2026
    f9370fb
  4. moved this from In progress to Done in PowerContexton Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions