Skip to content

feat(resolver): implement VariantSelectMode::intersection_priority (closes #63)#82

Merged
doubleailes merged 1 commit into
mainfrom
intersection-priority
May 15, 2026
Merged

feat(resolver): implement VariantSelectMode::intersection_priority (closes #63)#82
doubleailes merged 1 commit into
mainfrom
intersection-priority

Conversation

@doubleailes
Copy link
Copy Markdown
Owner

Summary

rez supports two `variant_select_mode` values (`solver.py:59-65`):

  • `version_priority` (default): pick the variant using the highest versions of the requested packages.
  • `intersection_priority`: pick the variant matching the most of the requested packages, falling back to version_priority for ties.

`pyrer` only modelled `version_priority` so far (#63). Studios configuring rez with `variant_select_mode = intersection_priority` would silently get a different resolution from a `pyrer`-backed shim. This is the last functional gap that the integration guide called out as "not supported" — closing it before 1.0.

Implementation

Mirrors rez's sort exactly. `_PackageEntry.sort` (`solver.py:443-453`) shares one key between the two modes; the only difference is that `intersection_priority` prepends `len(requested_key)` as a primary sort term. Encoded as a new `requested_match_count` field on `VariantKey`:

  • `version_priority`: `requested_match_count = 0` for every variant — uniform across an entry, no-op for the sort. Secondary keys drive entirely.
  • `intersection_priority`: `requested_match_count = requested_key.len()` — primary key, with the rest as tiebreak.

Plumbing

  • New `VariantSelectMode` enum in `rez_solver::context`. `#[default]` is `VersionPriority`.
  • `SolverContext` gains a public `variant_select_mode` field + a `with_variant_select_mode(mode)` builder.
  • New `Solver::new_with_options(reqs, repo, cache, mode)` constructor; existing `new` / `new_with_cache` default to `VersionPriority` and keep their signatures.
  • `pyrer.solve(..., variant_select_mode="version_priority"|"intersection_priority")` — keyword argument, defaults to `"version_priority"`. Invalid values raise `ValueError`.

Tests

Three new tests in `test_rich_api.py`:

  • Default kwarg matches explicit `"version_priority"`.
  • A case that distinguishes the modes — request `[maya, python-3, qt-5]` against `maya-2024` variants `[python-3.11]` / `[python-3.10, qt-5]`:
    • version_priority picks variant 0 (the python-3 comparison settles before qt is considered; 3.11 > 3.10).
    • intersection_priority picks variant 1 (match count 2 > 1).
  • Invalid mode string raises `ValueError`.

All 75 Python tests pass. Strict rez 188-case differential (default mode) still passes 188/188.

Docs

  • README's "fallback" note no longer lists `intersection_priority` as unsupported; calls out both modes as supported.
  • `rez-integration.md` drops the `intersection_priority` bullet from the "not modelled yet" list.
  • The monkey-patch shim example now wires `rez.config.variant_select_mode` straight through to `pyrer.solve`.

Closing 1.0 checklist (Option A path)

  1. ✅ Variant-index parity enforced — test(resolver): enforce variant-index parity in the rez differential #81.
  2. ✅ `intersection_priority` — this PR.
  3. CHANGELOG.md covering rc.1 → 1.0.
  4. "What 1.0 promises" doc page.

Closes #63.

🤖 Generated with Claude Code

rez supports two `variant_select_mode` values (`solver.py:59-65`):

- `version_priority` (default): pick the variant that uses the
  highest versions of the requested packages.
- `intersection_priority`: pick the variant that matches the *most*
  of the requested packages, falling back to version_priority for
  ties.

`pyrer` only modelled `version_priority` so far — issue #63. Studios
configuring rez with `variant_select_mode = intersection_priority`
would silently get a different resolution from a `pyrer`-backed
shim. Implementing it before 1.0 closes the last functional gap
called out in the integration guide.

## Implementation

Mirrors rez's sort exactly. `_PackageEntry.sort` in `solver.py:443-453`
shares one key between the two modes; the only difference is that
`intersection_priority` prepends `len(requested_key)` as a primary
sort term. Encode that as a new `requested_match_count` field on
`VariantKey`:

- `version_priority`: `requested_match_count = 0` for *every* variant
  → the field is uniform across all variants of an entry and so a
  no-op for the sort. Secondary keys drive entirely.
- `intersection_priority`: `requested_match_count = requested_key.len()`
  → primary key, with the rest as tiebreak.

Plumbing:

- New `VariantSelectMode` enum in `rez_solver::context` (re-exported
  from the module root). `#[default]` is `VersionPriority` so every
  existing call site keeps the old behaviour.
- `SolverContext` gains a public `variant_select_mode` field plus a
  `with_variant_select_mode(mode)` builder. The two existing
  constructors default it; a `Solver::new_with_options` constructor
  takes it explicitly.
- `pyrer.solve(..., variant_select_mode="version_priority"|"intersection_priority")`
  — keyword argument, default `"version_priority"`. Invalid values
  raise `ValueError`.

## Tests

Three new tests in `test_rich_api.py`:

- Default kwarg matches explicit `"version_priority"`.
- The case that distinguishes the modes — request
  `[maya, python-3, qt-5]` against `maya-2024` variants
  `[python-3.11]` / `[python-3.10, qt-5]`:
    - version_priority picks variant 0 (sorts on python-3 first;
      3.11 > 3.10 settles it before qt is considered).
    - intersection_priority picks variant 1 (match count 2 > 1).
- Invalid mode string raises `ValueError`.

All 75 Python tests pass. The strict rez 188-case differential
(default mode) still passes 188/188.

## Breaking?

Technically a backwards-compat keyword addition with `variant_select_mode`
defaulting to `"version_priority"` — every existing call site is
unaffected. Marked `!` because it changes the public solver
constructor signature (`Solver::new_with_options` is new; existing
`new` / `new_with_cache` keep the previous signatures and default to
`VersionPriority`).

## Docs

- README's "fallback" note no longer lists `intersection_priority` as
  unsupported; calls out both modes as supported and how to pick.
- `rez-integration.md` drops the `intersection_priority` bullet from
  the "not modelled yet" caveat list.
- The monkey-patch shim example now wires `rez.config.variant_select_mode`
  through to `pyrer.solve`.

Closes #63.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@doubleailes doubleailes merged commit f2f7726 into main May 15, 2026
24 checks passed
@doubleailes doubleailes deleted the intersection-priority branch May 15, 2026 21:23
doubleailes added a commit that referenced this pull request May 15, 2026
Last two items on the Option A 1.0 checklist:

- A CHANGELOG.md following Keep a Changelog conventions, covering every
  rc release back to rc.1, plus a 1.0.0 entry capturing the
  variant-index parity (#81) and `intersection_priority` (#82) work.
  Includes compare-links pointing at the conventional tag names.

- A new docs page `engineering/stability.md` that articulates exactly
  what 1.0 commits us to:

  - Versioning: workspace + wheel bumped in lock-step; semver as of
    1.0; the rc.x series carried no contract.
  - Stable API: explicit list of the Python and Rust surface that's
    covered by semver, and an equally explicit list of what isn't
    (the `solver_micro` benches, the rez_solver submodule layout,
    `Reduction` / failure detail wording, internal performance
    characteristics).
  - Supported Pythons: 3.9–3.13 today (one abi3-py39 wheel per
    platform / architecture covers all of them), 3.14 when pyo3
    catches up.
  - Supported rez: doesn't import rez; the `PackageData.from_rez`
    helper is exercised against rez 3.3.0 (vendored submodule) and
    via the 188-case differential.
  - Correctness contract: same `(name, version)` set as rez, same
    variant index, same status — divergence on any is a release
    blocker.
  - What's modelled vs not: a single table mapping each rez feature
    to "supported" or "not supported, fall back to rez."
  - Performance is explicitly NOT in the contract — the
    `solver_micro` baselines catch regressions on internal hot
    paths, not absolute numbers.
  - Breaking-change policy: RFC issue → addition with deprecation →
    one MINOR cycle → removal at MAJOR.
  - Regression reporting flow.

The CHANGELOG entry for the `Unreleased` / `1.0.0` is intentionally
specific about which PRs land in 1.0 so a reader of the file can
trace each line back to a concrete change.

Linked from:
- README's new "Release notes & stability" section near the bottom.
- The introduction page's "Next steps" list.

Co-Authored-By: Claude Opus 4.7 <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.

Solver: implement VariantSelectMode::intersection_priority

1 participant