From 7147ddf7060aa8c2d9abbbe728365b541b2b5c2c Mon Sep 17 00:00:00 2001 From: Philippe Llerena Date: Tue, 19 May 2026 11:24:16 +0200 Subject: [PATCH 1/2] chore(changelog): release entry for 1.0.0-rc.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidates the existing [Unreleased] section and the stale [1.0.0] — TBD stub into a single [1.0.0-rc.3] — 2026-05-19 release entry. Adds the parser-ecosystem and tooling items that landed via the experimental merge but weren't yet captured under Unreleased (the rer-package crate, parse_static_package_py / parse_static_packages_py PyO3 bindings, the differential safety net, the survey + bench + bisect scripts, and the production integration / engineering RFC docs). Structure follows Keep a Changelog: Added / Changed plus a Performance subsection that names the bench script behind each number and qualifies the corpus context (`/thierry/rez/pkg` on CIFS, against rez 3.3.0). The 0-mismatches differential result is highlighted as the load-bearing correctness claim. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 236 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 161 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2de94c..65c6bef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,93 +12,179 @@ page. ## [Unreleased] +## [1.0.0-rc.3] — 2026-05-19 + +First release candidate of the 1.0 line. Closes the integration +loop with rez: pyrer now ships a Rust `package.py` parser, a +batched parallel I/O path, and a lazy-discovery hook with +constraint-aware filtering, on top of the rez-faithful solver. +The strict 188-case rez differential still passes 188/188 — +public API is from this release forward under the +[Stability commitments](https://doubleailes.github.io/rer/docs/engineering/stability/). + ### Added +#### Rust `package.py` parser + +- **New crate `rer-package`** — hand-rolled lexer that extracts the + four solver-relevant fields (`name`, `version`, `requires`, + `variants`) from a rez `package.py` source string without + invoking Python. Zero non-stdlib dependencies. Accepts the static + subset: literal assignments, ignorable `def commands(...)` / + `def pre_commands(...)`, ignorable `with scope("config")` + declarative DSL. Bails to `None` (caller falls back to rez) on + `@early` / `@late`, top-level `if` / `for` / `class`, `import`, + non-literal assignment to a solver field, or anything the scanner + doesn't explicitly recognise — biased hard toward bailing so + silent correctness regressions can't slip in. See the + [engineering RFC](https://doubleailes.github.io/rer/docs/engineering/fast-package-py-parser/). +- **`pyrer.parse_static_package_py(source) -> Optional[PackageData]`** — + PyO3 binding for the per-file parser. Returns the four-field + `PackageData` on success, `None` on any reason to bail. No + exception ever escapes; even a syntax error becomes `None` (the + caller would invoke rez on it anyway). - **`pyrer.parse_static_packages_py(paths) -> list[PackageData | None]`** — - batched, Rayon-parallel variant of `parse_static_package_py` - that opens and parses every path in one Rust call across a - thread pool. Output is positionally aligned with the input; - missing files, unreadable bytes, and parser-bails all map to - `None`. The GIL is released for the whole batch via - `Python::allow_threads`. Pool size follows - `RAYON_NUM_THREADS` (default: logical core count). Targets - issue #94's profile finding that serial Python `open()` was - the top of the resolve flamegraph (3.20 s of 9.12 s, 35% of - wall time) after the per-file static parser landed. - Closes #94. -- **`version_range` hint on the `load_family` callback** (issue #92) — - `pyrer.solve(..., load_family=cb)` now invokes `cb` as - `cb(name, version_range="2+<3")` when the callback's signature can - accept a second `version_range` argument (named param or `**kwargs`). - The hint is a rez-syntax range string the shim can pass directly to + batched, Rayon-parallel variant. Opens and parses every path in + one Rust call across a thread pool with the GIL released + (`Python::allow_threads`). Output is positionally aligned with + the input; missing files, unreadable bytes, and parser-bails all + map to `None` at the matching index. Pool size follows + `RAYON_NUM_THREADS` (default: logical core count). Targets the + serial-Python-`open()`-loop bottleneck after the per-file parser + landed (#94 cProfile showed 3.20 s of 9.12 s — 35% of resolve + wall time — in serial `open()`). Closes #94. + +#### `pyrer.solve()` integration surface + +- **`load_family` callback** for lazy package discovery — pass + `load_family: Callable[[str], list[PackageData]]` and pyrer + invokes it on demand the first time the solver needs a family + it hasn't seen. Each family is loaded at most once per solve. + Returning `[]` means "no such family". Aimed at cold-cache and + network-filesystem integrations (Windows + CIFS in particular) + where the up-front BFS of every reachable family dominates + `rez env` wall time. Closes #86. +- **`version_range` hint on `load_family`** — the callback signature + can now accept a second `version_range` argument (named parameter + or `**kwargs`); pyrer detects via `inspect.signature` once per + `solve()`. The hint is a rez-syntax range string (e.g. `"2+<3"`, + `None` for unconstrained) the shim can pass directly to `rez.packages.iter_packages(range_=...)` to skip on-disk version - directories outside the request. Backward-compatible: 1-arg - callbacks (`def cb(name):`) keep working unchanged — pyrer detects - the signature via `inspect.signature` once per `solve()` call. - Targets the 95% load-fan-out waste documented in #92 (2,637 - packages loaded for 132 used on a typical Fortiche resolve); - projected 6-20× cut to `_load_family` wall time. The 188-case rez - differential still passes 188/188. + directories outside the request. Targets the 95% load-fan-out + waste documented in #92 (2,637 packages loaded for 132 used on a + typical Fortiche resolve). The repo tracks the loaded range per + family and reloads with a widened range if the solver backtracks + and needs more — see `PackageRepo` notes in **Changed** below. + Backward-compatible: 1-arg callbacks keep working. Closes #92. - **`PackageData.from_strings(name, version, requires=None, variants=None)`** — classmethod constructor for raw-string callers, symmetric with `from_rez(pkg)`. Skips rez's `AttributeForwardMeta` chain, the - `Requirement` parse, and the `str(Requirement)` round-trip — the - latter being a measurable fraction of integration overhead on - rez-shim hot paths (per-package, every package, every resolve). - Functionally equivalent to the four-arg constructor; the - classmethod form exists so callers wiring `pkg.resource.data` - through pyrer have a named, documented contract to reach for. - Falls back to `from_rez` for `@early` / `@late`-bound attributes. - Closes #88. -- **`load_family` callback** on `pyrer.solve()` — opt-in lazy package - discovery: pass `load_family: Callable[[str], list[PackageData]]` and the - solver calls it on demand the first time it needs a family it hasn't seen. - Each family is loaded at most once per solve; returning `[]` means "no - such family". Aimed at cold-cache / network-filesystem integrations - (Windows + CIFS in particular) where the up-front BFS of every reachable - family dominates the wall-clock cost of `rez env`. See the - [lazy-discovery section of the rez integration page](https://doubleailes.github.io/rer/docs/getting-started/rez-integration/#lazy-package-discovery-on-cold-caches). - Closes #86. -- **`resolved_ephemerals`** on `pyrer.SolveResult` — list of rez-style - ephemeral requirement strings (e.g. `[".feature-1.5", ".mode-debug"]`) - surfaced from the solver, matching `rez.solver.Solver.resolved_ephemerals`. - Closes #84. -- **Borrowing-iterator forms** on the Rust API: `Solver::resolved_packages_iter` - / `resolved_ephemerals_iter` and `ResolvePhase::iter_solved_variants` / - `iter_solved_ephemerals`. Avoid the intermediate `Vec` (and, for - ephemerals, the per-element `Requirement::clone`) when callers just want - to iterate. - -### Changed - -- **`PackageRepo` is now a struct**, not a `HashMap` type alias. Carries a - cache (`RefCell>`) and an optional `FamilyLoader` closure. - Construct with `PackageRepo::from_map(map)` for the eager case, or - `PackageRepo::with_loader(loader)` for lazy. `From>` is - implemented for back-compat with the old type-alias shape. The eager - path's perf is unchanged in measurement (within run-to-run noise of the - README baseline). - -## [1.0.0] — TBD - -The first stable release. Public API is now under semver — see the -[Stability commitments](https://doubleailes.github.io/rer/docs/engineering/stability/) -page for what's covered. + `Requirement` parse, and the `str(Requirement)` round-trip on + the rez-shim hot path. Functionally equivalent to the four-arg + constructor; the classmethod form exists so callers wiring + `pkg.resource.data` through pyrer have a named, documented + contract to reach for. Falls back to `from_rez` for `@early` / + `@late`-bound attributes. Closes #88. +- **`resolved_ephemerals`** on `pyrer.SolveResult` — list of + rez-style ephemeral requirement strings (`[".feature-1.5", + ".mode-debug"]`) surfaced from the solver, matching + `rez.solver.Solver.resolved_ephemerals`. Closes #84. +- **`variant_select_mode`** parameter on `pyrer.solve()` — + `"version_priority"` (rez's default) and `"intersection_priority"`. + Mirrors `config.variant_select_mode`. New `VariantSelectMode` enum + on the Rust side, plus `SolverContext::with_variant_select_mode` + / `Solver::new_with_options` constructors. Closes #63. -### Added +#### Rust API additions -- **`variant_select_mode`** parameter on `pyrer.solve()` — - `"version_priority"` (rez's default) and `"intersection_priority"`. Mirrors - rez's `config.variant_select_mode`. On the Rust side: new - `VariantSelectMode` enum, `SolverContext::with_variant_select_mode(mode)` - builder, `Solver::new_with_options(reqs, repo, cache, mode)` constructor. - Closes #63. +- **Borrowing-iterator forms** on `Solver` and `ResolvePhase`: + `resolved_packages_iter`, `resolved_ephemerals_iter`, + `iter_solved_variants`, `iter_solved_ephemerals`. Avoid the + intermediate `Vec` (and, for ephemerals, the per-element + `Requirement::clone`) when callers just want to iterate. ### Changed -- **Differential test now enforces variant-index parity.** The 188-case rez - benchmark gate previously checked `(name, version)` only; it now also - compares the variant index rez picked for each entry. 188/188 still pass. +- **`PackageRepo` is now a struct**, not a `HashMap` type alias. + Carries a cache (`RefCell>`), optional `FamilyLoader` + closure, and a per-family `loaded_range` so `get_family(name, + hint)` can reload with a widened range when the solver + backtracks. Construct with `PackageRepo::from_map(map)` for the + eager case or `PackageRepo::with_loader(loader)` for lazy; + `From>` is implemented for back-compat. Eager-path + perf unchanged in measurement (within run-to-run noise of the + README baseline). +- **`FamilyLoader` type signature** is now + `Fn(&str, Option<&VersionRange>) -> Vec<(String, PackageData)>` + (was 1-arg) — required to thread the `version_range` hint through. +- **Differential test now enforces variant-index parity.** The + 188-case rez benchmark gate previously checked `(name, version)` + only; it now also compares the variant index rez picked for each + entry. 188/188 still pass. + +### Performance + +Measured on the Fortiche corpus (`/thierry/rez/pkg`, ~6,400 `package.py` +files, served over CIFS) against rez 3.3.0: + +- **Static parser, end-to-end**: 75 μs/file (`open + read + parse`) + vs rez's `DeveloperPackage.from_path + from_rez` at 2,615 μs/file. + **34.8× speedup.** The parse step alone dropped from 1,990 μs + (V1 rustpython-parser) to **59 μs (V2 hand-rolled lexer)** — + the hand-rolled rewrite was a 33× win on its own layer. +- **Corpus accept rate**: 92.9% of files (5,979 / 6,439) are + statically parseable. Per the issue #84 differential harness, + the parser produces **0 mismatches** against rez on the + V2-accepted set. +- **Batched parser**: 2.81× speedup on 2,000-file batches over the + serial Python `open()` loop the shim ran before (4,234 ms → 1,508 + ms). Per-file saving ~1.36 ms; extrapolated to a 2,600-file + resolve, ~3.5 s saved per `_load_family`. +- **Solver**: unchanged. Strict 188-case rez differential remains + 188/188. + +### Tooling + +- **`scripts/survey_package_py.py`** — Stage 1 corpus classifier that + walks a rez repo and reports per-file: fast-parseable / dynamic- + requires / imports / top-level-classdef / etc. Pure stdlib; the + go/no-go signal for whether the parser is worth wiring into a + given studio. +- **`scripts/diff_against_rez.py`** — Stage 2 safety net. For every + file `parse_static_package_py` accepts, also load via rez's + `DeveloperPackage.from_path` and assert the four solver fields + match byte-for-byte. Run on the full Fortiche corpus: **0 + mismatches on 5,979 V2-accepted files** in 74 seconds. Treats + any divergence as a release blocker. +- **`scripts/bench_package_py_parser.py`** — full-load comparison + (`open + read + parse_static_package_py` vs `DeveloperPackage.from_path + + from_rez`), the source of the 34.8× number. +- **`scripts/bench_batched_parser.py`** — serial-loop vs batched + comparison, the source of the 2.81× number. +- **`scripts/bench_python_construction.py`** — `PackageData` + construction microbench (`from_strings` vs `from_rez` paths). +- **`scripts/compare_resolves.py`** — pyrer-vs-rez bisect tool for + divergence reports. Uses the recommended shim shape internally + (static parser + `load_family` + `version_range` + `from_rez` + fallback) and reports per-request agreement / divergence / + failure. Used to triage #96. + +### Docs + +- **[Wiring `pyrer` into `rez`](https://doubleailes.github.io/rer/docs/getting-started/rez-integration/)**: + full production integration guide covering the static parser + (per-file + batched), `load_family`, `version_range` hint, + `from_strings`, shadow-validation mode, metrics counters, + rollout plan, and a "Where this WON'T help" honest-caveat list + for each feature. +- **[Static `package.py` parser RFC](https://doubleailes.github.io/rer/docs/engineering/fast-package-py-parser/)**: + Stages 1-4 design + measured results, the V1 → V2 spike story + (1.7× → 34.8×), the differential safety-net result, and a + "Considered alternatives" section flagging the parsed-package + cache as the next architectural lever. +- **FAQ entry** for "Where does rer get package data from?" + updated to mention both eager and lazy (`load_family`) supply + paths. ## [0.1.0-rc.6] — 2026-05-15 From 657dbbd9284dffec425332e64ec84b4a5e25dd22 Mon Sep 17 00:00:00 2001 From: Philippe Llerena Date: Tue, 19 May 2026 12:01:01 +0200 Subject: [PATCH 2/2] docs(engineering): road-to-1.0.0 release checklist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Companion to the existing Stability commitments page. The stability page describes what 1.0 commits us to; this new page describes the work needed *before* cutting 1.0 so the commitment is defensible. Structured as a four-week sequence of small PRs: 1. API surface freeze — drop `filters` / `max_iterations` reserved-but-ignored kwargs, deprecate `SolveResult.resolved`, audit Rust `pub` items, add `#[non_exhaustive]` to public enums, decide the `FamilyLoader` / `VersionRange` exposure question. 2. Correctness net — extract a ~200-package fixture corpus, wire `compare_resolves.py` into CI, scaffold property/fuzz tests on the solver, triage open issues (including #96). 3. Documentation — stability page lists exact public symbols (not qualitative claims), README drops experimental framing, changelog retitled. 4. Release — bump pins, verify wheel + crates publish flows, tag, announce. Explicit "what's not in 1.0" section to resist scope creep: the Rex evaluator, persistent caches, parallel solve_many, and daemon model are all 1.1+ work. The 1.0 bar isn't more features — it's confidence we can support what's already shipped. Closes the planning loop around the release. Co-Authored-By: Claude Opus 4.7 --- .../content/docs/engineering/road-to-1.0.0.md | 290 ++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 docs/content/docs/engineering/road-to-1.0.0.md diff --git a/docs/content/docs/engineering/road-to-1.0.0.md b/docs/content/docs/engineering/road-to-1.0.0.md new file mode 100644 index 0000000..ae4410d --- /dev/null +++ b/docs/content/docs/engineering/road-to-1.0.0.md @@ -0,0 +1,290 @@ ++++ +title = "Road to 1.0.0 — release checklist" +description = "Concrete checklist for cutting a stable 1.0.0 from the 1.0.0-rc.X line. API surface review, correctness hardening, docs polish, release pipeline." +draft = false +weight = 25 +sort_by = "weight" +template = "docs/page.html" + +[extra] +lead = "Concrete checklist for cutting a stable 1.0.0 from the 1.0.0-rc.X line. The point of 1.0 isn't more features — it's freezing what we have so callers can rely on it." +toc = true +top = false ++++ + +> **Companion to the [Stability commitments](../stability/) page.** +> That page describes what 1.0 commits us to. This page describes +> the work we need to do *before* cutting 1.0 so the commitment is +> defensible. Read both together. + +The 1.0.0-rc.X line ships everything we want feature-wise. The +remaining work is *deciding what's `pub`*, *proving it's correct*, +and *announcing what we promise*. None of it adds new functionality. +All of it is small PRs. + +## Why a 1.0 cut isn't "ship rc.3 with a different version number" + +Once 1.0.0 lands: + +- Every public type / function / behaviour is under semver. + Breaking it requires a major-version bump. +- The integration shim authors (Fortiche, anyone else) take the + shape we ship as load-bearing forever. Any wart becomes + permanent. +- "It's documented as reserved-and-ignored" stops being a get-out- + of-jail-free card; reserved-and-ignored kwargs become API noise + we can't drop. + +So the bar isn't more features — it's confidence we can support +what's already shipped without painting ourselves into a corner. + +## Must-have work + +These items affect the public surface area we commit to. Doing +them before 1.0 is much cheaper than doing them after (where they +become 2.0 breaking changes with deprecation cycles). + +### 1. `pyrer.solve()` signature cleanup + +Today: + +```python +pyrer.solve(package_requests, packages=None, /, *, load_family=None, + variant_select_mode="version_priority", + filters=None, max_iterations=None) +``` + +Decisions: + +- **`filters` parameter.** Reserved-but-ignored. **Drop** unless + we plan to ship the implementation in 1.0. Reserved kwargs that + outlive a major version become permanent noise; reintroducing + later is cheap, removing later is not. +- **`max_iterations` parameter.** Same. **Drop.** +- **`load_family` 1-arg vs 2-arg signature.** Today pyrer auto- + detects via `inspect.signature` and dispatches accordingly. + **Decide**: commit to keeping the 1-arg form forever (no + change), or add a `DeprecationWarning` when a 1-arg callback is + detected and drop in 2.0 (slightly cleaner long-term). +- **`SolveResult.resolved` tuple form.** Documented as "kept for + compatibility with the original 0.1.0-rc.5 surface". **Decide**: + mark deprecated for 2.0 removal (recommended — `resolved_packages` + covers every use), or commit to permanent. + +### 2. Rust API audit per crate + +Walk each crate and mark anything internal as `pub(crate)`. The +exposed surface for `rer-resolver` today is large; some items are +implementation detail callers shouldn't depend on. + +- **`rer-version`** — `RerVersion`, `VersionRange`, + `Requirement`, `Requirements`. Audit each. +- **`rer-resolver`** — every `pub use` in `rez_solver/mod.rs`. + Items like `PackageVariantCache`, `SolverContext`, + `PackageScope`, `ResolvePhase` are arguably internal-only; + decide which the outside world needs. +- **`rer-package`** — `PackageInfo`, `parse_static_package_py`, + `parse_static_packages_py`. All seem genuinely public. + +For every `pub` item that stays, decide whether it's a stable +contract or experimental. + +### 3. Public enums need `#[non_exhaustive]` + +If we add a variant to `FailureReason` / `ScopeError` / +`SolverStatus` / `VariantSelectMode` in 1.1, that's a non-breaking +change *only if* the enum is `#[non_exhaustive]` so callers +already use `_ =>` arms. Add the attribute to every public enum +before 1.0. + +### 4. `FamilyLoader` type — hide or commit? + +The closure type leaks `&VersionRange` from `rer-version`, which +means external implementors of `FamilyLoader` depend on that +crate. Two options: + +- **Commit** to the current shape and add `rer-version` as a + required dependency for `FamilyLoader` users. +- **Hide** by changing the loader signature to accept a `&str` + (rez range syntax) — pyrer already converts to/from strings at + the FFI boundary. + +The second is more contained. Recommend it. + +## Should-have work + +Items that strengthen the release without changing the API. + +### 5. Real-corpus differential in CI + +The 188-case `test_rez_benchmark` is solid but issue #96 (even +when downstream-attributable) demonstrated it doesn't exercise +studio shapes: + +- Packages with 4+ variants each disagreeing on python version. +- Bare `requires` on long lists of internal families. +- Transitive variant requirements forcing deep backtracking. + +Plan: + +1. Extract ~200 packages from a representative real corpus + (anonymized — strip `commands()` content, replace internal + names with generic stand-ins) into a committed fixture in + `data_set/real_corpus/`. +2. Wire `scripts/compare_resolves.py` into CI as a + `cargo test --ignored`-style gate against that fixture. +3. Any divergence is a release blocker, mirroring the existing + 188-case bench's role. + +Effort: ~1 week. Catches future regressions on the long-tail +patterns the 188-case bench misses. + +### 6. Property / fuzz test on the solver + +Random variant trees and requests, run both pyrer (always) and +rez (when available), assert agreement. Codifies the +backtracking-completeness claim against synthetic inputs. + +Effort: ~1–2 weeks. Catches algorithmic regressions that +hand-crafted tests don't cover. + +### 7. Stability commitments page audit + +The page currently lists qualitative commitments. For 1.0, name +the exact items: + +- Python API: every public function on `pyrer`, every public + field on `PackageData` / `ResolvedVariant` / `SolveResult`. +- Rust API: every `pub use` in each crate's top-level `lib.rs`, + with a "stable" / "experimental" tag per item. +- Behavioural commitments: rez differential success criteria, + semver policy for solver result shape, error variants. + +Effort: ~3 days. Comes naturally out of the surface audit (item 2). + +### 8. README polish + +- Drop the "experimental" / "RC" framing where it still appears. +- Update headline numbers (34× solver / 34.8× parser / 2.81× + batched) with their corpus context. +- Add an honest "what rer is not" section. + +Effort: ~1 day. + +### 9. Issue triage + +Run `gh issue list --state open --repo doubleailes/rer`. Per +issue: + +- **#96** — confirm with the user that the bisect tool resolves + their concern (shim-attributable), or get a repro. +- Any "would be nice for 1.0" issue — explicit decision: in or + out. Default to "out" unless it changes public API. +- Anything tagged as a 1.0 blocker — resolve or document the + punt. + +## Release pipeline (must work before tagging 1.0.0) + +### 10. Wheel publishing for `pyrer` + +- Maturin builds for Linux / macOS / Windows on cp39+ ABI3. +- CI job triggered on `v*` tags publishes to PyPI. +- Verify on a real machine: `pip install pyrer==1.0.0` produces + a working install with the expected behaviour. + +Probably already exists from the rc cycle — verify the 1.0 tag +triggers it cleanly. + +### 11. crates.io publishing for the Rust crates + +Order matters because of inter-crate deps: + +1. `rer-version` (no internal deps). +2. `rer-resolver` (depends on `rer-version`). +3. `rer-package` (independent except for workspace metadata). + +`rer-python` stays `publish = false` — it ships via PyPI as +`pyrer`. + +For each: `cargo publish --dry-run` first, then real publish on +tag. + +### 12. Doc deployment + +Zola site rebuilds on tag → GitHub Pages. Verify the deployment +workflow exists and runs cleanly. Update the homepage version +pill (`docs/config.toml`, `docs/content/_index.md`) — same four +touchpoints as every prior rc bump. + +### 13. GitHub release + +- Tag `v1.0.0`. +- Release page with the `[1.0.0]` changelog excerpt as the body. +- Attach the platform wheels (or link to PyPI). +- Announce: discussions thread / Discord / wherever the + community lives. + +## Suggested sequence + +Four discrete PRs, each ~3-4 days of focused work. Each can be +merged independently; ordering matters only for the API audit +(item 2) which constrains the others. + +### Week 1 — API surface freeze + +PR: drop `filters` / `max_iterations`; deprecate +`SolveResult.resolved`; audit Rust `pub` items; add +`#[non_exhaustive]` to public enums; decide the +`FamilyLoader`-string question; update integration docs. + +### Week 2 — Correctness net + +PR: extract fixture corpus into `data_set/real_corpus/`; wire +`compare_resolves.py` into CI; scaffold the property test +generator; triage open issues. + +### Week 3 — Documentation + +PR: stability commitments page lists the exact public surface; +README updated; changelog `[1.0.0-rc.3]` retitled to +`[1.0.0] — `; migration guide (if any deprecations from +Week 1 need one). + +### Week 4 — Release + +PR: bump workspace + docs to `1.0.0`; verify pipelines; tag; +publish wheels; publish crates; announce. + +## What's *not* in 1.0 + +Resist scope creep. These belong in 1.1+, not in the 1.0 cut: + +- **Rex evaluator** — 2-3 month project, paired safety net required. + Worth shipping; not worth blocking 1.0 for. +- **Persistent caches** (parsed-package, solve-result) — orthogonal + to API stability. Can ship as feature work in 1.1. +- **Parallel `solve_many`** — needs an `Rc → Arc` refactor with + measurable single-resolve perf risk. Not part of the 1.0 API. +- **Daemon model** — wrong scope for "rer is the engine". +- **rez-equivalent features** (CLI, build system, plugin host) — + out of scope per the engine-only project mission. + +The 1.0 cut should be "everything we have now, with the API +frozen, the docs sharpened, and the release pipeline proven". +The bar isn't more features; it's confidence we can support what's +already shipped. + +## When to actually cut + +Sign-off conditions: + +1. Public API audit complete and committed. +2. `cargo test` + `pytest tests/` + 188-case differential + + real-corpus differential all pass on `main`. +3. Stability commitments page lists every public symbol. +4. README reflects shipped behaviour, no "experimental" framing. +5. At least one downstream consumer (Fortiche shim) has run + against the post-Week-1 API and confirmed it still works. +6. Open issues that affect 1.0 are resolved or explicitly punted. + +If any of those isn't true, ship another rc instead.