Repository navigation
Fix vendored planning ignoring PyPI refusals (#1908) - #1930
Conversation
Assisted-by: Claude Code:claude-opus-5-5
scan --mode vendored chose which new patches may take a --max-new-patches slot through its own copy of the preflight checks, and that copy never ran the PyPI wiring guards. A PyPI patch the wet run refuses before downloading (pypi_no_requirements, pypi_uv_no_sources) still took a rollout slot and pushed an installable patch to the next run, while --dry-run showed the same patch as would_refuse. The planning pass now reads the dry-run preview's would-refuse rows, so the two can no longer disagree. preview_vendor takes the run's GlobalArgs and resolves the patch-server origins and gem takeover refusals itself, and scan computes it once per run, reusing it for the dry-run rendering instead of running every preflight twice. Fixes #1908 Assisted-by: Claude Code:claude-opus-5-5
The dry-run rendering now reuses the planning pass's preview directly instead of keeping an unreachable recompute path, the preview tests call the inner helper by its own name, and get's layering allowlist drops its import of vendor, which it no longer needs. Refs #1908 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
1 similar comment
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 2a26a39. Configure here.
|
Ready for review (burn-down agent).
Generated by Claude Code |
Final review briefWhat it does: Risk: Low to medium. The planning logic is simpler, but wet vendored runs now also run the PyPI guards during planning (that is the fix, and it can probe Look here:
Verified: Read the full diff. Confirmed Changes I made: none. Open questions: none blocking. A known gap remains and is documented: the yarn, Cargo and non-hosted pnpm lock-text engine refusals still aren't predicted by the preview, so a refused new patch in those ecosystems can still take a slot. The follow-up is in the PR description. Auto-merge (squash) is armed, so approving sends it straight to the merge queue. Generated by Claude Code |
Wenxin Jiang (Wenxin-Jiang)
left a comment
There was a problem hiding this comment.
[agent] Reviewed 2a26a39b735e361f1a955b37c6ef4ffd7fecdff8. Checked shared vendor preview refusals through rollout eligibility and dry-run selection retention. No major correctness issues found. Validation: source and regression-test review plus the available exact-head CI checks; no local test pass is claimed.
#1930 made the rollout plan read the dry-run preview, so the lock-text refusal now lives only in the preview and the separate planning pass is gone. Also corrects the contract: only a detached ledger entry already holding this uuid's record is left out. Assisted-by: Claude Code:claude-opus-5-5
LLM Description written by Claude Code:claude-opus-5-5
Fixes #1908
Summary
scan --mode vendoredpicks which new patches may take a--max-new-patchesslot using a "would the wet run refuse this?" planning pass. That pass was a hand-written copy of the dry-run preview's preflight set, and the copy never ran the PyPI wiring guards (pypi_refusals). So a PyPI patch the wet run refuses before downloading (pypi_no_requirements,pypi_uv_no_sources) still took a rollout slot and pushed an installable patch to the next run, while--dry-runshowed the same patch aswould_refuse.Root cause
Two functions answered the same question:
preview_vendor(dry-run rows) andpreflight_refused_purls(planning). They drifted. The planning copy droppedpypi_refusals.Change
preview_vendor(common, selected)is now the single entry point. It resolves the--patch-server-urlorigins and the gem takeover refusals itself, where scan JSON, scan human andgeteach used to build them by hand. Unit tests drive the innerpreview_vendor_with.VendorPreview::refused_purls().preflight_refused_purlsis deleted.VendorPreview::retain_selected, instead of running every preflight a second time. This is safe because each verdict is per patch: Bun's exemptions, vlt, PyPI and gem are all judged purl by purl.getno longer importsvendor, so its edge is removed from the layering allowlist.Not done from the issue's list: replacing the test-only
preview_vendor_jsonhelper with assertions onPreviewVerdict. It's test scaffolding and isn't needed for the fix.Tests
scan::vendor_flow::preview_tests::planning_pass_excludes_a_pypi_refusalmain(test against the oldpreflight_refused_purls):refusedwas{}, so the test FAILED. Green on this branch, together with its control case (the same project withoutno-sourceskeeps its slot).cargo fmt --all -- --check: ok.cargo clippy --workspace --all-features -- -D warnings: ok.cargo test --workspace --all-features --no-fail-fast: everything passes, includingscan_rollout_e2e,scan_vendor_e2e,scan_vendor_requirements_unwired, all 26vendor_flowunit tests andcommand_module_layering. 11 tests fail only in this sandbox, which runs as root and has no access to the live patch API. They are chmod-based write-failure tests (copy_tree,vlt_heal,pypi_poetry/pypi_requirementswire failures,repair_*,in_process_redirectwrite failures) and twoe2e_bun_lockbtests that reach the live blob API. None of them touch the code in this PR, so CI is the judge.Code review (
/code-review high)Fixed: an unreachable recompute fallback in scan's dry-run branch, and a confusing test import alias.
Not fixed, with reasons:
lock_text_refusalsaren't covered, so in those ecosystems a refused new patch can still take a slot. This gap predates the PR and is documented onpreview_vendor("engine refusals outside the preflights are not predicted"). Adding those refusals would add newwould_refuserows, which the issue's acceptance criteria rule out ("preview rows are unchanged"). Follow-up: routepreview_vendor_withthroughlock_text_refusalstoo.hosted_claimed_purlsis read once in pypi and once in pnpm. This predates the PR and is unchanged here.refused_purlsrather thanStage::plan. Both planning call sites now passrefused_purls()tostage.plandirectly, with no other logic in between.https://claude.ai/code/session_011f9aeQujB4vD43SMXRBSVZ
Generated by Claude Code