@@ -19,9 +19,8 @@ use std::collections::{HashMap, HashSet};
1919use std:: path:: Path ;
2020
2121use socket_patch_core:: api:: types:: PatchSearchResult ;
22- use socket_patch_core:: utils :: purl :: strip_purl_qualifiers ;
22+ use socket_patch_core:: vendor :: load_state ;
2323use socket_patch_core:: vendor:: state:: VendorEntry ;
24- use socket_patch_core:: vendor:: { load_state, lookup_entry} ;
2524
2625/// The vendor ledger as the preflight consumes it: the caller's own
2726/// `load_state` outcome, so an UNREADABLE ledger is a fact the refusal can
@@ -32,18 +31,14 @@ pub(crate) type LedgerLoad<'a> = Result<&'a HashMap<String, VendorEntry>, &'a st
3231///
3332/// `exempt` holds the selected purls the refusal must NOT pre-empt — they
3433/// flow through to the engine, which lets them in exactly as on a non-Bun
35- /// project. A purl is exempt when EITHER
34+ /// project. A purl is exempt only when `bun.lock` wires every instance of
35+ /// its `name@version` to one of our `.socket/vendor/npm/` tuples, at any
36+ /// UUID ([`wired_instances_all_ours`]). This matches the engine's workspace
37+ /// gate: updating an already-local tuple introduces no new relative path,
38+ /// so in-sync runs, superseding patches and repairs remain supported.
3639///
37- /// * the vendor ledger already wires it at the SAME uuid this run selected
38- /// (an in-sync re-run: the engine's `already_vendored` skip), OR
39- /// * `bun.lock` already wires EVERY instance of its `name@version` to one
40- /// of our `.socket/vendor/npm/` tuples, at any uuid
41- /// ([`wired_instances_all_ours`]) — the engine's own criterion for
42- /// skipping the workspace gate: rewriting an already-local tuple to a
43- /// superseding uuid adds no new workspace-relative path, so a patch
44- /// UPDATE on a project vendored before it grew a workspace member (or
45- /// the same re-run after a wiped `state.json`) re-vendors in place
46- /// instead of dying here with a remedy Bun 1.2/1.3 teams cannot follow.
40+ /// A matching ledger UUID alone is insufficient: `rollback --preserve-state`
41+ /// retains the entry after removing its wiring.
4742///
4843/// An unreadable ledger exempts nothing (fail closed) and the refusal
4944/// itself becomes `vendor_state_unreadable` with the io/parse detail:
@@ -78,7 +73,7 @@ impl BunVendorRefusal {
7873
7974/// Run the Bun preflight once for `selected` — only when it holds at least
8075/// one npm purl, since nothing else can be affected — loading the vendor
81- /// ledger at `cwd` for the exemption . `None` means nothing to refuse.
76+ /// ledger at `cwd` to detect corruption . `None` means nothing to refuse.
8277pub ( crate ) async fn bun_vendor_preflight (
8378 cwd : & Path ,
8479 selected : & [ PatchSearchResult ] ,
@@ -142,7 +137,7 @@ fn selection_pairs(selected: &[PatchSearchResult]) -> Vec<(&str, &str)> {
142137}
143138
144139/// Turn the engine's project-level refusal into the per-purl verdict: the
145- /// ledger-or -lock exemption described on [`BunVendorRefusal`], or the
140+ /// live -lock exemption described on [`BunVendorRefusal`], or the
146141/// `vendor_state_unreadable` refusal when the ledger cannot be read.
147142async fn refusal_with_exemptions (
148143 cwd : & Path ,
@@ -151,37 +146,30 @@ async fn refusal_with_exemptions(
151146 pairs : & [ ( & str , & str ) ] ,
152147 ledger : LedgerLoad < ' _ > ,
153148) -> BunVendorRefusal {
154- let entries = match ledger {
155- Ok ( entries) => entries,
156- Err ( e) => {
157- return BunVendorRefusal {
158- code : "vendor_state_unreadable" ,
159- detail : e. to_string ( ) ,
160- exempt : HashSet :: new ( ) ,
161- } ;
162- }
163- } ;
149+ if let Err ( e) = ledger {
150+ return BunVendorRefusal {
151+ code : "vendor_state_unreadable" ,
152+ detail : e. to_string ( ) ,
153+ exempt : HashSet :: new ( ) ,
154+ } ;
155+ }
164156 // The lock-derived exemption exists only for the workspace gate: every
165157 // other preflight code means bun.lock could not be read or parsed, so
166158 // nothing in it can be ours and re-reading it per purl would be wasted
167159 // (guarded, but still) I/O.
168160 let lock_parsed = code == "vendor_bun_workspace_unsupported" ;
169161 let mut exempt = HashSet :: new ( ) ;
170- for ( purl, uuid ) in pairs {
162+ for ( purl, _ ) in pairs {
171163 if !purl. starts_with ( "pkg:npm/" ) {
172164 continue ;
173165 }
174- // The ledger is keyed by the manifest purl (possibly qualified) and
175- // `lookup_entry` also resolves base purls; try the selected spelling
176- // first, then its qualifier-free base.
177- let ledger_in_sync = lookup_entry ( entries, purl)
178- . or_else ( || lookup_entry ( entries, strip_purl_qualifiers ( purl) ) )
179- . is_some_and ( |e| e. uuid == * uuid) ;
166+ // A preserved ledger can outlive its wiring (rollback --preserve-state).
167+ // Only live lock tuples prove the engine can skip the workspace gate.
180168 let lock_all_ours = lock_parsed
181169 && socket_patch_core:: vendor:: bun_lock:: wired_instances_all_ours ( cwd, purl)
182170 . await
183171 . unwrap_or ( false ) ;
184- if ledger_in_sync || lock_all_ours {
172+ if lock_all_ours {
185173 exempt. insert ( ( * purl) . to_string ( ) ) ;
186174 }
187175 }
@@ -275,8 +263,8 @@ mod tests {
275263 }
276264
277265 /// `bun_vendor_preflight` never reads the lock when nothing selected is
278- /// npm (no needless I/O, no spurious refusal for other ecosystems); an
279- /// in-sync ledger entry exempts; an unreadable ledger exempts nothing
266+ /// npm (no needless I/O, no spurious refusal for other ecosystems);
267+ /// a ledger alone never exempts; an unreadable ledger exempts nothing
280268 /// (fail closed) AND is reported as the real problem
281269 /// (`vendor_state_unreadable`), never as a Bun lock remedy.
282270 #[ tokio:: test]
@@ -300,11 +288,11 @@ mod tests {
300288 assert ! ( refusal. applies_to( PURL ) ) ;
301289 assert ! ( !refusal. applies_to( "pkg:pypi/only@1.0.0" ) ) ;
302290
303- // Exempt when the ledger wires this purl at this uuid…
291+ // A ledger at this UUID cannot make a binary lock vendorable.
304292 seed_bun_vendor_entry ( tmp. path ( ) , PURL , UUID ) ;
305293 let refusal = bun_vendor_preflight ( tmp. path ( ) , & npm) . await . unwrap ( ) ;
306294 assert_eq ! ( refusal. code, "vendor_bun_lockb_unsupported" ) ;
307- assert ! ( ! refusal. applies_to( PURL ) , "in-sync ledger entry is exempt " ) ;
295+ assert ! ( refusal. applies_to( PURL ) , "the live lock must be compatible " ) ;
308296
309297 // …but a corrupt ledger exempts nothing and names itself.
310298 std:: fs:: write ( tmp. path ( ) . join ( ".socket/vendor/state.json" ) , b"{ not json" ) . unwrap ( ) ;
@@ -350,10 +338,14 @@ mod tests {
350338 "a fresh registry instance is refused"
351339 ) ;
352340
353- // Vendored at UUID, ledger in sync: exempt (both rules agree).
354- std:: fs:: write ( tmp. path ( ) . join ( "bun.lock" ) , vendored_lock ( UUID ) ) . unwrap ( ) ;
341+ // A preserved ledger does not make registry wiring exempt.
355342 seed_bun_vendor_entry ( tmp. path ( ) , PURL , UUID ) ;
356343 let refusal = bun_vendor_preflight ( tmp. path ( ) , & fresh) . await . unwrap ( ) ;
344+ assert ! ( refusal. applies_to( PURL ) ) ;
345+
346+ // Live vendored tuples remain exempt.
347+ std:: fs:: write ( tmp. path ( ) . join ( "bun.lock" ) , vendored_lock ( UUID ) ) . unwrap ( ) ;
348+ let refusal = bun_vendor_preflight ( tmp. path ( ) , & fresh) . await . unwrap ( ) ;
357349 assert ! ( !refusal. applies_to( PURL ) , "in-sync re-run is exempt" ) ;
358350
359351 // Superseding uuid: the ledger disagrees, the lock says ours → exempt.
0 commit comments