Skip to content

Vendor packse scenarios and cross-check against a SAT oracle - #61

Merged
jonyoder merged 2 commits into
mainfrom
test/18658-packse-sat-oracle
Sep 24, 2026
Merged

jonyoder merged 2 commits into
mainfrom
test/18658-packse-sat-oracle

Conversation

@jonyoder

Copy link
Copy Markdown
Collaborator

Vendors astral-sh/packse's scenario fixtures and runs them against resolver.Resolve, cross-checked by an independent SAT oracle. Test-only; no production code changed.


What this does

  • Vendors 147 non-universal-example scenario TOML files from astral-sh/packse at commit 18be7766b14aa3e03db37bc575959b3f4c6fbacb (2026-08-18, after tag 0.3.59) into resolver/testdata/packse/, plus its LICENSE-APACHE/LICENSE-MIT. See resolver/testdata/packse/README.md for the exact copy command and how to refresh.
  • resolver/packse_scenario_test.go: parses the scenarios with BurntSushi/toml (test-only import), failing on any undecoded key so a re-pull that adds a field can't silently drop it.
  • resolver/packse_test.go: runs each non-universal scenario against the real resolver.Resolve, classified into three named lists (see below).
  • resolver/satoracle_test.go: builds a CNF encoding of the same scenario data (never from provider/candidate output) and solves it with github.com/crillab/gophersat (test-only import), cross-checking the resolver's own answer in both directions.

Summary line

packse: 147 scenarios = 94 pass + 8 known-fail + 4 unsupported + 41 out-of-scope
SAT oracle: cross-checked 102 scenarios, 7 disagreement(s)

Classification

  • outOfScope (41): resolver_options.universal = true (all 32 of fork/, 5 tag_and_markers/, 2 backtracking/, 2 wheels/). go-pyresolver resolves one concrete marker environment at a time; universal resolution is deferred by RFD 0001.
  • unsupportedOption (4): wheels/no-binary, no-build, no-wheels-no-build, only-wheels-no-binary — resolver.Options has no no_build/no_binary equivalent. The harness refuses to run these rather than silently ignoring the option.
  • knownFail (8): run against the real resolver, asserted to still disagree with packse's expected result. Never t.Skip — if one starts passing, the assertion flips to "unexpectedly passed, remove it from knownFail".
Scenario Reason
extras/missing-extra, extras/extra-does-not-exist-backtrack go-pyresolver models name[extra] as a virtual package requiring a candidate that declares the extra; a candidate that omits it is excluded rather than the extra being silently dropped, which is what uv does
prereleases/package-only-prereleases, -boundary, transitive-package-only-prereleases candidate.PrereleaseSet admits a pre-release only via a specifier or an explicit opt-in; it does not implement pip/uv's further fallback of admitting one when a package publishes no final release at all
requires_python/python-less-than-current go-pyresolver enforces a Requires-Python upper bound per PEP 440; uv deliberately ignores one
yanked/transitive-package-only-yanked-in-range-opt-in, transitive-yanked-and-unyanked-dependency-opt-in FilteredIndex.ExcludeYanked drops a yanked version outright; PEP 592 (and uv) still allow it when a requirement pins it exactly

