@@ -61,19 +61,23 @@ async fn preview_vendor_json(cwd: &Path, selected: &[PatchSearchResult]) -> serd
6161/// [`download_patch_records`]; no manifest involvement at all).
6262///
6363/// `Ok((has_errors, envelope))` on a run that reached the engine;
64- /// `Err((code, message))` for the lock/stage/manifest failures the
65- /// caller folds into its own output shape (scan's ad-hoc JSON can't use
66- /// `acquire_or_emit`, which prints an Envelope).
64+ /// `Err((code, message, envelope))` for the lock/stage/manifest failures
65+ /// the caller folds into its own output shape (scan's ad-hoc JSON can't
66+ /// use `acquire_or_emit`, which prints an Envelope). The error carries
67+ /// the envelope built so far when the failure happened AFTER
68+ /// `reconcile_dropped` ran — the reconcile mutates the on-disk ledger,
69+ /// and its events must survive the error fold or the JSON consumer
70+ /// never learns about the mutation.
6771async fn run_scan_vendor_step (
6872 common : & GlobalArgs ,
6973 manifest_path : & Path ,
7074 socket_dir : & Path ,
7175 detached_records : Option < & HashMap < String , PatchRecord > > ,
72- ) -> Result < ( bool , Envelope ) , ( & ' static str , String ) > {
76+ ) -> Result < ( bool , Envelope ) , ( & ' static str , String , Option < Box < Envelope > > ) > {
7377 // The download phase created `.socket/` already in every flow that
7478 // reaches here, but `acquire` deliberately refuses to mkdir.
7579 if let Err ( e) = tokio:: fs:: create_dir_all ( socket_dir) . await {
76- return Err ( ( "socket_dir_unwritable" , e. to_string ( ) ) ) ;
80+ return Err ( ( "socket_dir_unwritable" , e. to_string ( ) , None ) ) ;
7781 }
7882 let guard = apply_lock:: acquire (
7983 socket_dir,
@@ -83,8 +87,9 @@ async fn run_scan_vendor_step(
8387 apply_lock:: LockError :: Held => (
8488 "lock_held" ,
8589 "another socket-patch process is operating in this directory" . to_string ( ) ,
90+ None ,
8691 ) ,
87- apply_lock:: LockError :: Io { .. } => ( "lock_io" , e. to_string ( ) ) ,
92+ apply_lock:: LockError :: Io { .. } => ( "lock_io" , e. to_string ( ) , None ) ,
8893 } ) ?;
8994
9095 let mut env = Envelope :: new ( EnvelopeCommand :: Vendor ) ;
@@ -111,7 +116,7 @@ async fn run_scan_vendor_step(
111116 drop ( guard) ;
112117 return Ok ( ( false , env) ) ;
113118 }
114- Err ( e) => return Err ( ( "invalid_manifest" , e. to_string ( ) ) ) ,
119+ Err ( e) => return Err ( ( "invalid_manifest" , e. to_string ( ) , None ) ) ,
115120 } ;
116121 // Same placement as the `vendor` command: dropped entries
117122 // are reverted even when zero in-scope patches remain.
@@ -123,12 +128,15 @@ async fn run_scan_vendor_step(
123128 match stage_vendor_sources_in_memory ( common, & manifest, socket_dir, & common. cwd ) . await {
124129 Ok ( MemStageOutcome :: Ready ( s) ) => s,
125130 Ok ( MemStageOutcome :: Unavailable ) => {
131+ // The reconcile above may have already reverted dropped
132+ // entries on disk — hand its envelope to the error fold.
126133 return Err ( (
127134 "no_local_source" ,
128135 "patch artifacts unavailable (offline or download failure)" . to_string ( ) ,
129- ) )
136+ Some ( Box :: new ( env) ) ,
137+ ) ) ;
130138 }
131- Err ( e) => return Err ( ( "stage_failed" , e) ) ,
139+ Err ( e) => return Err ( ( "stage_failed" , e, Some ( Box :: new ( env ) ) ) ) ,
132140 } ;
133141 let sources = staged. as_patch_sources ( ) ;
134142 has_errors |=
@@ -280,14 +288,21 @@ async fn run_vendor_json_path(
280288 serde_json:: to_value ( & venv) . unwrap_or_else ( |_| serde_json:: json!( { } ) ) ;
281289 i32:: from ( has_errors)
282290 }
283- Err ( ( code, message) ) => {
291+ Err ( ( code, message, venv ) ) => {
284292 track_patch_vendor_failed (
285293 & message,
286294 args. common . dry_run ,
287295 telemetry_token,
288296 telemetry_org,
289297 )
290298 . await ;
299+ // A pre-failure reconcile already mutated the ledger on disk;
300+ // its envelope (events included) must reach the JSON consumer
301+ // even though the run aborts here.
302+ if let Some ( venv) = venv {
303+ result[ "vendor" ] =
304+ serde_json:: to_value ( & * venv) . unwrap_or_else ( |_| serde_json:: json!( { } ) ) ;
305+ }
291306 result[ "status" ] = serde_json:: json!( "error" ) ;
292307 result[ "error" ] = serde_json:: json!( {
293308 "code" : code,
@@ -378,7 +393,10 @@ async fn run_vendor_interactive_path(
378393 . await ;
379394 i32:: from ( has_errors)
380395 }
381- Err ( ( code, message) ) => {
396+ // Human mode prints no per-event lines even on success, so the
397+ // carried envelope has no human rendering to feed — JSON mode is
398+ // where the reconcile events must survive (see the JSON fold above).
399+ Err ( ( code, message, _venv) ) => {
382400 track_patch_vendor_failed (
383401 & message,
384402 args. common . dry_run ,
@@ -534,7 +552,11 @@ fn boxed_scan_vendor_step<'a>(
534552 socket_dir : & ' a Path ,
535553 detached_records : Option < & ' a HashMap < String , PatchRecord > > ,
536554) -> std:: pin:: Pin <
537- Box < dyn std:: future:: Future < Output = Result < ( bool , Envelope ) , ( & ' static str , String ) > > + ' a > ,
555+ Box <
556+ dyn std:: future:: Future <
557+ Output = Result < ( bool , Envelope ) , ( & ' static str , String , Option < Box < Envelope > > ) > ,
558+ > + ' a ,
559+ > ,
538560> {
539561 Box :: pin ( run_scan_vendor_step (
540562 common,
0 commit comments