None of these are fixed in this PR (test-only, per the issue's scope). Each is a real, reproducible gap for a later PR to pick up.

Three-way disagreement table (packse expected / resolver / oracle)

The oracle implements the PEP 592 exact-pin exception directly (the resolver's known limitation above), so it agrees with packse and disagrees with the resolver on the two yanked scenarios. On the other 6, the oracle reuses the same admission input as the resolver (candidate.PrereleaseSet, and the same strict extras model), so it agrees with the resolver and disagrees with packse — i.e. the oracle's disagreement count (7) is a subset of the resolver's knownFail (8), minus 2 yanked scenarios where it goes the other way:

scenario packse resolver oracle
extras/missing-extra sat unsat unsat
prereleases/package-only-prereleases sat unsat unsat
prereleases/package-only-prereleases-boundary sat unsat unsat
prereleases/transitive-package-only-prereleases sat unsat unsat
requires_python/python-less-than-current sat unsat unsat
yanked/transitive-package-only-yanked-in-range-opt-in sat unsat sat
yanked/transitive-yanked-and-unyanked-dependency-opt-in sat unsat sat

Shared pre-release admission input

Per the brief's one deliberate exception: the oracle takes its admissible pre-release version set from candidate.PrereleaseSet (not from provider), plus Requires-Python and wheel/sdist availability it computes itself. Everything else in the CNF is built from the scenario's own TOML data.

Mutation proofs (run locally, not committed)

  1. Flipped candidate.Newest.Less to oldest-first: 13 package-list scenarios went RED.
  2. Removed requires_python/python-less-than-current from knownFail: RED (expected-fail scenario now reported as a real failure). Added local/local-simple to knownFail: RED ("unexpectedly passed, remove it from knownFail").
  3. Dropped the oracle's at-most-one clauses: disagreement count went from 7 to 16 (new false SATs), RED. Corrupted one resolver pin before the direct-model check: 18+ scenarios went RED ("does not satisfy the oracle's CNF").
  4. Copied a scenario into the vendored corpus with an injected unknown key: the loader's strict-decode check fired (undecoded keys [...]), RED.

Consumer impact (PPM, not this PR)

PPM pins go-pyresolver and vendors its dependency tree. Verified by experiment: a test-only import (sat in this case) does not enter a consumer's go.mod or vendor/ tree under go mod tidy/go mod vendor, but does appear in go list -m all under graph pruning. Expect at most a github.com/crillab/gophersat v1.4.0/go.mod h1: line in PPM's go.sum the next time it bumps this module — nothing changes in the vendored code, and module-graph license/SBOM scanners may list gophersat (MIT, harmless).

Not in this PR

  • The weekly re-pull job (RFD §14 "Weekly CI") — needs network access; follow-up.
  • Random-universe SAT cross-checks beyond the packse corpus — the issue scopes the oracle to packse; follow-up.
  • Fixes to anything a scenario exposes (see the knownFail table above) — each gets its own later PR with the scenario as the red test.

NEWS / CHANGELOG

No PPM NEWS.md entry (sibling repo, test-only, not customer-visible). No go-pyresolver CHANGELOG.md entry (test-only, per precedent ee1a650).

Refs rstudio/package-manager#18658

🤖 Generated with Claude Code

jonyoder and others added 2 commits September 24, 2026 10:51
… oracle

Vendors astral-sh/packse's 147 non-example scenario fixtures under
resolver/testdata/packse (pinned commit and refresh steps in its README),
loads them with BurntSushi/toml, and runs the non-universal ones against
resolver.Resolve. A second, independent CNF encoding built from the same
scenario data and solved with crillab/gophersat cross-checks the resolver's
own answer in both directions.

41 universal scenarios are out of scope (RFD 0001 defers universal
resolution). 4 use no_build/no_binary, which resolver.Options has no knob
for. 8 run and are asserted to still disagree with packse's expected result,
each with a one-line reason (PEP 592's yanked exact-pin exception, an extras
edge case, a pre-release fallback pip/uv apply that candidate.PrereleaseSet
does not, and Requires-Python upper bounds, which uv ignores and go-pyresolver
does not). The remaining 94 pass exactly.

Refs rstudio/package-manager#18658

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Match exactPinAdmitsYanked's yanked exact-pin check on the parsed
requirement name instead of a raw substring, closing a false-positive
risk from a prefixed package name. Guard buildOracleModel's final
conjunction against zero clauses (unreachable today, but the same
gophersat empty-AND footgun as atMostOne). Drop an unused binding.

Refs rstudio/package-manager#18658

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@jonyoder
jonyoder merged commit f3a1679 into main Sep 24, 2026
2 checks passed
@jonyoder
jonyoder deleted the test/18658-packse-sat-oracle branch September 24, 2026 20:16
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.

1 participant