.trae/documents/Restrict Public API to BundleManager.md
···11-## Goal
22-Expose only the `BundleManager` API (and the minimal inputs/outputs it requires) to library users, hiding all other modules and helpers.
33-44-## Strategy
55-- Turn crate root into a narrow facade that re-exports `BundleManager` and its associated types.
66-- Make all non-essential modules private (`mod` or `pub(crate)`) instead of `pub mod`.
77-- Keep only the re-exports required by `BundleManager` method signatures.
88-- Gate CLI/server/FFI pieces behind features so they are not part of the default public surface.
99-1010-## Current Surface (reference)
1111-- `BundleManager` is defined at `src/manager.rs:83` and implemented starting `src/manager.rs:204`.
1212-- Crate root re-exports a large set starting at `src/lib.rs:73`.
1313-- Many modules are public at `src/lib.rs:33–55`.
1414-1515-## Changes in `src/lib.rs`
1616-- Module visibility:
1717- - Change all `pub mod ...` to `pub(crate) mod ...` (or `mod ...`) except `manager`.
1818- - Examples: `bundle_format`, `cache`, `constants`, `did_index`, `format`, `handle_resolver`, `index`, `iterators`, `mempool`, `operations`, `options`, `plc_client`, `processor`, `remote`, `resolver`, `runtime`, `sync`, `verification`, `server` become crate-private.
1919-- Re-export trimming:
2020- - Remove broad re-exports at `src/lib.rs:57–93`.
2121- - Keep only items needed by `BundleManager` signatures:
2222- - Constructors/options: `ManagerOptions`, `IntoManagerOptions`.
2323- - Query/export: `QuerySpec`, `QueryMode`, `ExportSpec`, `ExportIterator`, `QueryIterator`, `RangeIterator`.
2424- - Loading: `LoadOptions`, `LoadResult`.
2525- - Operations: `Operation`, `OperationRequest`, `OperationWithLocation`, `OperationResult`.
2626- - DID: `ResolveResult`, `DIDIndexStats`, `DIDLookupStats`, `DIDLookupTimings`, `DIDOperationsResult` (if present; otherwise keep the return struct used in `get_did_operations()` at `src/manager.rs:510–586`).
2727- - Verification: `VerifySpec`, `VerifyResult`, `ChainVerifySpec`, `ChainVerifyResult`.
2828- - Info/Stats: `InfoFlags`, `BundleInfo`, `ManagerStats`, `SizeInfo`.
2929- - Rollback: `RollbackSpec`, `RollbackPlan`, `RollbackResult`.
3030- - Warm-up: `WarmUpSpec`, `WarmUpStrategy`.
3131- - Re-export from `manager` when possible, otherwise re-export specific types from their modules without exposing the whole module.
3232-- Optional prelude:
3333- - Add `pub mod prelude` (facade-only) that re-exports the curated set for `use plcbundle::prelude::*;` consumers.
3434-3535-## Changes in internal modules
3636-- Where an internal type is only used by `BundleManager`, make it `pub(crate)` and reference via `crate::module::Type` from `manager`.
3737-- Avoid leaking helpers (functions and structs) by keeping them `pub(crate)` or private.
3838-- If a `BundleManager` method requires a type from another module, prefer re-exporting that type at the crate root rather than making the module public.
3939-4040-## Feature gating
4141-- Hide CLI/server by default:
4242- - Ensure `server` remains behind `#[cfg(feature = "server")]` and is not re-exported.
4343- - Keep `ffi` off by default; expose only via `feature = "ffi"` for `cdylib` consumers.
4444-- Confirm `[lib] crate-type = ["cdylib", "rlib"]` remains, but do not publicly expose `ffi` unless the feature is enabled.
4545-4646-## Docs and examples
4747-- Update crate-level docs to show only `use plcbundle::{BundleManager, ManagerOptions, QuerySpec, BundleRange, QueryMode};` (already present in `src/lib.rs:11–25`).
4848-- Ensure rustdoc for internal modules is hidden or marked as crate-private.
4949-- Optionally add `#![deny(missing_docs)]` and document the curated public types.
5050-5151-## Validation
5252-- Build and run rustdoc to verify only the curated items appear in the public API.
5353-- Compile with `--all-features` to ensure feature-gated code compiles.
5454-- Run existing tests; add a test that `use plcbundle::*;` only imports the intended items.
5555-5656-## Result
5757-Library users see a minimal surface:
5858-- `use plcbundle::{BundleManager, ManagerOptions, QuerySpec, BundleRange, QueryMode, ...}`
5959-- Everything else is crate-private, accessible only via `BundleManager` methods.
6060-6161-Confirm, and I will implement these visibility and re-export adjustments in `src/lib.rs` and tighten module visibilities accordingly.
+19-13
src/bundle_format.rs
···310310 })
311311}
312312313313-314313/// Serialize operations to JSONL (uncompressed)
315314///
316315/// CRITICAL: This function implements the V1 specification requirement (docs/specification.md § 4.2)
···349348 Ok(format!("{:x}", hasher.finalize()))
350349}
351350352352-353353-354351/// Create bundle metadata structure
355352#[allow(clippy::too_many_arguments)]
356353pub fn create_bundle_metadata(
···359356 content_hash: &str,
360357 parent_hash: Option<&str>,
361358 uncompressed_size: Option<u64>,
362362- compressed_size: Option<u64>,
359359+ compressed_size: Option<u64>,
363360 operation_count: usize,
364361 did_count: usize,
365362 start_time: &str,
···375372 content_hash: content_hash.to_string(),
376373 parent_hash: parent_hash.map(|s| s.to_string()),
377374 uncompressed_size,
378378- compressed_size,
375375+ compressed_size,
379376 operation_count,
380380- did_count,
377377+ did_count,
381378 start_time: start_time.to_string(),
382379 end_time: end_time.to_string(),
383380 created_at: chrono::Utc::now().to_rfc3339(),
···462459 let mut operations = Vec::new();
463460 for i in 0..250 {
464461 // Multiple frames (250 ops = 3 frames with FRAME_SIZE=100)
465465- let operation_json = format!(
466466- r#"{{"type":"create","data":"test data {}"}}"#,
467467- i
468468- );
462462+ let operation_json = format!(r#"{{"type":"create","data":"test data {}"}}"#, i);
469463 let operation_value: Value = from_str(&operation_json).unwrap();
470464 let extra_value: Value = from_str("{}").unwrap_or_else(|_| Value::new());
471465 operations.push(Operation {
···599593600594 // Write with invalid magic (not in skippable range)
601595 buffer.write_all(&0x12345678u32.to_le_bytes()).unwrap();
602602- buffer.write_all(&(data.len() as u32).to_le_bytes()).unwrap();
596596+ buffer
597597+ .write_all(&(data.len() as u32).to_le_bytes())
598598+ .unwrap();
603599 buffer.write_all(data).unwrap();
604600605601 let mut cursor = std::io::Cursor::new(&buffer);
606602 let result = read_skippable_frame(&mut cursor);
607603 assert!(result.is_err());
608608- assert!(result.unwrap_err().to_string().contains("Not a skippable frame"));
604604+ assert!(
605605+ result
606606+ .unwrap_err()
607607+ .to_string()
608608+ .contains("Not a skippable frame")
609609+ );
609610 }
610611611612 #[test]
···682683683684 let mut buffer = Vec::new();
684685 // Write with wrong magic
685685- write_skippable_frame(&mut buffer, 0x184D2A51, &sonic_rs::to_vec(&metadata).unwrap()).unwrap();
686686+ write_skippable_frame(
687687+ &mut buffer,
688688+ 0x184D2A51,
689689+ &sonic_rs::to_vec(&metadata).unwrap(),
690690+ )
691691+ .unwrap();
686692687693 let mut cursor = std::io::Cursor::new(&buffer);
688694 let result = read_metadata_frame(&mut cursor);
+16-11
src/cache.rs
···23232424 pub fn insert(&self, bundle: u32, ops: Vec<Operation>) {
2525 let mut cache = self.cache.write().unwrap();
2626- if cache.len() >= self.capacity && let Some(&key) = cache.keys().next() {
2626+ if cache.len() >= self.capacity
2727+ && let Some(&key) = cache.keys().next()
2828+ {
2729 cache.remove(&key);
2830 }
2931 cache.insert(bundle, ops);
···76787779 cache.insert(1, ops.clone());
7880 assert!(cache.contains(1));
7979-8181+8082 let retrieved = cache.get(1);
8183 assert!(retrieved.is_some());
8284 assert_eq!(retrieved.unwrap().len(), 2);
···8688 fn test_cache_contains() {
8789 let cache = BundleCache::new(10);
8890 assert!(!cache.contains(1));
8989-9191+9092 cache.insert(1, vec![create_test_operation("did:plc:test1")]);
9193 assert!(cache.contains(1));
9294 assert!(!cache.contains(2));
···9799 let cache = BundleCache::new(10);
98100 cache.insert(1, vec![create_test_operation("did:plc:test1")]);
99101 assert!(cache.contains(1));
100100-102102+101103 cache.remove(1);
102104 assert!(!cache.contains(1));
103105 }
···109111 cache.insert(2, vec![create_test_operation("did:plc:test2")]);
110112 assert!(cache.contains(1));
111113 assert!(cache.contains(2));
112112-114114+113115 cache.clear();
114116 assert!(!cache.contains(1));
115117 assert!(!cache.contains(2));
···118120 #[test]
119121 fn test_cache_capacity_eviction() {
120122 let cache = BundleCache::new(2);
121121-123123+122124 // Fill cache to capacity
123125 cache.insert(1, vec![create_test_operation("did:plc:test1")]);
124126 cache.insert(2, vec![create_test_operation("did:plc:test2")]);
125127 assert!(cache.contains(1));
126128 assert!(cache.contains(2));
127127-129129+128130 // Adding third should evict one (HashMap iteration order is not guaranteed)
129131 cache.insert(3, vec![create_test_operation("did:plc:test3")]);
130132 // One of the first two should be evicted, and 3 should be present
···140142 #[test]
141143 fn test_cache_multiple_bundles() {
142144 let cache = BundleCache::new(10);
143143-145145+144146 for i in 1..=5 {
145145- cache.insert(i, vec![create_test_operation(&format!("did:plc:test{}", i))]);
147147+ cache.insert(
148148+ i,
149149+ vec![create_test_operation(&format!("did:plc:test{}", i))],
150150+ );
146151 }
147147-152152+148153 for i in 1..=5 {
149154 assert!(cache.contains(i));
150155 let ops = cache.get(i).unwrap();
···157162 fn test_cache_empty_operations() {
158163 let cache = BundleCache::new(10);
159164 cache.insert(1, vec![]);
160160-165165+161166 let ops = cache.get(1);
162167 assert!(ops.is_some());
163168 assert_eq!(ops.unwrap().len(), 0);
+14-8
src/cli/cmd_bench.rs
···403403 let mut bundle_op_counts = Vec::with_capacity(bundles.len());
404404 for &bundle_num in &bundles {
405405 if let Ok(bundle) = manager.load_bundle(bundle_num, LoadOptions::default())
406406- && !bundle.operations.is_empty() {
407407- bundle_op_counts.push((bundle_num, bundle.operations.len()));
408408- }
406406+ && !bundle.operations.is_empty()
407407+ {
408408+ bundle_op_counts.push((bundle_num, bundle.operations.len()));
409409+ }
409410 }
410411411412 if bundle_op_counts.is_empty() {
···421422422423 // Benchmark - random bundle and random position each iteration
423424 let mut timings = Vec::with_capacity(iterations);
424424-425425+425426 let pb = if interactive {
426427 Some(ProgressBar::new(iterations))
427428 } else {
···481482 // Ensure DID index is loaded (sample_random_dids already does this, but be explicit)
482483 // The did_index will be loaded by sample_random_dids above
483484 let did_index = manager.get_did_index();
484484-485485+485486 // Ensure it's actually loaded (in case sample_random_dids didn't load it)
486487 {
487488 let guard = did_index.read().unwrap();
···502503503504 // Benchmark - different DID each iteration
504505 let mut timings = Vec::with_capacity(iterations);
505505-506506+506507 let pb = if interactive {
507508 Some(ProgressBar::new(iterations))
508509 } else {
···516517517518 let did = &dids[i % dids.len()];
518519 let start = Instant::now();
519519- let _ = did_index.read().unwrap().as_ref().unwrap().get_did_locations(did)?;
520520+ let _ = did_index
521521+ .read()
522522+ .unwrap()
523523+ .as_ref()
524524+ .unwrap()
525525+ .get_did_locations(did)?;
520526 timings.push(start.elapsed().as_secs_f64() * 1000.0);
521527 }
522528···554560 // Benchmark - different DID each iteration
555561 manager.clear_caches();
556562 let mut timings = Vec::with_capacity(iterations);
557557-563563+558564 let pb = if interactive {
559565 Some(ProgressBar::new(iterations))
560566 } else {
+20-10
src/cli/cmd_clean.rs
···82828383 // Show errors if any
8484 if let Some(errors) = &result.errors
8585- && !errors.is_empty() {
8686- eprintln!("\n⚠ Warning: Some errors occurred during cleanup:");
8787- for error in errors {
8888- eprintln!(" - {}", error);
8989- }
8585+ && !errors.is_empty()
8686+ {
8787+ eprintln!("\n⚠ Warning: Some errors occurred during cleanup:");
8888+ for error in errors {
8989+ eprintln!(" - {}", error);
9090 }
9191+ }
91929293 Ok(())
9394}
···120121 if !root_files.is_empty() {
121122 println!(" Repository root:");
122123 for file in &root_files {
123123- let rel_path = file.path.strip_prefix(dir)
124124+ let rel_path = file
125125+ .path
126126+ .strip_prefix(dir)
124127 .unwrap_or(&file.path)
125128 .to_string_lossy();
126129 println!(" • {} ({})", rel_path, utils::format_bytes(file.size));
···132135 if !config_files.is_empty() {
133136 println!(" DID index directory:");
134137 for file in &config_files {
135135- let rel_path = file.path.strip_prefix(dir)
138138+ let rel_path = file
139139+ .path
140140+ .strip_prefix(dir)
136141 .unwrap_or(&file.path)
137142 .to_string_lossy();
138143 println!(" • {} ({})", rel_path, utils::format_bytes(file.size));
···144149 if !shard_files.is_empty() {
145150 println!(" Shards directory:");
146151 for file in &shard_files {
147147- let rel_path = file.path.strip_prefix(dir)
152152+ let rel_path = file
153153+ .path
154154+ .strip_prefix(dir)
148155 .unwrap_or(&file.path)
149156 .to_string_lossy();
150157 println!(" • {} ({})", rel_path, utils::format_bytes(file.size));
···152159 println!();
153160 }
154161155155- println!(" Total: {} file(s), {}", preview.files.len(), utils::format_bytes(preview.total_size));
162162+ println!(
163163+ " Total: {} file(s), {}",
164164+ preview.files.len(),
165165+ utils::format_bytes(preview.total_size)
166166+ );
156167 println!();
157168158169 Ok(())
···168179 let response = response.trim().to_lowercase();
169180 Ok(response == "y" || response == "yes")
170181}
171171-
+24-15
src/cli/cmd_clone.rs
···11use anyhow::{Context, Result};
22use clap::{Args, ValueHint};
33-use plcbundle::{constants, remote::RemoteClient, BundleManager};
33+use plcbundle::{BundleManager, constants, remote::RemoteClient};
44use std::path::PathBuf;
55use std::sync::Arc;
66···51515252pub fn run(cmd: CloneCommand) -> Result<()> {
5353 // Create tokio runtime for async operations
5454- tokio::runtime::Runtime::new()?.block_on(async {
5555- run_async(cmd).await
5656- })
5454+ tokio::runtime::Runtime::new()?.block_on(async { run_async(cmd).await })
5755}
58565957async fn run_async(cmd: CloneCommand) -> Result<()> {
···115113116114 // Create target directory if it doesn't exist
117115 if !target_dir.exists() {
118118- std::fs::create_dir_all(&target_dir)
119119- .context("Failed to create target directory")?;
116116+ std::fs::create_dir_all(&target_dir).context("Failed to create target directory")?;
120117 }
121118122119 // Check if target directory is empty or if resuming
···181178 if let Some(free_space) = super::utils::get_free_disk_space(&target_dir) {
182179 // Add 10% buffer for safety (filesystem overhead, temporary files, etc.)
183180 let required_space = total_bytes + (total_bytes / 10);
184184-181181+185182 if free_space < required_space {
186183 let free_display = plcbundle::format::format_bytes(free_space);
187184 let required_display = plcbundle::format::format_bytes(required_space);
188185 let shortfall = required_space - free_space;
189186 let shortfall_display = plcbundle::format::format_bytes(shortfall);
190190-187187+191188 eprintln!("⚠️ Warning: Insufficient disk space");
192189 eprintln!(" Required: {}", required_display);
193190 eprintln!(" Available: {}", free_display);
194191 eprintln!(" Shortfall: {}", shortfall_display);
195192 eprintln!();
196196-193193+197194 // Prompt user to continue
198195 use dialoguer::Confirm;
199196 let proceed = Confirm::new()
···201198 .default(false)
202199 .interact()
203200 .context("Failed to read user input")?;
204204-201201+205202 if !proceed {
206203 anyhow::bail!("Clone cancelled by user");
207204 }
208208-205205+209206 println!();
210207 }
211208 }
···214211 println!();
215212216213 // Create progress bar with byte tracking
217217- let progress = Arc::new(super::progress::ProgressBar::with_bytes(bundles_count, total_bytes));
214214+ let progress = Arc::new(super::progress::ProgressBar::with_bytes(
215215+ bundles_count,
216216+ total_bytes,
217217+ ));
218218219219 // Clone using BundleManager API with progress callback
220220 let progress_clone = Arc::clone(&progress);
···233233 println!();
234234235235 if failed_count > 0 {
236236- eprintln!("✗ Clone incomplete: {} succeeded, {} failed", downloaded_count, failed_count);
236236+ eprintln!(
237237+ "✗ Clone incomplete: {} succeeded, {} failed",
238238+ downloaded_count, failed_count
239239+ );
237240 eprintln!(" Use --resume to retry failed downloads");
238241 anyhow::bail!("Clone failed");
239242 }
···244247 println!();
245248 println!("Next steps:");
246249 println!(" cd {}", display_path(&target_dir).display());
247247- println!(" {} status # Check repository status", constants::BINARY_NAME);
248248- println!(" {} sync # Sync to latest", constants::BINARY_NAME);
250250+ println!(
251251+ " {} status # Check repository status",
252252+ constants::BINARY_NAME
253253+ );
254254+ println!(
255255+ " {} sync # Sync to latest",
256256+ constants::BINARY_NAME
257257+ );
249258 println!(" {} server --sync # Run server", constants::BINARY_NAME);
250259251260 Ok(())
+40-32
src/cli/cmd_compare.rs
···11// Compare command - compare repositories
22use anyhow::{Context, Result, bail};
33use clap::{Args, ValueHint};
44-use plcbundle::{BundleManager, constants, remote};
54use plcbundle::constants::bundle_position_to_global;
55+use plcbundle::{BundleManager, constants, remote};
66use sonic_rs::JsonValueTrait;
77use std::collections::HashMap;
88use std::path::{Path, PathBuf};
···7373 let last_bundle = manager.get_last_bundle();
7474 let bundle_nums = super::utils::parse_bundle_spec(Some(bundles_str), last_bundle)?;
7575 if bundle_nums.len() != 1 {
7676- anyhow::bail!("--bundles must specify a single bundle number for comparison (e.g., \"23\")");
7676+ anyhow::bail!(
7777+ "--bundles must specify a single bundle number for comparison (e.g., \"23\")"
7878+ );
7779 }
7880 let bundle_num = bundle_nums[0];
7981 rt.block_on(diff_specific_bundle(
···126128 eprintln!("📥 Loading target index...");
127129 let target_index = if target.starts_with("http://") || target.starts_with("https://") {
128130 let client = remote::RemoteClient::new(target)?;
129129- client.fetch_index().await
131131+ client
132132+ .fetch_index()
133133+ .await
130134 .context("Failed to load target index")?
131135 } else {
132132- remote::load_local_index(target)
133133- .context("Failed to load target index")?
136136+ remote::load_local_index(target).context("Failed to load target index")?
134137 };
135138136139 // Check origins - CRITICAL: must match for valid comparison
···204207 eprintln!("📥 Loading remote index...");
205208 let remote_index = if target.starts_with("http://") || target.starts_with("https://") {
206209 let client = remote::RemoteClient::new(target)?;
207207- client.fetch_index().await
210210+ client
211211+ .fetch_index()
212212+ .await
208213 .context("Failed to load remote index")?
209214 } else {
210210- remote::load_local_index(target)
211211- .context("Failed to load remote index")?
215215+ remote::load_local_index(target).context("Failed to load remote index")?
212216 };
213217 let target_origin = &remote_index.origin;
214218···256260 eprintln!("📦 Loading remote bundle {}...\n", bundle_num);
257261 let remote_ops = if target.starts_with("http://") || target.starts_with("https://") {
258262 let client = remote::RemoteClient::new(target)?;
259259- client.fetch_bundle_operations(bundle_num)
263263+ client
264264+ .fetch_bundle_operations(bundle_num)
260265 .await
261266 .context("Failed to load remote bundle")?
262267 } else {
···270275 };
271276272277 // Try to load bundle from target directory
273273- let target_manager = super::utils::create_manager(target_dir.to_path_buf(), false, false, false)?;
278278+ let target_manager =
279279+ super::utils::create_manager(target_dir.to_path_buf(), false, false, false)?;
274280 let target_result = target_manager
275281 .load_bundle(bundle_num, plcbundle::LoadOptions::default())
276282 .context("Failed to load bundle from target directory")?;
···612618 } else {
613619 // Just missing/extra bundles - not critical
614620 eprintln!("ℹ️ Indexes differ (missing or extra bundles, but hashes match)");
615615- eprintln!(
616616- " This is normal when comparing repositories at different sync states."
617617- );
621621+ eprintln!(" This is normal when comparing repositories at different sync states.");
618622 }
619623 }
620624}
···1113111711141118 // Add hints for exploring missing operations
11151119 if let Some((first_cid, first_pos)) = missing_in_local.first()
11161116- && target.starts_with("http") {
11171117- let base_url = if let Some(stripped) = target.strip_suffix('/') {
11181118- stripped
11191119- } else {
11201120- target
11211121- };
11221122- let global_pos = bundle_position_to_global(bundle_num, *first_pos);
11231123- eprintln!(" 💡 To explore missing operations:");
11241124- eprintln!(
11251125- " • Global position: {} (bundle {} position {})",
11261126- global_pos, bundle_num, first_pos
11271127- );
11281128- eprintln!(
11291129- " • View in remote: curl '{}/op/{}' | grep '{}' | jq .",
11301130- base_url, global_pos, first_cid
11311131- );
11201120+ && target.starts_with("http")
11211121+ {
11221122+ let base_url = if let Some(stripped) = target.strip_suffix('/') {
11231123+ stripped
11241124+ } else {
11251125+ target
11261126+ };
11271127+ let global_pos = bundle_position_to_global(bundle_num, *first_pos);
11281128+ eprintln!(" 💡 To explore missing operations:");
11291129+ eprintln!(
11301130+ " • Global position: {} (bundle {} position {})",
11311131+ global_pos, bundle_num, first_pos
11321132+ );
11331133+ eprintln!(
11341134+ " • View in remote: curl '{}/op/{}' | grep '{}' | jq .",
11351135+ base_url, global_pos, first_cid
11361136+ );
11321137 }
11331138 eprintln!();
11341139 }
···1162116711631168 // Add hints for exploring missing operations
11641169 if let Some((_first_cid, first_pos)) = missing_in_remote.first() {
11651165- let global_pos =
11661166- bundle_position_to_global(bundle_num, *first_pos);
11701170+ let global_pos = bundle_position_to_global(bundle_num, *first_pos);
11671171 eprintln!(" 💡 To explore missing operations:");
11681172 eprintln!(
11691173 " • Global position: {} (bundle {} position {})",
···12111215 sample_size.min(local_ops.len())
12121216 );
12131217 eprintln!("────────────────────────────────");
12141214- for (i, op) in local_ops.iter().enumerate().take(sample_size.min(local_ops.len())) {
12181218+ for (i, op) in local_ops
12191219+ .iter()
12201220+ .enumerate()
12211221+ .take(sample_size.min(local_ops.len()))
12221222+ {
12151223 let remote_match = if let Some(ref cid) = op.cid {
12161224 if let Some(&remote_pos) = remote_cids.get(cid) {
12171225 if remote_pos == i {
+33-15
src/cli/cmd_completions.rs
···11// Completions command - generate shell completion scripts
22use anyhow::Result;
33use clap::{Args, CommandFactory, ValueEnum};
44-use clap_complete::{generate, Shell};
44+use clap_complete::{Shell, generate};
55use plcbundle::constants;
66use std::io;
77···8484 let shell: Shell = shell_arg.into();
8585 let mut app = super::Cli::command();
8686 let bin_name = app.get_name().to_string();
8787-8787+8888 generate(shell, &mut app, bin_name, &mut io::stdout());
8989 } else {
9090 // Show instructions
9191 show_instructions();
9292 }
9393-9393+9494 Ok(())
9595}
96969797fn show_instructions() {
9898 let bin_name = constants::BINARY_NAME;
9999-9999+100100 println!("Shell Completion Setup Instructions");
101101 println!("════════════════════════════════════\n");
102102- println!("Generate completion scripts for your shell to enable tab completion for {}.\n", bin_name);
102102+ println!(
103103+ "Generate completion scripts for your shell to enable tab completion for {}.\n",
104104+ bin_name
105105+ );
103106 println!("Usage:");
104107 println!(" {} completions <SHELL>\n", bin_name);
105108 println!("Supported shells:");
···108111 println!(" fish - Fish completion script");
109112 println!(" powershell - PowerShell completion script\n");
110113 println!("Examples:\n");
111111-114114+112115 // Bash
113116 println!("Bash:");
114117 println!(" # Generate completion script");
115115- println!(" {} completions bash > ~/.bash_completion.d/{}", bin_name, bin_name);
118118+ println!(
119119+ " {} completions bash > ~/.bash_completion.d/{}",
120120+ bin_name, bin_name
121121+ );
116122 println!(" # Add to ~/.bashrc:");
117117- println!(" echo 'source ~/.bash_completion.d/{}' >> ~/.bashrc", bin_name);
123123+ println!(
124124+ " echo 'source ~/.bash_completion.d/{}' >> ~/.bashrc",
125125+ bin_name
126126+ );
118127 println!(" source ~/.bashrc\n");
119119-128128+120129 // Zsh
121130 println!("Zsh:");
122131 println!(" # Generate completion script");
123132 println!(" mkdir -p ~/.zsh/completions");
124124- println!(" {} completions zsh > ~/.zsh/completions/_{}", bin_name, bin_name);
133133+ println!(
134134+ " {} completions zsh > ~/.zsh/completions/_{}",
135135+ bin_name, bin_name
136136+ );
125137 println!(" # Add to ~/.zshrc:");
126138 println!(" echo 'fpath=(~/.zsh/completions $fpath)' >> ~/.zshrc");
127139 println!(" echo 'autoload -U compinit && compinit' >> ~/.zshrc");
128140 println!(" source ~/.zshrc\n");
129129-141141+130142 // Fish
131143 println!("Fish:");
132144 println!(" # Generate completion script");
133145 println!(" mkdir -p ~/.config/fish/completions");
134134- println!(" {} completions fish > ~/.config/fish/completions/{}.fish", bin_name, bin_name);
146146+ println!(
147147+ " {} completions fish > ~/.config/fish/completions/{}.fish",
148148+ bin_name, bin_name
149149+ );
135150 println!(" # Fish will automatically load completions from this directory\n");
136136-151151+137152 // PowerShell
138153 println!("PowerShell:");
139154 println!(" # Generate completion script");
···143158 println!(" # Or add to your PowerShell profile:");
144159 println!(" . $PROFILE # if it exists, or create it");
145160 println!(" echo '. ./{}.ps1' >> $PROFILE\n", bin_name);
146146-161161+147162 println!("After installation, restart your shell or source the configuration file.");
148148- println!("Then you can use tab completion for {} commands and arguments!", bin_name);
163163+ println!(
164164+ "Then you can use tab completion for {} commands and arguments!",
165165+ bin_name
166166+ );
149167}
+184-138
src/cli/cmd_did.rs
···210210 raw,
211211 compare,
212212 } => {
213213- cmd_did_resolve(dir, did, handle_resolver, global_verbose, query, raw, compare)?;
213213+ cmd_did_resolve(
214214+ dir,
215215+ did,
216216+ handle_resolver,
217217+ global_verbose,
218218+ query,
219219+ raw,
220220+ compare,
221221+ )?;
214222 }
215215- DIDCommands::Log { did, json, format, no_header, separator, reverse } => {
216216- cmd_did_lookup(dir, did, global_verbose, json, format, no_header, separator, reverse)?;
223223+ DIDCommands::Log {
224224+ did,
225225+ json,
226226+ format,
227227+ no_header,
228228+ separator,
229229+ reverse,
230230+ } => {
231231+ cmd_did_lookup(
232232+ dir,
233233+ did,
234234+ global_verbose,
235235+ json,
236236+ format,
237237+ no_header,
238238+ separator,
239239+ reverse,
240240+ )?;
217241 }
218242 DIDCommands::Batch {
219243 action,
···285309286310 // If compare is enabled, fetch remote document and compare
287311 if let Some(maybe_url) = compare {
288288- use tokio::runtime::Runtime;
289312 use crate::plc_client::PLCClient;
290313 use std::time::Instant;
314314+ use tokio::runtime::Runtime;
291315292316 // Use provided URL, or use repository origin, or fall back to default
293317 let plc_url = match maybe_url {
···296320 log::info!("Using provided PLC directory URL: {}", url);
297321 }
298322 url
299299- },
323323+ }
300324 _ => {
301325 // Get origin from local repository index
302326 let local_index = manager.get_index();
303327 let origin = &local_index.origin;
304304-328328+305329 // If origin is "local" or empty, use default PLC directory
306330 if origin == "local" || origin.is_empty() {
307331 if verbose {
308308- log::info!("Origin is '{}', using default PLC directory: {}", origin, constants::DEFAULT_PLC_DIRECTORY_URL);
332332+ log::info!(
333333+ "Origin is '{}', using default PLC directory: {}",
334334+ origin,
335335+ constants::DEFAULT_PLC_DIRECTORY_URL
336336+ );
309337 }
310338 constants::DEFAULT_PLC_DIRECTORY_URL.to_string()
311339 } else {
···318346 };
319347320348 eprintln!("🔍 Comparing with remote PLC directory...");
321321-349349+322350 if verbose {
323351 log::info!("Target PLC directory: {}", plc_url);
324352 log::info!("DID to fetch: {}", did);
325353 }
326326-354354+327355 let fetch_start = Instant::now();
328356 let rt = Runtime::new().context("Failed to create tokio runtime")?;
329357 use plcbundle::resolver::DIDDocument;
330330- let (remote_doc, remote_json_raw): (DIDDocument, String) = rt.block_on(async {
331331- let client = PLCClient::new(&plc_url)
332332- .context("Failed to create PLC client")?;
333333- if verbose {
334334- log::info!("Created PLC client, fetching DID document...");
335335- }
336336- // Fetch both the parsed document and raw JSON for accurate comparison
337337- let raw_json = client.fetch_did_document_raw(&did).await?;
338338- let parsed_doc = client.fetch_did_document(&did).await?;
339339- Ok::<(DIDDocument, String), anyhow::Error>((parsed_doc, raw_json))
340340- })
341341- .context("Failed to fetch DID document from remote PLC directory")?;
358358+ let (remote_doc, remote_json_raw): (DIDDocument, String) = rt
359359+ .block_on(async {
360360+ let client = PLCClient::new(&plc_url).context("Failed to create PLC client")?;
361361+ if verbose {
362362+ log::info!("Created PLC client, fetching DID document...");
363363+ }
364364+ // Fetch both the parsed document and raw JSON for accurate comparison
365365+ let raw_json = client.fetch_did_document_raw(&did).await?;
366366+ let parsed_doc = client.fetch_did_document(&did).await?;
367367+ Ok::<(DIDDocument, String), anyhow::Error>((parsed_doc, raw_json))
368368+ })
369369+ .context("Failed to fetch DID document from remote PLC directory")?;
342370 let fetch_duration = fetch_start.elapsed();
343371344372 if verbose {
···349377 eprintln!();
350378351379 // Compare documents and return early (don't show full document)
352352- compare_did_documents(&result.document, &remote_doc, &remote_json_raw, &did, &plc_url, fetch_duration)?;
380380+ compare_did_documents(
381381+ &result.document,
382382+ &remote_doc,
383383+ &remote_json_raw,
384384+ &did,
385385+ &plc_url,
386386+ fetch_duration,
387387+ )?;
353388 return Ok(());
354389 }
355390···479514 if result.bundle_number == 0 {
480515 // Operation from mempool
481516 let index = manager.get_index();
482482- let global_pos = plcbundle::constants::mempool_position_to_global(index.last_bundle, result.position);
517517+ let global_pos = plcbundle::constants::mempool_position_to_global(
518518+ index.last_bundle,
519519+ result.position,
520520+ );
483521 log::info!(
484522 "Source: mempool position {} (global: {})\n",
485523 result.position,
···487525 );
488526 } else {
489527 // Operation from bundle
490490- let global_pos = plcbundle::constants::bundle_position_to_global(result.bundle_number, result.position);
528528+ let global_pos = plcbundle::constants::bundle_position_to_global(
529529+ result.bundle_number,
530530+ result.position,
531531+ );
491532 log::info!(
492533 "Source: bundle {}, position {} (global: {})\n",
493534 result.bundle_number,
···503544 // If query is provided, apply JMESPath query
504545 let output_json = if let Some(query_expr) = query {
505546 // Compile JMESPath expression
506506- let expr = jmespath::compile(&query_expr)
507507- .map_err(|e| anyhow::anyhow!("Failed to compile JMESPath query '{}': {}", query_expr, e))?;
547547+ let expr = jmespath::compile(&query_expr).map_err(|e| {
548548+ anyhow::anyhow!("Failed to compile JMESPath query '{}': {}", query_expr, e)
549549+ })?;
508550509551 // Execute query
510552 let data = jmespath::Variable::from_json(&document_json)
511553 .map_err(|e| anyhow::anyhow!("Failed to parse DID document JSON: {}", e))?;
512554513513- let result = expr.search(&data)
555555+ let result = expr
556556+ .search(&data)
514557 .map_err(|e| anyhow::anyhow!("JMESPath query failed: {}", e))?;
515558516559 if result.is_null() {
···578621579622 eprintln!("📊 Document Comparison");
580623 eprintln!("═══════════════════════");
581581-624624+582625 // Construct the full URL that was fetched
583626 let full_url = format!("{}/{}", remote_url.trim_end_matches('/'), _did);
584627 eprintln!(" Remote URL: {}", full_url);
···655698656699 eprintln!();
657700 use super::utils::colors;
658658- eprintln!("Legend: {}Local{} {}Remote{}", colors::RED, colors::RESET, colors::GREEN, colors::RESET);
701701+ eprintln!(
702702+ "Legend: {}Local{} {}Remote{}",
703703+ colors::RED,
704704+ colors::RESET,
705705+ colors::GREEN,
706706+ colors::RESET
707707+ );
659708 eprintln!();
660709 }
661710···674723 // Parse JSON using sonic_rs (faster than serde_json)
675724 let value: sonic_rs::Value = sonic_rs::from_str(json)
676725 .map_err(|e| anyhow::anyhow!("Failed to parse JSON for normalization: {}", e))?;
677677-726726+678727 // Re-serialize with consistent formatting (compact, no whitespace)
679728 // This normalizes key ordering and whitespace differences
680729 let normalized = sonic_rs::to_string(&value)
681730 .map_err(|e| anyhow::anyhow!("Failed to serialize normalized JSON: {}", e))?;
682682-731731+683732 Ok(normalized)
684733}
685734···687736fn json_to_pretty(json: &str) -> Result<String> {
688737 let value: sonic_rs::Value = sonic_rs::from_str(json)
689738 .map_err(|e| anyhow::anyhow!("Failed to parse JSON for pretty printing: {}", e))?;
690690-739739+691740 let pretty = sonic_rs::to_string_pretty(&value)
692741 .map_err(|e| anyhow::anyhow!("Failed to serialize pretty JSON: {}", e))?;
693693-742742+694743 Ok(pretty)
695744}
696745···1036108510371086 // Calculate column widths for alignment
10381087 let mut column_widths = vec![0; fields.len()];
10391039-10881088+10401089 // Check header widths
10411090 for (i, field) in fields.iter().enumerate() {
10421091 let header = get_lookup_field_header(field);
10431092 column_widths[i] = column_widths[i].max(header.len());
10441093 }
10451045-10941094+10461095 // Check data widths
10471096 for owl in bundled_ops.iter() {
10481097 for (i, field) in fields.iter().enumerate() {
···10501099 column_widths[i] = column_widths[i].max(value.len());
10511100 }
10521101 }
10531053-11021102+10541103 for op in mempool_ops.iter() {
10551104 for (i, field) in fields.iter().enumerate() {
10561105 let value = get_lookup_field_value_mempool(op, field);
···1060110910611110 // Print column header (unless disabled)
10621111 if !no_header {
10631063- let headers: Vec<String> = fields.iter()
11121112+ let headers: Vec<String> = fields
11131113+ .iter()
10641114 .enumerate()
10651115 .map(|(i, f)| {
10661116 let header = get_lookup_field_header(f);
···10831133 };
1084113410851135 for owl in bundled_iter {
10861086- let values: Vec<String> = fields.iter()
11361136+ let values: Vec<String> = fields
11371137+ .iter()
10871138 .enumerate()
10881139 .map(|(i, f)| {
10891140 let value = get_lookup_field_value(owl, None, f);
···10961147 })
10971148 .collect();
10981149 println!("{}", values.join(separator));
10991099-11501150+11001151 if verbose && !owl.nullified {
11011152 let op_val = &owl.operation.operation;
11021153 if let Some(op_type) = op_val.get("type").and_then(|v| v.as_str()) {
···11051156 if let Some(handle) = op_val.get("handle").and_then(|v| v.as_str()) {
11061157 eprintln!(" handle: {}", handle);
11071158 } else if let Some(aka) = op_val.get("alsoKnownAs").and_then(|v| v.as_array())
11081108- && let Some(aka_str) = aka.first().and_then(|v| v.as_str()) {
11091109- let handle = aka_str.strip_prefix("at://").unwrap_or(aka_str);
11101110- eprintln!(" handle: {}", handle);
11591159+ && let Some(aka_str) = aka.first().and_then(|v| v.as_str())
11601160+ {
11611161+ let handle = aka_str.strip_prefix("at://").unwrap_or(aka_str);
11621162+ eprintln!(" handle: {}", handle);
11111163 }
11121164 }
11131165 }
···11201172 };
1121117311221174 for op in mempool_iter {
11231123- let values: Vec<String> = fields.iter()
11751175+ let values: Vec<String> = fields
11761176+ .iter()
11241177 .enumerate()
11251178 .map(|(i, f)| {
11261179 let value = get_lookup_field_value_mempool(op, f);
···11711224 "bundle" => format!("{}", owl.bundle),
11721225 "position" | "pos" => format!("{:04}", owl.position),
11731226 "global" | "global_pos" => {
11741174- let global_pos = plcbundle::constants::bundle_position_to_global(owl.bundle, owl.position);
12271227+ let global_pos =
12281228+ plcbundle::constants::bundle_position_to_global(owl.bundle, owl.position);
11751229 format!("{}", global_pos)
11761176- },
12301230+ }
11771231 "status" => {
11781232 if owl.nullified {
11791233 "✗".to_string()
···11811235 "✓".to_string()
11821236 }
11831237 }
11841184- "cid" => {
11851185- owl.operation.cid.clone()
11861186- .unwrap_or_default()
11871187- }
12381238+ "cid" => owl.operation.cid.clone().unwrap_or_default(),
11881239 "created_at" | "created" | "date" | "time" => owl.operation.created_at.clone(),
11891240 "nullified" => {
11901241 if owl.nullified {
···12171268 "position" | "pos" => "".to_string(),
12181269 "global" | "global_pos" => "".to_string(),
12191270 "status" => "✓".to_string(),
12201220- "cid" => {
12211221- op.cid.clone()
12221222- .unwrap_or_default()
12231223- }
12711271+ "cid" => op.cid.clone().unwrap_or_default(),
12241272 "created_at" | "created" | "date" | "time" => op.created_at.clone(),
12251273 "nullified" => "false".to_string(),
12261274 "date_short" => {
···12401288 _ => String::new(),
12411289 }
12421290}
12431243-1244129112451292// DID BATCH - Process multiple DIDs (TODO)
12461293···13941441 if verbose {
13951442 println!("📋 Operations:");
13961443 for (i, entry) in audit_log.iter().enumerate() {
13971397- let status = if entry.nullified { "❌ NULLIFIED" } else { "✅" };
13981398- println!(
13991399- " [{}] {} {} - {}",
14001400- i, status, entry.cid, entry.created_at
14011401- );
14441444+ let status = if entry.nullified {
14451445+ "❌ NULLIFIED"
14461446+ } else {
14471447+ "✅"
14481448+ };
14491449+ println!(" [{}] {} {} - {}", i, status, entry.cid, entry.created_at);
14021450 if entry.operation.is_genesis() {
14031451 println!(" Type: Genesis (creates the DID)");
14041452 } else {
···15761624 for (j, key) in rotation_keys.iter().enumerate() {
15771625 println!(" [{}] {}", j, key);
15781626 }
15791579- println!(" ⚠️ Genesis signature cannot be verified (bootstrapping trust)");
16271627+ println!(
16281628+ " ⚠️ Genesis signature cannot be verified (bootstrapping trust)"
16291629+ );
15801630 }
15811631 }
15821632 continue;
···15911641 // Validate signature using current rotation keys
15921642 if !current_rotation_keys.is_empty() {
15931643 if verbose {
15941594- println!(" Available rotation keys: {}", current_rotation_keys.len());
16441644+ println!(
16451645+ " Available rotation keys: {}",
16461646+ current_rotation_keys.len()
16471647+ );
15951648 for (j, key) in current_rotation_keys.iter().enumerate() {
15961649 println!(" [{}] {}", j, key);
15971650 }
···16261679 // Final attempt with all keys (for comprehensive error)
16271680 if let Err(e) = entry.operation.verify(&verifying_keys) {
16281681 eprintln!();
16291629- eprintln!(
16301630- "❌ Validation failed: Invalid signature at operation {}",
16311631- i
16321632- );
16821682+ eprintln!("❌ Validation failed: Invalid signature at operation {}", i);
16331683 eprintln!(" Error: {}", e);
16341684 eprintln!(" CID: {}", entry.cid);
16351685 eprintln!(
···1655170516561706 // Update rotation keys if this operation changes them
16571707 if let Some(new_rotation_keys) = entry.operation.rotation_keys()
16581658- && new_rotation_keys != current_rotation_keys {
16591659- if verbose {
16601660- println!(" 🔄 Rotation keys updated by this operation");
16611661- println!(" Old keys: {}", current_rotation_keys.len());
16621662- println!(" New keys: {}", new_rotation_keys.len());
16631663- for (j, key) in new_rotation_keys.iter().enumerate() {
16641664- println!(" [{}] {}", j, key);
16651665- }
17081708+ && new_rotation_keys != current_rotation_keys
17091709+ {
17101710+ if verbose {
17111711+ println!(" 🔄 Rotation keys updated by this operation");
17121712+ println!(" Old keys: {}", current_rotation_keys.len());
17131713+ println!(" New keys: {}", new_rotation_keys.len());
17141714+ for (j, key) in new_rotation_keys.iter().enumerate() {
17151715+ println!(" [{}] {}", j, key);
16661716 }
16671667- current_rotation_keys = new_rotation_keys.to_vec();
17171717+ }
17181718+ current_rotation_keys = new_rotation_keys.to_vec();
16681719 }
16691720 }
16701721···17201771 let mut prev_to_indices: HashMap<String, Vec<usize>> = HashMap::new();
17211772 for (i, entry) in audit_log.iter().enumerate() {
17221773 if let Some(prev) = entry.operation.prev() {
17231723- prev_to_indices
17241724- .entry(prev.to_string())
17251725- .or_default()
17261726- .push(i);
17741774+ prev_to_indices.entry(prev.to_string()).or_default().push(i);
17271775 }
17281776 }
17291777···18171865 .map_err(|e| anyhow::anyhow!("Failed to parse operation: {}", e))?;
1818186618191867 // Get CID from bundle operation (should always be present)
18201820- let cid = owl
18211821- .operation
18221822- .cid
18231823- .clone()
18241824- .unwrap_or_else(|| {
18251825- // Fallback: this shouldn't happen in real data, but provide a placeholder
18261826- format!("bundle_{}_pos_{}", owl.bundle, owl.position)
18271827- });
18681868+ let cid = owl.operation.cid.clone().unwrap_or_else(|| {
18691869+ // Fallback: this shouldn't happen in real data, but provide a placeholder
18701870+ format!("bundle_{}_pos_{}", owl.bundle, owl.position)
18711871+ });
1828187218291873 audit_log.push(AuditLogEntry {
18301874 did: owl.operation.did.clone(),
···18391883}
1840188418411885/// Visualize forks in the audit log
18421842-fn visualize_forks(
18431843- audit_log: &[AuditLogEntry],
18441844- did_str: &str,
18451845- verbose: bool,
18461846-) -> Result<()> {
18861886+fn visualize_forks(audit_log: &[AuditLogEntry], did_str: &str, verbose: bool) -> Result<()> {
18471887 println!("🔍 Analyzing forks in: {}", did_str);
18481888 println!(" Source: local bundles");
18491889 println!();
···20132053}
2014205420152055/// Find which rotation key signed an operation
20162016-fn find_signing_key(operation: &Operation, rotation_keys: &[String]) -> (Option<usize>, Option<String>) {
20562056+fn find_signing_key(
20572057+ operation: &Operation,
20582058+ rotation_keys: &[String],
20592059+) -> (Option<usize>, Option<String>) {
20172060 for (index, key_did) in rotation_keys.iter().enumerate() {
20182061 if let Ok(verifying_key) = VerifyingKey::from_did_key(key_did)
20192019- && operation.verify(&[verifying_key]).is_ok() {
20202020- return (Some(index), Some(key_did.clone()));
20622062+ && operation.verify(&[verifying_key]).is_ok()
20632063+ {
20642064+ return (Some(index), Some(key_did.clone()));
20212065 }
20222066 }
20232067 (None, None)
···2072211620732117 // Check if this operation is part of a fork
20742118 if let Some(_prev_cid) = prev
20752075- && let Some(fork) = fork_map.get(&entry.cid) {
20762076- // This is a fork point
20772077- if !processed_forks.contains(&fork.prev_cid) {
20782078- processed_forks.insert(fork.prev_cid.clone());
21192119+ && let Some(fork) = fork_map.get(&entry.cid)
21202120+ {
21212121+ // This is a fork point
21222122+ if !processed_forks.contains(&fork.prev_cid) {
21232123+ processed_forks.insert(fork.prev_cid.clone());
2079212420802080- println!("Fork at operation referencing {}", truncate_cid(&fork.prev_cid));
21252125+ println!(
21262126+ "Fork at operation referencing {}",
21272127+ truncate_cid(&fork.prev_cid)
21282128+ );
2081212920822082- for (j, fork_op) in fork.operations.iter().enumerate() {
20832083- let symbol = if fork_op.is_winner { "✓" } else { "✗" };
20842084- let color = if fork_op.is_winner { "🟢" } else { "🔴" };
20852085- let prefix = if j == fork.operations.len() - 1 {
20862086- "└─"
20872087- } else {
20882088- "├─"
20892089- };
21302130+ for (j, fork_op) in fork.operations.iter().enumerate() {
21312131+ let symbol = if fork_op.is_winner { "✓" } else { "✗" };
21322132+ let color = if fork_op.is_winner { "🟢" } else { "🔴" };
21332133+ let prefix = if j == fork.operations.len() - 1 {
21342134+ "└─"
21352135+ } else {
21362136+ "├─"
21372137+ };
2090213820912091- println!(
20922092- " {} {} {} CID: {}",
20932093- prefix,
20942094- color,
20952095- symbol,
20962096- truncate_cid(&fork_op.cid)
20972097- );
21392139+ println!(
21402140+ " {} {} {} CID: {}",
21412141+ prefix,
21422142+ color,
21432143+ symbol,
21442144+ truncate_cid(&fork_op.cid)
21452145+ );
2098214620992099- if let Some(key_idx) = fork_op.signing_key_index {
21002100- println!(" │ Signed by: rotation_key[{}]", key_idx);
21012101- if verbose
21022102- && let Some(key) = &fork_op.signing_key {
21032103- println!(" │ Key: {}", truncate_cid(key));
21042104- }
21472147+ if let Some(key_idx) = fork_op.signing_key_index {
21482148+ println!(" │ Signed by: rotation_key[{}]", key_idx);
21492149+ if verbose && let Some(key) = &fork_op.signing_key {
21502150+ println!(" │ Key: {}", truncate_cid(key));
21052151 }
21522152+ }
2106215321072107- println!(
21082108- " │ Timestamp: {}",
21092109- fork_op.timestamp.format("%Y-%m-%d %H:%M:%S UTC")
21102110- );
21542154+ println!(
21552155+ " │ Timestamp: {}",
21562156+ fork_op.timestamp.format("%Y-%m-%d %H:%M:%S UTC")
21572157+ );
2111215821122112- if !fork_op.is_winner {
21132113- if let Some(reason) = &fork_op.rejection_reason {
21142114- println!(" │ Reason: {}", reason);
21152115- }
21162116- } else {
21172117- println!(" │ Status: CANONICAL (winner)");
21592159+ if !fork_op.is_winner {
21602160+ if let Some(reason) = &fork_op.rejection_reason {
21612161+ println!(" │ Reason: {}", reason);
21182162 }
21632163+ } else {
21642164+ println!(" │ Status: CANONICAL (winner)");
21652165+ }
2119216621202120- if j < fork.operations.len() - 1 {
21212121- println!(" │");
21222122- }
21672167+ if j < fork.operations.len() - 1 {
21682168+ println!(" │");
21232169 }
21242124- println!();
21252170 }
21262126- continue;
21712171+ println!();
21722172+ }
21732173+ continue;
21272174 }
2128217521292176 // Regular operation (not part of a fork)
···21312178 println!("🌱 Genesis");
21322179 println!(" CID: {}", truncate_cid(&entry.cid));
21332180 println!(" Timestamp: {}", entry.created_at);
21342134- if verbose
21352135- && let Operation::PlcOperation { rotation_keys, .. } = &entry.operation {
21362136- println!(" Rotation keys: {}", rotation_keys.len());
21812181+ if verbose && let Operation::PlcOperation { rotation_keys, .. } = &entry.operation {
21822182+ println!(" Rotation keys: {}", rotation_keys.len());
21372183 }
21382184 println!();
21392185 }
+70-44
src/cli/cmd_export.rs
···10101111use super::progress::ProgressBar;
1212use super::utils;
1313-use plcbundle::constants::{global_to_bundle_position, total_operations_from_bundles, bundle_position_to_global};
1313+use plcbundle::constants::{
1414+ bundle_position_to_global, global_to_bundle_position, total_operations_from_bundles,
1515+};
1416use plcbundle::format::format_std_duration_auto;
15171618#[derive(Args)]
···7476 pub reverse: bool,
7577}
76787777-7879pub fn run(cmd: ExportCommand, dir: PathBuf, quiet: bool, verbose: bool) -> Result<()> {
7980 let output = cmd.output;
8081 let count = cmd.count;
···132133 eprintln!("Exporting {} operations", utils::format_number(op_count));
133134 } else {
134135 let bundle_range_str = format_bundle_range(&bundle_numbers);
135135- eprintln!("Exporting bundles: {} ({})", bundle_range_str, bundle_numbers.len());
136136+ eprintln!(
137137+ "Exporting bundles: {} ({})",
138138+ bundle_range_str,
139139+ bundle_numbers.len()
140140+ );
136141 }
137142 }
138143···156161 let total_compressed_size: u64 = bundle_numbers
157162 .iter()
158163 .filter_map(|bundle_num| {
159159- index.get_bundle(*bundle_num).map(|meta| meta.compressed_size)
164164+ index
165165+ .get_bundle(*bundle_num)
166166+ .map(|meta| meta.compressed_size)
160167 })
161168 .sum();
162162-169169+163170 // Create progress bar tracking bundles processed
164171 // Skip progress bar if quiet mode or if only one bundle needs to be loaded
165172 let pb = if quiet || bundle_numbers.len() == 1 {
166173 None
167174 } else {
168168- Some(ProgressBar::with_bytes(bundle_numbers.len(), total_uncompressed_size))
175175+ Some(ProgressBar::with_bytes(
176176+ bundle_numbers.len(),
177177+ total_uncompressed_size,
178178+ ))
169179 };
170180171181 let start = Instant::now();
···179189 for bundle_num in bundle_numbers {
180190 // Check count limit
181191 if let Some(limit) = count
182182- && exported_count >= limit {
183183- break;
192192+ && exported_count >= limit
193193+ {
194194+ break;
184195 }
185196186197 // Use BundleManager API to get decompressed stream
···226237227238 // Check count limit
228239 if let Some(limit) = count
229229- && exported_count >= limit {
230230- break;
240240+ && exported_count >= limit
241241+ {
242242+ break;
231243 }
232244233245 output_buffer.push_str(&line);
···246258247259 // Progress update (operations count in message, but bundles in progress bar)
248260 if let Some(ref pb) = pb
249249- && (exported_count % BATCH_SIZE == 0 || exported_count == 1) {
250250- let bytes = bytes_written.lock().unwrap();
251251- let total_bytes = *bytes + output_buffer.len() as u64;
252252- drop(bytes);
253253- pb.set_with_bytes(bundles_processed, total_bytes);
254254- pb.set_message(format!("{} ops", utils::format_number(exported_count as u64)));
261261+ && (exported_count % BATCH_SIZE == 0 || exported_count == 1)
262262+ {
263263+ let bytes = bytes_written.lock().unwrap();
264264+ let total_bytes = *bytes + output_buffer.len() as u64;
265265+ drop(bytes);
266266+ pb.set_with_bytes(bundles_processed, total_bytes);
267267+ pb.set_message(format!(
268268+ "{} ops",
269269+ utils::format_number(exported_count as u64)
270270+ ));
255271 }
256272 }
257257-273273+258274 // Update progress after processing each bundle
259275 bundles_processed += 1;
260276 if let Some(ref pb) = pb {
···262278 let total_bytes = *bytes + output_buffer.len() as u64;
263279 drop(bytes);
264280 pb.set_with_bytes(bundles_processed, total_bytes);
265265- pb.set_message(format!("{} ops", utils::format_number(exported_count as u64)));
281281+ pb.set_message(format!(
282282+ "{} ops",
283283+ utils::format_number(exported_count as u64)
284284+ ));
266285 }
267286 }
268287···287306 if !quiet {
288307 let elapsed = start.elapsed();
289308 let elapsed_secs = elapsed.as_secs_f64();
290290-309309+291310 // Format duration with auto-scaling using utility function
292311 let duration_str = format_std_duration_auto(elapsed);
293293-312312+294313 let mut parts = Vec::new();
295295- parts.push(format!("Exported: {} ops", utils::format_number(exported_count as u64)));
314314+ parts.push(format!(
315315+ "Exported: {} ops",
316316+ utils::format_number(exported_count as u64)
317317+ ));
296318 parts.push(format!("in {}", duration_str));
297297-319319+298320 if elapsed_secs > 0.0 {
299321 // Calculate throughputs
300300- let uncompressed_throughput_mb = (total_uncompressed_size as f64 / elapsed_secs) / (1024.0 * 1024.0);
301301- let compressed_throughput_mb = (total_compressed_size as f64 / elapsed_secs) / (1024.0 * 1024.0);
302302-303303- parts.push(format!("({:.1} MB/s uncompressed, {:.1} MB/s compressed)",
304304- uncompressed_throughput_mb,
305305- compressed_throughput_mb
322322+ let uncompressed_throughput_mb =
323323+ (total_uncompressed_size as f64 / elapsed_secs) / (1024.0 * 1024.0);
324324+ let compressed_throughput_mb =
325325+ (total_compressed_size as f64 / elapsed_secs) / (1024.0 * 1024.0);
326326+327327+ parts.push(format!(
328328+ "({:.1} MB/s uncompressed, {:.1} MB/s compressed)",
329329+ uncompressed_throughput_mb, compressed_throughput_mb
306330 ));
307331 }
308308-332332+309333 eprintln!("{}", parts.join(" "));
310334 }
311335···355379 }
356380357381 let result = ranges.join(", ");
358358-382382+359383 // If the formatted string is too long, fall back to min-max
360384 if result.len() > 200 {
361385 let min = bundles.iter().min().copied().unwrap_or(0);
···380404 /// This is much more efficient for large ranges like "0-10000000"
381405 fn parse(spec: &str, max_operation: u64) -> Result<Self> {
382406 use anyhow::Context;
383383-407407+384408 if max_operation == 0 {
385409 anyhow::bail!("No operations available");
386410 }
387411388412 let mut ranges = Vec::new();
389413 let mut individual = HashSet::new();
390390-414414+391415 for part in spec.split(',') {
392416 let part = part.trim();
393417 if part.is_empty() {
···435459 individual.insert(num);
436460 }
437461 }
438438-462462+439463 // Sort ranges for efficient lookup
440464 ranges.sort_unstable();
441441-465465+442466 Ok(OperationFilter { ranges, individual })
443467 }
444444-468468+445469 /// Check if a global operation position is included in the filter
446470 fn contains(&self, global_pos: u64) -> bool {
447471 // Check individual operations first (O(1))
448472 if self.individual.contains(&global_pos) {
449473 return true;
450474 }
451451-475475+452476 // Check ranges (O(log n) with binary search, but we use linear for simplicity)
453477 // For small number of ranges, linear is fine
454478 for &(start, end) in &self.ranges {
···456480 return true;
457481 }
458482 }
459459-483483+460484 false
461485 }
462462-486486+463487 /// Get bundle numbers that contain operations in this filter
464488 /// This calculates bundles from range bounds without materializing all operations
465489 fn get_bundle_numbers(&self, max_bundle: u32) -> Vec<u32> {
466490 let mut bundle_set = HashSet::new();
467467-491491+468492 // Process ranges
469493 for &(start, end) in &self.ranges {
470494 // Convert start and end to bundle numbers
471495 let (start_bundle, _) = global_to_bundle_position(start);
472496 let (end_bundle, _) = global_to_bundle_position(end);
473473-497497+474498 // Add all bundles in the range
475499 for bundle in start_bundle..=end_bundle.min(max_bundle) {
476500 bundle_set.insert(bundle);
477501 }
478502 }
479479-503503+480504 // Process individual operations
481505 for &op in &self.individual {
482506 let (bundle, _) = global_to_bundle_position(op);
···484508 bundle_set.insert(bundle);
485509 }
486510 }
487487-511511+488512 let mut bundles: Vec<u32> = bundle_set.into_iter().collect();
489513 bundles.sort_unstable();
490514 bundles
491515 }
492492-516516+493517 /// Estimate the number of operations (for display purposes)
494518 fn estimated_count(&self) -> u64 {
495495- let range_count: u64 = self.ranges.iter()
519519+ let range_count: u64 = self
520520+ .ranges
521521+ .iter()
496522 .map(|&(start, end)| end.saturating_sub(start).saturating_add(1))
497523 .sum();
498524 let individual_count = self.individual.len() as u64;
+289-148
src/cli/cmd_index.rs
···150150151151pub fn run(cmd: IndexCommand, dir: PathBuf, global_verbose: bool) -> Result<()> {
152152 match cmd.command {
153153- IndexCommands::Build { force, threads, flush_interval } => {
153153+ IndexCommands::Build {
154154+ force,
155155+ threads,
156156+ flush_interval,
157157+ } => {
154158 cmd_index_build(dir, force, threads, flush_interval)?;
155159 }
156156- IndexCommands::Repair { threads, flush_interval } => {
160160+ IndexCommands::Repair {
161161+ threads,
162162+ flush_interval,
163163+ } => {
157164 cmd_index_repair(dir, threads, flush_interval)?;
158165 }
159166 IndexCommands::Status { json } => {
160167 cmd_index_stats(dir, json)?;
161168 }
162162- IndexCommands::Verify { flush_interval, full } => {
169169+ IndexCommands::Verify {
170170+ flush_interval,
171171+ full,
172172+ } => {
163173 cmd_index_verify(dir, global_verbose, flush_interval, full)?;
164174 }
165175 IndexCommands::Debug { shard, did, json } => {
···184194/// Parse shard number from string (supports hex 0xac or decimal)
185195fn parse_shard(s: &str) -> Result<u8> {
186196 if s.starts_with("0x") || s.starts_with("0X") {
187187- u8::from_str_radix(&s[2..], 16)
188188- .map_err(|_| anyhow::anyhow!("Invalid shard number: {}", s))
197197+ u8::from_str_radix(&s[2..], 16).map_err(|_| anyhow::anyhow!("Invalid shard number: {}", s))
189198 } else {
190199 s.parse::<u8>()
191200 .map_err(|_| anyhow::anyhow!("Invalid shard number: {}", s))
192201 }
193202}
194203195195-pub fn cmd_index_build(dir: PathBuf, force: bool, threads: usize, flush_interval: u32) -> Result<()> {
204204+pub fn cmd_index_build(
205205+ dir: PathBuf,
206206+ force: bool,
207207+ threads: usize,
208208+ flush_interval: u32,
209209+) -> Result<()> {
196210 // Set thread pool size
197211 let num_threads = super::utils::get_worker_threads(threads, 4);
198212 rayon::ThreadPoolBuilder::new()
···212226 .get("exists")
213227 .and_then(|v| v.as_bool())
214228 .unwrap_or(false);
215215-229229+216230 if index_exists && total_dids > 0 && !force {
217231 let last_bundle = manager.get_last_bundle();
218232 let index_last_bundle = did_index
···223237 .get("shards_with_data")
224238 .and_then(|v| v.as_i64())
225239 .unwrap_or(0);
226226-240240+227241 eprintln!("\n✅ DID Index Already Built");
228228- eprintln!(" Total DIDs: {}", utils::format_number(total_dids as u64));
242242+ eprintln!(
243243+ " Total DIDs: {}",
244244+ utils::format_number(total_dids as u64)
245245+ );
229246 eprintln!(" Last bundle: {} / {}", index_last_bundle, last_bundle);
230247 eprintln!(" Shards: {} with data", shards_with_data);
231231- eprintln!(" Location: {}/{}/", utils::display_path(&dir).display(), constants::DID_INDEX_DIR);
232232-248248+ eprintln!(
249249+ " Location: {}/{}/",
250250+ utils::display_path(&dir).display(),
251251+ constants::DID_INDEX_DIR
252252+ );
253253+233254 if index_last_bundle < last_bundle {
234255 let missing = last_bundle - index_last_bundle;
235256 eprintln!();
···239260 eprintln!();
240261 eprintln!(" ✓ Index is up-to-date");
241262 }
242242-263263+243264 eprintln!();
244265 eprintln!(" 💡 Use --force to rebuild from scratch");
245266 return Ok(());
···260281 use std::sync::Arc;
261282 let last_bundle = manager.get_last_bundle() as u32;
262283 let progress = TwoStageProgress::new(last_bundle, total_bytes);
263263-284284+264285 // Set up cleanup guard to ensure temp files are deleted on CTRL+C or panic
265286 // Use Arc to share manager for cleanup
266287 let manager_arc = Arc::new(manager);
267288 let manager_for_cleanup = manager_arc.clone();
268268-289289+269290 struct IndexBuildCleanup {
270291 manager: Arc<BundleManager>,
271292 }
272272-293293+273294 impl Drop for IndexBuildCleanup {
274295 fn drop(&mut self) {
275296 // Cleanup temp files on drop (CTRL+C, panic, or normal exit)
276297 let did_index = self.manager.get_did_index();
277298 if let Some(idx) = did_index.read().unwrap().as_ref()
278278- && let Err(e) = idx.cleanup_temp_files() {
279279- log::warn!("[Index Build] Failed to cleanup temp files: {}", e);
299299+ && let Err(e) = idx.cleanup_temp_files()
300300+ {
301301+ log::warn!("[Index Build] Failed to cleanup temp files: {}", e);
280302 }
281303 }
282304 }
283283-305305+284306 let _cleanup_guard = IndexBuildCleanup {
285307 manager: manager_for_cleanup.clone(),
286308 };
···290312 flush_interval,
291313 Some(progress.callback_for_build_did_index()),
292314 Some(num_threads),
293293- Some(progress.interrupted())
315315+ Some(progress.interrupted()),
294316 );
295317296318 // Handle build result - ensure cleanup happens on error
···302324 // Error occurred - explicitly cleanup temp files before returning
303325 let did_index = manager_arc.get_did_index();
304326 if let Some(idx) = did_index.read().unwrap().as_ref()
305305- && let Err(cleanup_err) = idx.cleanup_temp_files() {
306306- log::warn!("[Index Build] Failed to cleanup temp files after error: {}", cleanup_err);
327327+ && let Err(cleanup_err) = idx.cleanup_temp_files()
328328+ {
329329+ log::warn!(
330330+ "[Index Build] Failed to cleanup temp files after error: {}",
331331+ cleanup_err
332332+ );
307333 }
308334 return Err(e);
309335 }
···361387 // Check if repair is needed before setting up progress bar
362388 let did_index = manager.get_did_index();
363389 let idx = did_index.read().unwrap();
364364- let idx_ref = idx.as_ref().ok_or_else(|| anyhow::anyhow!("DID index not initialized"))?;
390390+ let idx_ref = idx
391391+ .as_ref()
392392+ .ok_or_else(|| anyhow::anyhow!("DID index not initialized"))?;
365393 let repair_info = idx_ref.get_repair_info(last_bundle)?;
366394 let needs_work = repair_info.needs_rebuild || repair_info.needs_compact;
367395 drop(idx);
368368-396396+369397 // Use BundleManager API for repair
370398 let start = Instant::now();
371371-399399+372400 // Set up progress callback only if work is needed
373401 use super::progress::TwoStageProgress;
374402 let progress = if needs_work {
···379407 } else {
380408 None
381409 };
382382-410410+383411 let repair_result = manager.repair_did_index(
384412 num_threads,
385413 flush_interval,
386414 progress.as_ref().map(|p| p.callback_for_build_did_index()),
387415 )?;
388388-416416+389417 if let Some(progress) = progress {
390418 progress.finish();
391419 }
···409437 use super::utils::colors;
410438 eprintln!();
411439 if repair_result.repaired || repair_result.compacted {
412412- eprintln!("{}✓{} Index repair completed successfully in {:?}", colors::GREEN, colors::RESET, elapsed);
440440+ eprintln!(
441441+ "{}✓{} Index repair completed successfully in {:?}",
442442+ colors::GREEN,
443443+ colors::RESET,
444444+ elapsed
445445+ );
413446 eprintln!();
414447 eprintln!("📊 Index Statistics:");
415415- eprintln!(" Total DIDs: {}", utils::format_number(total_dids as u64));
448448+ eprintln!(
449449+ " Total DIDs: {}",
450450+ utils::format_number(total_dids as u64)
451451+ );
416452 eprintln!(" Last bundle: {}", last_bundle);
417453 eprintln!(" Shards: {}", shard_count);
418454 eprintln!(" Delta segments: {}", final_delta_segments);
419419-455455+420456 // Show what was fixed
421457 eprintln!();
422458 eprintln!("🔧 Repairs performed:");
423459 if repair_result.repaired {
424460 eprintln!(" • Processed {} bundles", repair_result.bundles_processed);
425461 if repair_result.segments_rebuilt > 0 {
426426- eprintln!(" • Rebuilt {} delta segment(s)", repair_result.segments_rebuilt);
462462+ eprintln!(
463463+ " • Rebuilt {} delta segment(s)",
464464+ repair_result.segments_rebuilt
465465+ );
427466 }
428467 }
429468 if repair_result.compacted {
430469 eprintln!(" • Compacted delta segments");
431470 }
432432-471471+433472 // Show compaction recommendation if needed
434473 if (50..100).contains(&final_delta_segments) {
435474 eprintln!();
436436- eprintln!("💡 Tip: Consider running '{} index compact' to optimize performance", constants::BINARY_NAME);
475475+ eprintln!(
476476+ "💡 Tip: Consider running '{} index compact' to optimize performance",
477477+ constants::BINARY_NAME
478478+ );
437479 }
438480 } else {
439439- eprintln!("{}✓{} Index is up-to-date and optimized", colors::GREEN, colors::RESET);
481481+ eprintln!(
482482+ "{}✓{} Index is up-to-date and optimized",
483483+ colors::GREEN,
484484+ colors::RESET
485485+ );
440486 eprintln!(" Last bundle: {}", index_last_bundle);
441487 eprintln!(" Delta segments: {}", delta_segments);
442488 }
···449495450496 // Get raw stats from did_index
451497 let stats_map = manager.get_did_index_stats();
452452-498498+453499 // Check if index has been built
454500 let is_built = stats_map
455501 .get("exists")
···596642 Ok(())
597643}
598644599599-600600-pub fn cmd_index_verify(dir: PathBuf, verbose: bool, flush_interval: u32, full: bool) -> Result<()> {
645645+pub fn cmd_index_verify(
646646+ dir: PathBuf,
647647+ verbose: bool,
648648+ flush_interval: u32,
649649+ full: bool,
650650+) -> Result<()> {
601651 let manager = super::utils::create_manager(dir.clone(), false, false, false)?;
602652603653 let stats_map = manager.get_did_index_stats();
···631681 let bundle_numbers: Vec<u32> = (1..=last_bundle).collect();
632682 let total_bytes = index.total_uncompressed_size_for_bundles(&bundle_numbers);
633683 let progress = TwoStageProgress::new(last_bundle, total_bytes);
634634-684684+635685 // Print header info like build command
636686 eprintln!("\n📦 Building Temporary DID Index (for verification)");
637687 eprintln!(" Strategy: Streaming (memory-efficient)");
···639689 if flush_interval > 0 {
640690 if flush_interval == constants::DID_INDEX_FLUSH_INTERVAL {
641691 // Default value - show with tuning hint
642642- eprintln!(" Flush: Every {} bundles (tune with --flush-interval)", flush_interval);
692692+ eprintln!(
693693+ " Flush: Every {} bundles (tune with --flush-interval)",
694694+ flush_interval
695695+ );
643696 } else {
644697 // Non-default value - show with tuning hint
645645- eprintln!(" Flush: {} bundles (you can tune with --flush-interval)", flush_interval);
698698+ eprintln!(
699699+ " Flush: {} bundles (you can tune with --flush-interval)",
700700+ flush_interval
701701+ );
646702 }
647703 } else {
648704 eprintln!(" Flush: Only at end (maximum memory usage)");
649705 }
650706 eprintln!();
651707 eprintln!("📊 Stage 1: Processing bundles...");
652652-708708+653709 let result = manager.verify_did_index(
654710 verbose,
655711 flush_interval,
656712 full,
657713 Some(progress.callback_for_build_did_index()),
658714 )?;
659659-715715+660716 progress.finish();
661717 eprintln!("\n");
662718 result
663719 } else {
664720 // Standard verification: no progress bar
665665- manager.verify_did_index::<fn(u32, u32, u64, u64)>(
666666- verbose,
667667- flush_interval,
668668- full,
669669- None,
670670- )?
721721+ manager.verify_did_index::<fn(u32, u32, u64, u64)>(verbose, flush_interval, full, None)?
671722 };
672723673724 let errors = verify_result.errors;
···781832 // Summary
782833 if errors > 0 {
783834 use super::utils::colors;
784784- eprintln!("{}✗{} Index verification failed", colors::RED, colors::RESET);
835835+ eprintln!(
836836+ "{}✗{} Index verification failed",
837837+ colors::RED,
838838+ colors::RESET
839839+ );
785840 eprintln!(" Errors: {}", errors);
786841 eprintln!(" Warnings: {}", warnings);
787787-842842+788843 // Print error breakdown
789844 if missing_base_shards > 0 {
790845 eprintln!(" • Missing base shards: {}", missing_base_shards);
···792847 if missing_delta_segments > 0 {
793848 eprintln!(" • Missing delta segments: {}", missing_delta_segments);
794849 }
795795-850850+796851 // Print other error categories
797852 for (category, count) in &error_categories {
798853 eprintln!(" • {}: {}", category, count);
799854 }
800800-855855+801856 eprintln!("\n Run: {} index repair", constants::BINARY_NAME);
802857 std::process::exit(1);
803858 } else if warnings > 0 {
804859 use super::utils::colors;
805860 use super::utils::format_number;
806806- eprintln!("\x1b[33m⚠️{} Index verification passed with warnings", colors::RESET);
861861+ eprintln!(
862862+ "\x1b[33m⚠️{} Index verification passed with warnings",
863863+ colors::RESET
864864+ );
807865 eprintln!(" Warnings: {}", warnings);
808866 eprintln!(" Total DIDs: {}", format_number(total_dids as u64));
809867 eprintln!(" Last bundle: {}", format_number(last_bundle as u64));
···830888831889/// Get raw shard data as JSON
832890fn get_raw_shard_data_json(dir: &Path, shard_num: u8) -> Result<serde_json::Value> {
833833- use std::fs;
834891 use plcbundle::constants;
835892 use plcbundle::did_index::OpLocation;
836893 use serde_json::json;
894894+ use std::fs;
837895838896 const DID_IDENTIFIER_LEN: usize = 24;
839897···859917 let offset_table_start = 1056;
860918861919 let shard_filename = format!("{:02x}.idx", shard_num);
862862-920920+863921 // Parse entries
864922 let max_entries_to_show = 10;
865923 let entries_to_show = entry_count.min(max_entries_to_show);
···891949 current_offset += DID_IDENTIFIER_LEN;
892950893951 // Read location count
894894- let loc_count = u16::from_le_bytes([data[current_offset], data[current_offset + 1]]) as usize;
952952+ let loc_count =
953953+ u16::from_le_bytes([data[current_offset], data[current_offset + 1]]) as usize;
895954 current_offset += 2;
896955897956 // Read locations
···934993}
935994936995/// Get raw delta segment data as JSON
937937-fn get_raw_segment_data_json(dir: &Path, shard_num: u8, file_name: &str) -> Result<serde_json::Value> {
938938- use std::fs;
996996+fn get_raw_segment_data_json(
997997+ dir: &Path,
998998+ shard_num: u8,
999999+ file_name: &str,
10001000+) -> Result<serde_json::Value> {
9391001 use plcbundle::constants;
9401002 use plcbundle::did_index::OpLocation;
9411003 use serde_json::json;
10041004+ use std::fs;
94210059431006 const DID_IDENTIFIER_LEN: usize = 24;
9441007···9951058 current_offset += DID_IDENTIFIER_LEN;
99610599971060 // Read location count
998998- let loc_count = u16::from_le_bytes([data[current_offset], data[current_offset + 1]]) as usize;
10611061+ let loc_count =
10621062+ u16::from_le_bytes([data[current_offset], data[current_offset + 1]]) as usize;
9991063 current_offset += 2;
1000106410011065 // Read locations
···1039110310401104/// Display raw shard data in a readable format
10411105fn display_raw_shard_data(dir: &Path, shard_num: u8) -> Result<()> {
10421042- use std::fs;
10431106 use plcbundle::constants;
10441107 use plcbundle::did_index::OpLocation;
11081108+ use std::fs;
1045110910461110 const DID_IDENTIFIER_LEN: usize = 24;
10471111···11041168 current_offset += DID_IDENTIFIER_LEN;
1105116911061170 // Read location count
11071107- let loc_count = u16::from_le_bytes([data[current_offset], data[current_offset + 1]]) as usize;
11711171+ let loc_count =
11721172+ u16::from_le_bytes([data[current_offset], data[current_offset + 1]]) as usize;
11081173 current_offset += 2;
1109117411101175 // Read locations
···11241189 }
1125119011261191 println!(" Entry {}:", i);
11271127-11921192+11281193 let identifier_clean = identifier.trim_end_matches('\0');
11291194 let full_did = format!("did:plc:{}", identifier_clean);
11301130- println!(" Identifier: {} [did={}]", identifier_clean, full_did);
11311131-11951195+ println!(
11961196+ " Identifier: {} [did={}]",
11971197+ identifier_clean, full_did
11981198+ );
11991199+11321200 print!(" Locations ({}): [ ", loc_count);
11331201 for (idx, loc) in locations.iter().enumerate() {
11341202 if idx > 0 {
···11431211 }
1144121211451213 if entry_count > max_entries_to_show {
11461146- println!("\n ... ({} more entries)", entry_count - max_entries_to_show);
12141214+ println!(
12151215+ "\n ... ({} more entries)",
12161216+ entry_count - max_entries_to_show
12171217+ );
11471218 }
11481219 } else {
11491220 println!(" No entries in shard");
···1154122511551226/// Display raw delta segment data in a readable format
11561227fn display_raw_segment_data(dir: &Path, shard_num: u8, file_name: &str) -> Result<()> {
11571157- use std::fs;
11581158- use plcbundle::constants;
11591228 use crate::did_index::OpLocation;
12291229+ use plcbundle::constants;
12301230+ use std::fs;
1160123111611232 const DID_IDENTIFIER_LEN: usize = 24;
11621233···12181289 current_offset += DID_IDENTIFIER_LEN;
1219129012201291 // Read location count
12211221- let loc_count = u16::from_le_bytes([data[current_offset], data[current_offset + 1]]) as usize;
12921292+ let loc_count =
12931293+ u16::from_le_bytes([data[current_offset], data[current_offset + 1]]) as usize;
12221294 current_offset += 2;
1223129512241296 // Read locations
···12381310 }
1239131112401312 println!(" Entry {}:", i);
12411241-13131313+12421314 let identifier_clean = identifier.trim_end_matches('\0');
12431315 let full_did = format!("did:plc:{}", identifier_clean);
12441244- println!(" Identifier: {} [did={}]", identifier_clean, full_did);
12451245-13161316+ println!(
13171317+ " Identifier: {} [did={}]",
13181318+ identifier_clean, full_did
13191319+ );
13201320+12461321 print!(" Locations ({}): [ ", loc_count);
12471322 for (idx, loc) in locations.iter().enumerate() {
12481323 if idx > 0 {
···12571332 }
1258133312591334 if entry_count > max_entries_to_show {
12601260- println!("\n ... ({} more entries)", entry_count - max_entries_to_show);
13351335+ println!(
13361336+ "\n ... ({} more entries)",
13371337+ entry_count - max_entries_to_show
13381338+ );
12611339 }
12621340 } else {
12631341 println!(" No entries in segment");
···1284136212851363 // Resolve handle to DID if needed
12861364 let (did, handle_resolve_time) = manager.resolve_handle_or_did(&input)?;
12871287-13651365+12881366 let is_handle = did != input;
12891367 if is_handle {
12901368 log::info!("Resolved handle: {} → {}", input, did);
···12971375 .and_then(|v| v.as_bool())
12981376 .unwrap_or(false)
12991377 {
13001300- anyhow::bail!("DID index does not exist. Run: {} index build", constants::BINARY_NAME);
13781378+ anyhow::bail!(
13791379+ "DID index does not exist. Run: {} index build",
13801380+ constants::BINARY_NAME
13811381+ );
13011382 }
1302138313031384 // Ensure the index is loaded
13041385 manager.ensure_did_index()?;
13051305-13861386+13061387 // Get DID index and lookup DID with stats
13071388 let did_index = manager.get_did_index();
13081389 let lookup_start = Instant::now();
···1392147313931474 println!(" Locations ({}):", locations.len());
13941475 println!(" ───────────────────────────────────────");
13951395-14761476+13961477 // Group locations for more compact display
13971478 let mut current_line = String::new();
13981479 for (idx, loc) in locations.iter().enumerate() {
···14001481 println!(" {}", current_line);
14011482 current_line.clear();
14021483 }
14031403-14841484+14041485 if !current_line.is_empty() {
14051486 current_line.push_str(", ");
14061487 }
14071407-14881488+14081489 let mut loc_str = format!("{}", loc.global_position());
14091490 if loc.nullified() {
14101491 loc_str.push_str(" (nullified)");
14111492 }
14121493 current_line.push_str(&loc_str);
14131494 }
14141414-14951495+14151496 if !current_line.is_empty() {
14161497 println!(" {}", current_line);
14171498 }
···1419150014201501 println!(" Lookup Statistics:");
14211502 println!(" ───────────────────────────────────────");
14221422- println!(" Shard size: {} bytes", utils::format_bytes(shard_stats.shard_size as u64));
14231423- println!(" Total entries: {}", utils::format_number(shard_stats.total_entries as u64));
14241424- println!(" Binary search attempts: {}", shard_stats.binary_search_attempts);
14251425- println!(" Prefix narrowed to: {}", shard_stats.prefix_narrowed_to);
14261426- println!(" Locations found: {}", shard_stats.locations_found);
15031503+ println!(
15041504+ " Shard size: {} bytes",
15051505+ utils::format_bytes(shard_stats.shard_size as u64)
15061506+ );
15071507+ println!(
15081508+ " Total entries: {}",
15091509+ utils::format_number(shard_stats.total_entries as u64)
15101510+ );
15111511+ println!(
15121512+ " Binary search attempts: {}",
15131513+ shard_stats.binary_search_attempts
15141514+ );
15151515+ println!(
15161516+ " Prefix narrowed to: {}",
15171517+ shard_stats.prefix_narrowed_to
15181518+ );
15191519+ println!(
15201520+ " Locations found: {}",
15211521+ shard_stats.locations_found
15221522+ );
14271523 println!();
1428152414291525 println!(" Timing:");
14301526 println!(" ───────────────────────────────────────");
14311431- println!(" Extract identifier: {:.3}ms", lookup_timings.extract_identifier.as_secs_f64() * 1000.0);
14321432- println!(" Calculate shard: {:.3}ms", lookup_timings.calculate_shard.as_secs_f64() * 1000.0);
14331433- println!(" Load shard: {:.3}ms ({})",
15271527+ println!(
15281528+ " Extract identifier: {:.3}ms",
15291529+ lookup_timings.extract_identifier.as_secs_f64() * 1000.0
15301530+ );
15311531+ println!(
15321532+ " Calculate shard: {:.3}ms",
15331533+ lookup_timings.calculate_shard.as_secs_f64() * 1000.0
15341534+ );
15351535+ println!(
15361536+ " Load shard: {:.3}ms ({})",
14341537 lookup_timings.load_shard.as_secs_f64() * 1000.0,
14351435- if lookup_timings.cache_hit { "cache hit" } else { "cache miss" });
14361436- println!(" Search: {:.3}ms", lookup_timings.search.as_secs_f64() * 1000.0);
15381538+ if lookup_timings.cache_hit {
15391539+ "cache hit"
15401540+ } else {
15411541+ "cache miss"
15421542+ }
15431543+ );
15441544+ println!(
15451545+ " Search: {:.3}ms",
15461546+ lookup_timings.search.as_secs_f64() * 1000.0
15471547+ );
14371548 if let Some(base_time) = lookup_timings.base_search_time {
14381438- println!(" Base search: {:.3}ms", base_time.as_secs_f64() * 1000.0);
15491549+ println!(
15501550+ " Base search: {:.3}ms",
15511551+ base_time.as_secs_f64() * 1000.0
15521552+ );
14391553 }
14401554 if !lookup_timings.delta_segment_times.is_empty() {
14411441- println!(" Delta segments: {} segments", lookup_timings.delta_segment_times.len());
15551555+ println!(
15561556+ " Delta segments: {} segments",
15571557+ lookup_timings.delta_segment_times.len()
15581558+ );
14421559 for (idx, (name, time)) in lookup_timings.delta_segment_times.iter().enumerate() {
14431443- println!(" Segment {} ({:20}): {:.3}ms", idx + 1, name, time.as_secs_f64() * 1000.0);
15601560+ println!(
15611561+ " Segment {} ({:20}): {:.3}ms",
15621562+ idx + 1,
15631563+ name,
15641564+ time.as_secs_f64() * 1000.0
15651565+ );
14441566 }
14451567 }
14461446- println!(" Merge: {:.3}ms", lookup_timings.merge_time.as_secs_f64() * 1000.0);
15681568+ println!(
15691569+ " Merge: {:.3}ms",
15701570+ lookup_timings.merge_time.as_secs_f64() * 1000.0
15711571+ );
14471572 if handle_resolve_time > 0 {
14481573 println!(" Handle resolve: {}ms", handle_resolve_time);
14491574 }
14501450- println!(" Total: {:.3}ms", lookup_elapsed.as_secs_f64() * 1000.0);
15751575+ println!(
15761576+ " Total: {:.3}ms",
15771577+ lookup_elapsed.as_secs_f64() * 1000.0
15781578+ );
14511579 println!();
1452158014531581 Ok(())
14541582}
1455158314561456-pub fn cmd_index_debug(dir: PathBuf, shard: Option<u8>, did: Option<String>, json: bool) -> Result<()> {
15841584+pub fn cmd_index_debug(
15851585+ dir: PathBuf,
15861586+ shard: Option<u8>,
15871587+ did: Option<String>,
15881588+ json: bool,
15891589+) -> Result<()> {
14571590 let manager = super::utils::create_manager(dir.clone(), false, false, false)?;
1458159114591592 let stats_map = manager.get_did_index_stats();
···14741607 }
1475160814761609 let did_index = manager.get_did_index();
14771477- let mut shard_details = did_index.read().unwrap().as_ref().unwrap().get_shard_details(shard)?;
16101610+ let mut shard_details = did_index
16111611+ .read()
16121612+ .unwrap()
16131613+ .as_ref()
16141614+ .unwrap()
16151615+ .get_shard_details(shard)?;
1478161614791617 if json {
14801618 // Add raw data to JSON output if a specific shard is requested
14811619 if let Some(shard_num) = shard
14821482- && let Some(detail) = shard_details.first_mut() {
14831483- let base_exists = detail
14841484- .get("base_exists")
14851485- .and_then(|v| v.as_bool())
14861486- .unwrap_or(false);
14871487-14881488- if base_exists
14891489- && let Ok(raw_data) = get_raw_shard_data_json(&dir, shard_num) {
14901490- detail.insert("raw_data".to_string(), raw_data);
14911491- }
14921492-14931493- // Add raw data for delta segments
14941494- if let Some(segments) = detail.get_mut("segments").and_then(|v| v.as_array_mut()) {
14951495- for seg in segments {
14961496- let file_name = seg.get("file_name").and_then(|v| v.as_str()).unwrap_or("");
14971497- let exists = seg.get("exists").and_then(|v| v.as_bool()).unwrap_or(false);
14981498- if exists && !file_name.is_empty()
14991499- && let Ok(raw_data) = get_raw_segment_data_json(&dir, shard_num, file_name) {
15001500- seg.as_object_mut().unwrap().insert("raw_data".to_string(), raw_data);
15011501- }
16201620+ && let Some(detail) = shard_details.first_mut()
16211621+ {
16221622+ let base_exists = detail
16231623+ .get("base_exists")
16241624+ .and_then(|v| v.as_bool())
16251625+ .unwrap_or(false);
16261626+16271627+ if base_exists && let Ok(raw_data) = get_raw_shard_data_json(&dir, shard_num) {
16281628+ detail.insert("raw_data".to_string(), raw_data);
16291629+ }
16301630+16311631+ // Add raw data for delta segments
16321632+ if let Some(segments) = detail.get_mut("segments").and_then(|v| v.as_array_mut()) {
16331633+ for seg in segments {
16341634+ let file_name = seg.get("file_name").and_then(|v| v.as_str()).unwrap_or("");
16351635+ let exists = seg.get("exists").and_then(|v| v.as_bool()).unwrap_or(false);
16361636+ if exists
16371637+ && !file_name.is_empty()
16381638+ && let Ok(raw_data) = get_raw_segment_data_json(&dir, shard_num, file_name)
16391639+ {
16401640+ seg.as_object_mut()
16411641+ .unwrap()
16421642+ .insert("raw_data".to_string(), raw_data);
15021643 }
15031644 }
16451645+ }
15041646 }
15051505-16471647+15061648 let json_str = serde_json::to_string_pretty(&shard_details)?;
15071649 println!("{}", json_str);
15081650 return Ok(());
···15581700 println!(" Next segment ID: {}", next_segment_id);
1559170115601702 if let Some(segments) = detail.get("segments").and_then(|v| v.as_array())
15611561- && !segments.is_empty() {
15621562- println!("\n Delta Segments:");
15631563- println!(" ───────────────────────────────────────");
15641564- for (idx, seg) in segments.iter().enumerate() {
15651565- let file_name =
15661566- seg.get("file_name").and_then(|v| v.as_str()).unwrap_or("?");
15671567- let exists = seg.get("exists").and_then(|v| v.as_bool()).unwrap_or(false);
15681568- let size = seg.get("size_bytes").and_then(|v| v.as_u64()).unwrap_or(0);
15691569- let did_count = seg.get("did_count").and_then(|v| v.as_u64()).unwrap_or(0);
15701570- let bundle_start = seg
15711571- .get("bundle_start")
15721572- .and_then(|v| v.as_u64())
15731573- .unwrap_or(0);
15741574- let bundle_end =
15751575- seg.get("bundle_end").and_then(|v| v.as_u64()).unwrap_or(0);
17031703+ && !segments.is_empty()
17041704+ {
17051705+ println!("\n Delta Segments:");
17061706+ println!(" ───────────────────────────────────────");
17071707+ for (idx, seg) in segments.iter().enumerate() {
17081708+ let file_name = seg.get("file_name").and_then(|v| v.as_str()).unwrap_or("?");
17091709+ let exists = seg.get("exists").and_then(|v| v.as_bool()).unwrap_or(false);
17101710+ let size = seg.get("size_bytes").and_then(|v| v.as_u64()).unwrap_or(0);
17111711+ let did_count = seg.get("did_count").and_then(|v| v.as_u64()).unwrap_or(0);
17121712+ let bundle_start = seg
17131713+ .get("bundle_start")
17141714+ .and_then(|v| v.as_u64())
17151715+ .unwrap_or(0);
17161716+ let bundle_end = seg.get("bundle_end").and_then(|v| v.as_u64()).unwrap_or(0);
1576171715771577- println!(
15781578- " [{:2}] {} {} ({})",
15791579- idx + 1,
15801580- if exists { "✓" } else { "✗" },
15811581- file_name,
15821582- utils::format_bytes(size)
15831583- );
15841584- println!(
15851585- " Bundles: {}-{}, DIDs: {}, Locations: {}",
15861586- bundle_start,
15871587- bundle_end,
15881588- utils::format_number(did_count),
15891589- seg.get("location_count")
15901590- .and_then(|v| v.as_u64())
15911591- .unwrap_or(0)
15921592- );
15931593- }
17181718+ println!(
17191719+ " [{:2}] {} {} ({})",
17201720+ idx + 1,
17211721+ if exists { "✓" } else { "✗" },
17221722+ file_name,
17231723+ utils::format_bytes(size)
17241724+ );
17251725+ println!(
17261726+ " Bundles: {}-{}, DIDs: {}, Locations: {}",
17271727+ bundle_start,
17281728+ bundle_end,
17291729+ utils::format_number(did_count),
17301730+ seg.get("location_count")
17311731+ .and_then(|v| v.as_u64())
17321732+ .unwrap_or(0)
17331733+ );
17341734+ }
15941735 }
1595173615961737 // Show raw shard data
+5-2
src/cli/cmd_init.rs
···11use anyhow::Result;
22use clap::{Args, ValueHint};
33-use plcbundle::{constants, BundleManager};
33+use plcbundle::{BundleManager, constants};
44use std::path::{Path, PathBuf};
5566#[derive(Args)]
···218218 origin: None,
219219 force: false,
220220 };
221221- assert!(run(cmd).is_err(), "Should fail when trying to initialize already-initialized repository without --force");
221221+ assert!(
222222+ run(cmd).is_err(),
223223+ "Should fail when trying to initialize already-initialized repository without --force"
224224+ );
222225223226 // Verify the origin is still "first" (not overwritten)
224227 let index = Index::load(temp.path()).unwrap();
+111-106
src/cli/cmd_inspect.rs
···201201 // For arbitrary file paths, we still need filesystem access - this should be refactored
202202 // to use a manager method for loading from arbitrary paths in the future if supported.
203203 // For now, it will return an error as per `resolve_bundle_target`.
204204- anyhow::bail!("Loading from arbitrary paths not yet implemented. Please specify a bundle number.");
204204+ anyhow::bail!(
205205+ "Loading from arbitrary paths not yet implemented. Please specify a bundle number."
206206+ );
205207 };
206208207209 if !cmd.json {
···380382 if let Some(aka) = op_val.get("alsoKnownAs").and_then(|v| v.as_array()) {
381383 for item in aka.iter() {
382384 if let Some(aka_str) = item.as_str()
383383- && aka_str.starts_with("at://") {
384384- total_handles += 1;
385385+ && aka_str.starts_with("at://")
386386+ {
387387+ total_handles += 1;
385388386386- // Extract domain
387387- let handle = aka_str.strip_prefix("at://").unwrap_or("");
388388- let handle = handle.split('/').next().unwrap_or("");
389389+ // Extract domain
390390+ let handle = aka_str.strip_prefix("at://").unwrap_or("");
391391+ let handle = handle.split('/').next().unwrap_or("");
389392390390- // Count domain (TLD)
391391- let parts: Vec<&str> = handle.split('.').collect();
392392- if parts.len() >= 2 {
393393- let domain = format!(
394394- "{}.{}",
395395- parts[parts.len() - 2],
396396- parts[parts.len() - 1]
397397- );
398398- *domain_counts.entry(domain).or_insert(0) += 1;
399399- }
393393+ // Count domain (TLD)
394394+ let parts: Vec<&str> = handle.split('.').collect();
395395+ if parts.len() >= 2 {
396396+ let domain =
397397+ format!("{}.{}", parts[parts.len() - 2], parts[parts.len() - 1]);
398398+ *domain_counts.entry(domain).or_insert(0) += 1;
399399+ }
400400401401- // Check for invalid patterns
402402- if handle.contains('_') {
403403- invalid_handles += 1;
404404- }
401401+ // Check for invalid patterns
402402+ if handle.contains('_') {
403403+ invalid_handles += 1;
404404+ }
405405 }
406406 }
407407 }
···413413 // Extract PDS endpoints
414414 if let Some(pds_val) = op_val.get("services").and_then(|v| v.get("atproto_pds"))
415415 && let Some(_pds) = pds_val.as_object()
416416- && let Some(endpoint) = pds_val.get("endpoint").and_then(|v| v.as_str()) {
417417- // Normalize endpoint
418418- let endpoint = endpoint
419419- .strip_prefix("https://")
420420- .or_else(|| endpoint.strip_prefix("http://"))
421421- .unwrap_or(endpoint);
422422- let endpoint = endpoint.split('/').next().unwrap_or(endpoint);
423423- *endpoint_counts.entry(endpoint.to_string()).or_insert(0) += 1;
416416+ && let Some(endpoint) = pds_val.get("endpoint").and_then(|v| v.as_str())
417417+ {
418418+ // Normalize endpoint
419419+ let endpoint = endpoint
420420+ .strip_prefix("https://")
421421+ .or_else(|| endpoint.strip_prefix("http://"))
422422+ .unwrap_or(endpoint);
423423+ let endpoint = endpoint.split('/').next().unwrap_or(endpoint);
424424+ *endpoint_counts.entry(endpoint.to_string()).or_insert(0) += 1;
424425 }
425426 }
426427 }
···663664 println!();
664665665666 // Embedded metadata (if available and not skipped)
666666- if !cmd.skip_metadata && result.has_metadata_frame
667667- && let Some(ref meta) = result.embedded_metadata {
668668- println!("📋 Embedded Metadata (Skippable Frame)");
669669- println!("───────────────────────────────────────");
670670- println!(" Format: {}", meta.format);
671671- println!(" Origin: {}", meta.origin);
672672- println!(" Bundle Number: {}", meta.bundle_number);
667667+ if !cmd.skip_metadata
668668+ && result.has_metadata_frame
669669+ && let Some(ref meta) = result.embedded_metadata
670670+ {
671671+ println!("📋 Embedded Metadata (Skippable Frame)");
672672+ println!("───────────────────────────────────────");
673673+ println!(" Format: {}", meta.format);
674674+ println!(" Origin: {}", meta.origin);
675675+ println!(" Bundle Number: {}", meta.bundle_number);
673676674674- if !meta.created_by.is_empty() {
675675- println!(" Created by: {}", meta.created_by);
676676- }
677677- println!(" Created at: {}", meta.created_at);
677677+ if !meta.created_by.is_empty() {
678678+ println!(" Created by: {}", meta.created_by);
679679+ }
680680+ println!(" Created at: {}", meta.created_at);
678681679679- println!("\n Content:");
680680- println!(
681681- " Operations: {}",
682682- format_number(meta.operation_count)
683683- );
684684- println!(" Unique DIDs: {}", format_number(meta.did_count));
685685- println!(
686686- " Frames: {} × {} ops",
687687- meta.frame_count,
688688- format_number(meta.frame_size)
689689- );
690690- println!(
691691- " Timespan: {} → {}",
692692- meta.start_time, meta.end_time
693693- );
682682+ println!("\n Content:");
683683+ println!(
684684+ " Operations: {}",
685685+ format_number(meta.operation_count)
686686+ );
687687+ println!(" Unique DIDs: {}", format_number(meta.did_count));
688688+ println!(
689689+ " Frames: {} × {} ops",
690690+ meta.frame_count,
691691+ format_number(meta.frame_size)
692692+ );
693693+ println!(
694694+ " Timespan: {} → {}",
695695+ meta.start_time, meta.end_time
696696+ );
697697+698698+ let duration = if let (Ok(start), Ok(end)) = (
699699+ DateTime::parse_from_rfc3339(&meta.start_time),
700700+ DateTime::parse_from_rfc3339(&meta.end_time),
701701+ ) {
702702+ end.signed_duration_since(start)
703703+ } else {
704704+ chrono::Duration::seconds(0)
705705+ };
706706+ println!(
707707+ " Duration: {}",
708708+ format_duration_verbose(duration)
709709+ );
694710695695- let duration = if let (Ok(start), Ok(end)) = (
696696- DateTime::parse_from_rfc3339(&meta.start_time),
697697- DateTime::parse_from_rfc3339(&meta.end_time),
698698- ) {
699699- end.signed_duration_since(start)
700700- } else {
701701- chrono::Duration::seconds(0)
702702- };
703703- println!(
704704- " Duration: {}",
705705- format_duration_verbose(duration)
706706- );
711711+ println!("\n Integrity:");
712712+ println!(" Content hash: {}", meta.content_hash);
713713+ if let Some(ref parent) = meta.parent_hash
714714+ && !parent.is_empty()
715715+ {
716716+ println!(" Parent hash: {}", parent);
717717+ }
707718708708- println!("\n Integrity:");
709709- println!(" Content hash: {}", meta.content_hash);
710710- if let Some(ref parent) = meta.parent_hash
711711- && !parent.is_empty() {
712712- println!(" Parent hash: {}", parent);
719719+ // Index metadata for chain info
720720+ if let Some(ref index_meta) = result.index_metadata {
721721+ println!("\n Chain:");
722722+ println!(" Chain hash: {}", index_meta.hash);
723723+ if !index_meta.parent.is_empty() {
724724+ println!(" Parent: {}", index_meta.parent);
713725 }
714714-715715- // Index metadata for chain info
716716- if let Some(ref index_meta) = result.index_metadata {
717717- println!("\n Chain:");
718718- println!(" Chain hash: {}", index_meta.hash);
719719- if !index_meta.parent.is_empty() {
720720- println!(" Parent: {}", index_meta.parent);
721721- }
722722- if !index_meta.cursor.is_empty() {
723723- println!(" Cursor: {}", index_meta.cursor);
724724- }
726726+ if !index_meta.cursor.is_empty() {
727727+ println!(" Cursor: {}", index_meta.cursor);
725728 }
729729+ }
726730727727- if !meta.frame_offsets.is_empty() {
728728- println!("\n Frame Index:");
729729- println!(" {} frame offsets (embedded)", meta.frame_offsets.len());
731731+ if !meta.frame_offsets.is_empty() {
732732+ println!("\n Frame Index:");
733733+ println!(" {} frame offsets (embedded)", meta.frame_offsets.len());
730734731731- if let Some(metadata_size) = meta.metadata_frame_size {
732732- println!(" Metadata size: {}", format_bytes(metadata_size));
733733- }
735735+ if let Some(metadata_size) = meta.metadata_frame_size {
736736+ println!(" Metadata size: {}", format_bytes(metadata_size));
737737+ }
734738735735- // Show compact list of first few offsets
736736- if meta.frame_offsets.len() <= 10 {
737737- println!(" Offsets: {:?}", meta.frame_offsets);
738738- } else {
739739- println!(
740740- " First offsets: {:?} ... ({} more)",
741741- &meta.frame_offsets[..5],
742742- meta.frame_offsets.len() - 5
743743- );
744744- }
739739+ // Show compact list of first few offsets
740740+ if meta.frame_offsets.len() <= 10 {
741741+ println!(" Offsets: {:?}", meta.frame_offsets);
742742+ } else {
743743+ println!(
744744+ " First offsets: {:?} ... ({} more)",
745745+ &meta.frame_offsets[..5],
746746+ meta.frame_offsets.len() - 5
747747+ );
745748 }
749749+ }
746750747747- println!();
748748- }
751751+ println!();
752752+ }
749753750754 // Operations breakdown
751755 println!("📊 Operations Analysis");
···812816 println!("────────────────────");
813817 println!(" Total handles: {}", format_number(total_handles));
814818 if let Some(invalid) = result.invalid_handles
815815- && invalid > 0 {
816816- println!(
817817- " Invalid patterns: {} ({:.1}%)",
818818- format_number(invalid),
819819- (invalid as f64 / total_handles as f64 * 100.0)
820820- );
819819+ && invalid > 0
820820+ {
821821+ println!(
822822+ " Invalid patterns: {} ({:.1}%)",
823823+ format_number(invalid),
824824+ (invalid as f64 / total_handles as f64 * 100.0)
825825+ );
821826 }
822827823828 if !result.top_domains.is_empty() {
···149149 pub errors: Option<Vec<String>>,
150150}
151151152152-153152#[derive(Debug, Clone)]
154153pub struct CleanPreview {
155154 pub files: Vec<CleanPreviewFile>,
···225224 pub fn new<O: IntoManagerOptions>(directory: PathBuf, options: O) -> Result<Self> {
226225 let options = options.into_options();
227226 let init_start = std::time::Instant::now();
228228- let display_dir = directory.canonicalize().unwrap_or_else(|_| directory.clone());
229229- log::debug!("[BundleManager] Initializing BundleManager from: {}", display_dir.display());
227227+ let display_dir = directory
228228+ .canonicalize()
229229+ .unwrap_or_else(|_| directory.clone());
230230+ log::debug!(
231231+ "[BundleManager] Initializing BundleManager from: {}",
232232+ display_dir.display()
233233+ );
230234 let index = Index::load(&directory)?;
231235232236 let handle_resolver = options
···257261 } else {
258262 let mempool_preload_time = mempool_preload_start.elapsed();
259263 let mempool_preload_ms = mempool_preload_time.as_secs_f64() * 1000.0;
260260- if let Ok(stats) = manager.get_mempool_stats() && stats.count > 0 {
264264+ if let Ok(stats) = manager.get_mempool_stats()
265265+ && stats.count > 0
266266+ {
261267 log::debug!(
262268 "[BundleManager] Pre-loaded mempool: {} operations for bundle {} ({:.3}ms)",
263269 stats.count,
···270276271277 let total_elapsed = init_start.elapsed();
272278 let total_elapsed_ms = total_elapsed.as_secs_f64() * 1000.0;
273273- log::debug!("[BundleManager] BundleManager initialized successfully ({:.3}ms total)", total_elapsed_ms);
279279+ log::debug!(
280280+ "[BundleManager] BundleManager initialized successfully ({:.3}ms total)",
281281+ total_elapsed_ms
282282+ );
274283 Ok(manager)
275284 }
276285···283292 let did_index = did_index::Manager::new(self.directory.clone())?;
284293 let did_index_elapsed = did_index_start.elapsed();
285294 let did_index_elapsed_ms = did_index_elapsed.as_secs_f64() * 1000.0;
286286- log::debug!("[BundleManager] DID index loaded ({:.3}ms)", did_index_elapsed_ms);
295295+ log::debug!(
296296+ "[BundleManager] DID index loaded ({:.3}ms)",
297297+ did_index_elapsed_ms
298298+ );
287299 *did_index_guard = Some(did_index);
288300 }
289301 Ok(())
···430442 /// Use `get_operation_raw()` if you only need the JSON.
431443 pub fn get_operation(&self, bundle_num: u32, position: usize) -> Result<Operation> {
432444 let json = self.get_operation_raw(bundle_num, position)?;
433433- let op = Operation::from_json(&json)
434434- .with_context(|| format!("Failed to parse operation JSON (bundle {}, position {})", bundle_num, position))?;
445445+ let op = Operation::from_json(&json).with_context(|| {
446446+ format!(
447447+ "Failed to parse operation JSON (bundle {}, position {})",
448448+ bundle_num, position
449449+ )
450450+ })?;
435451 Ok(op)
436452 }
437453···501517502518 // === DID Operations ===
503519 /// Get all operations for a DID from both bundles and mempool
504504- ///
520520+ ///
505521 /// # Arguments
506522 /// * `did` - The DID to look up
507523 /// * `include_locations` - If true, also return operations with location information
···512528 include_locations: bool,
513529 include_stats: bool,
514530 ) -> Result<DIDOperationsResult> {
515515- use std::time::Instant;
516531 use chrono::DateTime;
532532+ use std::time::Instant;
517533518534 self.ensure_did_index()?;
519535520536 let index_start = Instant::now();
521537 let (locations, shard_stats, shard_num, lookup_timings) = if include_stats {
522538 let did_index = self.did_index.read().unwrap();
523523- did_index.as_ref().unwrap().get_did_locations_with_stats(did)?
539539+ did_index
540540+ .as_ref()
541541+ .unwrap()
542542+ .get_did_locations_with_stats(did)?
524543 } else {
525544 let did_index = self.did_index.read().unwrap();
526545 let locs = did_index.as_ref().unwrap().get_did_locations(did)?;
527527- (locs, did_index::DIDLookupStats::default(), 0, did_index::DIDLookupTimings::default())
546546+ (
547547+ locs,
548548+ did_index::DIDLookupStats::default(),
549549+ 0,
550550+ did_index::DIDLookupTimings::default(),
551551+ )
528552 };
529553 let _index_time = index_start.elapsed();
530554531555 // Get operations from bundles
532556 let (bundle_ops_with_loc, load_time) = self.collect_operations_for_locations(&locations)?;
533533- let mut bundle_operations: Vec<Operation> = bundle_ops_with_loc.iter().map(|owl| owl.operation.clone()).collect();
557557+ let mut bundle_operations: Vec<Operation> = bundle_ops_with_loc
558558+ .iter()
559559+ .map(|owl| owl.operation.clone())
560560+ .collect();
534561535562 // Get operations from mempool (only once)
536563 let (mempool_ops, _mempool_load_time) = self.get_did_operations_from_mempool(did)?;
···563590564591 // Sort all operations by timestamp
565592 ops_with_loc.sort_by(|a, b| {
566566- let time_a = DateTime::parse_from_rfc3339(&a.operation.created_at)
567567- .unwrap_or_else(|_| DateTime::parse_from_rfc3339("1970-01-01T00:00:00Z").unwrap());
568568- let time_b = DateTime::parse_from_rfc3339(&b.operation.created_at)
569569- .unwrap_or_else(|_| DateTime::parse_from_rfc3339("1970-01-01T00:00:00Z").unwrap());
593593+ let time_a =
594594+ DateTime::parse_from_rfc3339(&a.operation.created_at).unwrap_or_else(|_| {
595595+ DateTime::parse_from_rfc3339("1970-01-01T00:00:00Z").unwrap()
596596+ });
597597+ let time_b =
598598+ DateTime::parse_from_rfc3339(&b.operation.created_at).unwrap_or_else(|_| {
599599+ DateTime::parse_from_rfc3339("1970-01-01T00:00:00Z").unwrap()
600600+ });
570601 time_a.cmp(&time_b)
571602 });
572603···578609 Ok(DIDOperationsResult {
579610 operations: bundle_operations,
580611 operations_with_locations,
581581- stats: if include_stats { Some(shard_stats) } else { None },
612612+ stats: if include_stats {
613613+ Some(shard_stats)
614614+ } else {
615615+ None
616616+ },
582617 shard_num: if include_stats { Some(shard_num) } else { None },
583583- lookup_timings: if include_stats { Some(lookup_timings) } else { None },
618618+ lookup_timings: if include_stats {
619619+ Some(lookup_timings)
620620+ } else {
621621+ None
622622+ },
584623 load_time: if include_stats { Some(load_time) } else { None },
585624 })
586625 }
587587-588626589627 /// Sample random DIDs directly from the DID index without reading bundles.
590628 pub fn sample_random_dids(&self, count: usize, seed: Option<u64>) -> Result<Vec<String>> {
···596634 /// Get DID operations from mempool (internal helper)
597635 /// Mempool should be preloaded at initialization, so this is just a fast in-memory lookup
598636 /// Returns (operations, load_time) where load_time is always ZERO (no lazy loading)
599599- fn get_did_operations_from_mempool(&self, did: &str) -> Result<(Vec<Operation>, std::time::Duration)> {
637637+ fn get_did_operations_from_mempool(
638638+ &self,
639639+ did: &str,
640640+ ) -> Result<(Vec<Operation>, std::time::Duration)> {
600641 use std::time::Instant;
601601-642642+602643 let mempool_start = Instant::now();
603603-644644+604645 // Mempool should be preloaded at initialization (no lazy loading)
605646 let mempool_guard = self.mempool.read().unwrap();
606647 match mempool_guard.as_ref() {
···628669 }
629670 }
630671631631- fn get_latest_did_operation_from_mempool(&self, did: &str) -> Result<(Option<(Operation, usize)>, std::time::Duration)> {
672672+ fn get_latest_did_operation_from_mempool(
673673+ &self,
674674+ did: &str,
675675+ ) -> Result<(Option<(Operation, usize)>, std::time::Duration)> {
632676 use std::time::Instant;
633633-677677+634678 let mempool_start = Instant::now();
635635-679679+636680 // Mempool should be preloaded at initialization (no lazy loading)
637681 let mempool_guard = self.mempool.read().unwrap();
638682 let result = match mempool_guard.as_ref() {
···645689 None
646690 }
647691 };
648648-692692+649693 let mempool_elapsed = mempool_start.elapsed();
650694 log::debug!(
651695 "[Mempool] Latest operation lookup for DID {} in {:?}",
652696 did,
653697 mempool_elapsed
654698 );
655655-699699+656700 Ok((result, std::time::Duration::ZERO))
657701 }
658702···660704 /// Returns the latest non-nullified DID document.
661705 /// If mempool has operations, uses the latest from mempool and skips bundle/index lookup.
662706 pub fn resolve_did(&self, did: &str) -> Result<ResolveResult> {
663663- use std::time::Instant;
664707 use chrono::DateTime;
708708+ use std::time::Instant;
665709666710 let total_start = Instant::now();
667711···671715 // Check mempool first (most recent operations)
672716 log::debug!("[Resolve] Checking mempool first for DID: {}", did);
673717 let mempool_start = Instant::now();
674674- let (latest_mempool_op, mempool_load_time) = self.get_latest_did_operation_from_mempool(did)?;
718718+ let (latest_mempool_op, mempool_load_time) =
719719+ self.get_latest_did_operation_from_mempool(did)?;
675720 let mempool_time = mempool_start.elapsed();
676676- log::debug!("[Resolve] Mempool check: found latest operation in {:?} (load: {:?})", mempool_time, mempool_load_time);
721721+ log::debug!(
722722+ "[Resolve] Mempool check: found latest operation in {:?} (load: {:?})",
723723+ mempool_time,
724724+ mempool_load_time
725725+ );
677726678727 // If mempool has a non-nullified operation, use it and skip bundle lookup
679728 if let Some((operation, position)) = latest_mempool_op {
680729 let load_start = Instant::now();
681681- log::debug!("[Resolve] Found latest non-nullified operation in mempool, skipping bundle lookup");
730730+ log::debug!(
731731+ "[Resolve] Found latest non-nullified operation in mempool, skipping bundle lookup"
732732+ );
682733683734 // Build document from latest mempool operation
684684- let document = crate::resolver::resolve_did_document(did, std::slice::from_ref(&operation))?;
735735+ let document =
736736+ crate::resolver::resolve_did_document(did, std::slice::from_ref(&operation))?;
685737 let load_time = load_start.elapsed();
686738687739 return Ok(ResolveResult {
···695747 load_time,
696748 total_time: total_start.elapsed(),
697749 locations_found: 1, // Found one operation in mempool
698698- shard_num: 0, // No shard for mempool
750750+ shard_num: 0, // No shard for mempool
699751 shard_stats: None,
700752 lookup_timings: None,
701753 });
702754 }
703755704756 // Mempool is empty or all nullified - check bundles
705705- log::debug!("[Resolve] No non-nullified operations in mempool, checking bundles for DID: {}", did);
757757+ log::debug!(
758758+ "[Resolve] No non-nullified operations in mempool, checking bundles for DID: {}",
759759+ did
760760+ );
706761 self.ensure_did_index()?;
707762 let index_start = Instant::now();
708763 let did_index = self.did_index.read().unwrap();
709709- let (locations, shard_stats, shard_num, lookup_timings) =
710710- did_index.as_ref().unwrap().get_did_locations_with_stats(did)?;
764764+ let (locations, shard_stats, shard_num, lookup_timings) = did_index
765765+ .as_ref()
766766+ .unwrap()
767767+ .get_did_locations_with_stats(did)?;
711768 let index_time = index_start.elapsed();
712712- log::debug!("[Resolve] Bundle index lookup: {} locations found in {:?}", locations.len(), index_time);
769769+ log::debug!(
770770+ "[Resolve] Bundle index lookup: {} locations found in {:?}",
771771+ locations.len(),
772772+ index_time
773773+ );
713774714775 // Find latest non-nullified operation from bundles
715776 let load_start = Instant::now();
716777 let mut latest_operation: Option<(Operation, u32, usize)> = None;
717778 let mut latest_time = DateTime::parse_from_rfc3339("1970-01-01T00:00:00Z").unwrap();
718718-779779+719780 for loc in &locations {
720781 if !loc.nullified()
721782 && let Ok(op) = self.get_operation(loc.bundle() as u32, loc.position() as usize)
···728789 }
729790 let load_time = load_start.elapsed();
730791731731- let (operation, bundle_number, position) = latest_operation
732732- .ok_or_else(|| anyhow::anyhow!("DID not found: {} (checked bundles and mempool)", did))?;
792792+ let (operation, bundle_number, position) = latest_operation.ok_or_else(|| {
793793+ anyhow::anyhow!("DID not found: {} (checked bundles and mempool)", did)
794794+ })?;
733795734796 // Build document from latest bundle operation
735735- let document = crate::resolver::resolve_did_document(did, std::slice::from_ref(&operation))?;
797797+ let document =
798798+ crate::resolver::resolve_did_document(did, std::slice::from_ref(&operation))?;
736799737800 Ok(ResolveResult {
738801 document,
···783846 }
784847 }
785848786786- ops_with_loc.sort_by_key(|owl| {
787787- bundle_position_to_global(owl.bundle, owl.position)
788788- });
849849+ ops_with_loc.sort_by_key(|owl| bundle_position_to_global(owl.bundle, owl.position));
789850790851 Ok((ops_with_loc, load_start.elapsed()))
791852 }
···9421003 .retain(|b| b.bundle_number <= spec.target_bundle);
94310049441005 // Use default flush interval for rollback
945945- self.build_did_index(crate::constants::DID_INDEX_FLUSH_INTERVAL, None::<fn(u32, u32, u64, u64)>, None, None)?;
10061006+ self.build_did_index(
10071007+ crate::constants::DID_INDEX_FLUSH_INTERVAL,
10081008+ None::<fn(u32, u32, u64, u64)>,
10091009+ None,
10101010+ None,
10111011+ )?;
94610129471013 Ok(RollbackResult {
9481014 success: true,
···9811047 }
98210489831049 // === DID Index ===
984984- pub fn build_did_index<F>(&self, flush_interval: u32, progress_cb: Option<F>, num_threads: Option<usize>, interrupted: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>) -> Result<RebuildStats>
10501050+ pub fn build_did_index<F>(
10511051+ &self,
10521052+ flush_interval: u32,
10531053+ progress_cb: Option<F>,
10541054+ num_threads: Option<usize>,
10551055+ interrupted: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
10561056+ ) -> Result<RebuildStats>
9851057 where
9861058 F: Fn(u32, u32, u64, u64) + Send + Sync, // (current, total, bytes_processed, total_bytes)
9871059 {
9881060 use std::time::Instant;
98910619901062 let actual_threads = num_threads.unwrap_or(0); // 0 = auto-detect
991991-10631063+9921064 let last_bundle = self.get_last_bundle();
9931065 let mut stats = RebuildStats::default();
9941066···10101082 if flush_interval > 0 {
10111083 if flush_interval == crate::constants::DID_INDEX_FLUSH_INTERVAL {
10121084 // Default value - show with tuning hint
10131013- eprintln!(" Flush: Every {} bundles (tune with --flush-interval)", flush_interval);
10851085+ eprintln!(
10861086+ " Flush: Every {} bundles (tune with --flush-interval)",
10871087+ flush_interval
10881088+ );
10141089 } else {
10151090 // Non-default value - show with tuning hint
10161016- eprintln!(" Flush: {} bundles (you can tune with --flush-interval)", flush_interval);
10911091+ eprintln!(
10921092+ " Flush: {} bundles (you can tune with --flush-interval)",
10931093+ flush_interval
10941094+ );
10171095 }
10181096 } else {
10191097 eprintln!(" Flush: Only at end (maximum memory usage)");
···10511129 }
10521130 }),
10531131 actual_threads,
10541054- interrupted
11321132+ interrupted,
10551133 )?
10561134 } else {
10571135 return Err(anyhow::anyhow!("DID index not initialized"));
···1065114310661144 eprintln!("\n");
10671145 eprintln!("✅ Index Build Complete");
10681068- eprintln!(" Time: {:.1}s (Stage 1: {:.1}s, Stage 2: {:.1}s)",
11461146+ eprintln!(
11471147+ " Time: {:.1}s (Stage 1: {:.1}s, Stage 2: {:.1}s)",
10691148 total_duration.as_secs_f64(),
10701149 stage1_duration.as_secs_f64(),
10711150 stage2_duration.as_secs_f64()
10721151 );
10731073- eprintln!(" Operations: {}", crate::format::format_number(total_operations));
11521152+ eprintln!(
11531153+ " Operations: {}",
11541154+ crate::format::format_number(total_operations)
11551155+ );
1074115610751157 // Get final stats
10761158 let final_stats = self.get_did_index_stats();
···10791161 .and_then(|v| v.as_i64())
10801162 .unwrap_or(0);
1081116310821082- eprintln!(" Total DIDs: {}", crate::format::format_number(total_dids as u64));
11641164+ eprintln!(
11651165+ " Total DIDs: {}",
11661166+ crate::format::format_number(total_dids as u64)
11671167+ );
1083116810841169 Ok(stats)
10851170 }
···10891174 /// Returns keys like `exists`, `total_dids`, `last_bundle`, `delta_segments`, `shard_count` when available.
10901175 pub fn get_did_index_stats(&self) -> HashMap<String, serde_json::Value> {
10911176 self.ensure_did_index().ok(); // Stats might be called even if index doesn't exist
10921092- self.did_index.read().unwrap().as_ref().map(|idx| idx.get_stats()).unwrap_or_default()
11771177+ self.did_index
11781178+ .read()
11791179+ .unwrap()
11801180+ .as_ref()
11811181+ .map(|idx| idx.get_stats())
11821182+ .unwrap_or_default()
10931183 }
1094118410951185 /// Get DID index stats as struct (legacy format)
···11121202 }
1113120311141204 /// Verify DID index and return detailed result
11151115- ///
12051205+ ///
11161206 /// Performs standard integrity check by default. If `full` is true, also rebuilds
11171207 /// the index in a temporary directory and compares with the existing index.
11181118- ///
12081208+ ///
11191209 /// For server startup checks, call with `full=false` and check `verify_result.missing_base_shards`
11201210 /// and `verify_result.missing_delta_segments` to determine if the index is corrupted.
11211211 pub fn verify_did_index<F>(
···11291219 F: Fn(u32, u32, u64, u64) + Send + Sync, // (current, total, bytes_processed, total_bytes)
11301220 {
11311221 self.ensure_did_index()?;
11321132-12221222+11331223 let did_index = self.did_index.read().unwrap();
11341134- let idx = did_index.as_ref().ok_or_else(|| anyhow::anyhow!("DID index not initialized"))?;
11351135-12241224+ let idx = did_index
12251225+ .as_ref()
12261226+ .ok_or_else(|| anyhow::anyhow!("DID index not initialized"))?;
12271227+11361228 let last_bundle = self.get_last_bundle();
11371229 let mut verify_result = idx.verify_integrity(last_bundle)?;
11381138-12301230+11391231 // If full verification requested, rebuild and compare
11401232 if full {
11411233 // Adapt callback for build_from_scratch which expects Option<String> as 4th param
···11521244 build_callback,
11531245 )?;
11541246 verify_result.errors += rebuild_result.errors;
11551155- verify_result.error_categories.extend(rebuild_result.error_categories);
12471247+ verify_result
12481248+ .error_categories
12491249+ .extend(rebuild_result.error_categories);
11561250 }
11571157-12511251+11581252 Ok(verify_result)
11591253 }
11601254···11691263 F: Fn(u32, u32, u64, u64) + Send + Sync, // (current, total, bytes_processed, total_bytes)
11701264 {
11711265 self.ensure_did_index()?;
11721172-12661266+11731267 let last_bundle = self.get_last_bundle();
11741174-12681268+11751269 // Create bundle loader closure
11761270 let bundle_loader = |bundle_num: u32| -> Result<Vec<(String, bool)>> {
11771271 let result = self.load_bundle(bundle_num, LoadOptions::default())?;
···11811275 .map(|op| (op.did.clone(), op.nullified))
11821276 .collect())
11831277 };
11841184-12781278+11851279 let mut did_index = self.did_index.write().unwrap();
11861186- let idx = did_index.as_mut().ok_or_else(|| anyhow::anyhow!("DID index not initialized"))?;
11871187-12801280+ let idx = did_index
12811281+ .as_mut()
12821282+ .ok_or_else(|| anyhow::anyhow!("DID index not initialized"))?;
12831283+11881284 let mut repair_result = idx.repair(last_bundle, bundle_loader)?;
11891189-12851285+11901286 // If repair indicates full rebuild is needed, do it
11911287 if repair_result.repaired && repair_result.bundles_processed == 0 {
11921288 drop(did_index);
11931193-12891289+11941290 // Adapt callback signature for build_did_index
11951291 let build_callback = progress_callback.map(|cb| {
11961292 move |current: u32, total: u32, bytes: u64, total_bytes: u64| {
···11981294 }
11991295 });
12001296 self.build_did_index(flush_interval, build_callback, Some(num_threads), None)?;
12011201-12971297+12021298 repair_result.bundles_processed = last_bundle;
12031299 }
12041204-13001300+12051301 Ok(repair_result)
12061302 }
12071303···13341430 if let Ok(entries) = std::fs::read_dir(&self.directory) {
13351431 for entry in entries.flatten() {
13361432 if let Some(name) = entry.file_name().to_str()
13371337- && name.starts_with(constants::MEMPOOL_FILE_PREFIX) && name.ends_with(".jsonl")
14331433+ && name.starts_with(constants::MEMPOOL_FILE_PREFIX)
14341434+ && name.ends_with(".jsonl")
13381435 {
13391436 let _ = std::fs::remove_file(entry.path());
13401437 }
···13941491 if let Ok(entries) = std::fs::read_dir(&self.directory) {
13951492 for entry in entries.flatten() {
13961493 if let Some(name) = entry.file_name().to_str()
13971397- && name.starts_with(constants::MEMPOOL_FILE_PREFIX) && name.ends_with(".jsonl")
14941494+ && name.starts_with(constants::MEMPOOL_FILE_PREFIX)
14951495+ && name.ends_with(".jsonl")
13981496 {
13991497 // Extract bundle number from filename: plc_mempool_NNNNNN.jsonl
14001498 if let Some(num_str) = name
14011499 .strip_prefix(constants::MEMPOOL_FILE_PREFIX)
14021500 .and_then(|s| s.strip_suffix(".jsonl"))
14031403- && let Ok(bundle_num) = num_str.parse::<u32>() {
15011501+ && let Ok(bundle_num) = num_str.parse::<u32>()
15021502+ {
14041503 // Delete mempool files for completed bundles or way future bundles
14051504 if bundle_num <= last_bundle || bundle_num > next_bundle_num {
14061406- log::warn!(
14071407- "Removing stale mempool file for bundle {:06}",
14081408- bundle_num
14091409- );
15051505+ log::warn!("Removing stale mempool file for bundle {:06}", bundle_num);
14101506 let _ = std::fs::remove_file(entry.path());
14111507 found_stale_files = true;
14121508 }
···14321528 }
1433152914341530 // Get the last operation from the previous bundle
14351435- let last_bundle_time = if next_bundle_num > 1
14361436- && let Ok(last_bundle_result) = self.load_bundle(next_bundle_num - 1, LoadOptions::default()) {
14371437- last_bundle_result.operations.last().and_then(|last_op| {
14381438- chrono::DateTime::parse_from_rfc3339(&last_op.created_at)
14391439- .ok()
14401440- .map(|dt| dt.with_timezone(&chrono::Utc))
14411441- })
14421442- } else {
14431443- None
14441444- };
15311531+ let last_bundle_time = if next_bundle_num > 1
15321532+ && let Ok(last_bundle_result) =
15331533+ self.load_bundle(next_bundle_num - 1, LoadOptions::default())
15341534+ {
15351535+ last_bundle_result.operations.last().and_then(|last_op| {
15361536+ chrono::DateTime::parse_from_rfc3339(&last_op.created_at)
15371537+ .ok()
15381538+ .map(|dt| dt.with_timezone(&chrono::Utc))
15391539+ })
15401540+ } else {
15411541+ None
15421542+ };
1445154314461544 // Special case: When creating the first bundle (next_bundle_num == 1, meaning
14471545 // last_bundle == 0, i.e., empty repository), any existing mempool is likely stale
···1464156214651563 // Check if mempool operations are chronologically valid relative to last bundle
14661564 if let Some(last_time) = last_bundle_time
14671467- && let Some(first_mempool_time) = mempool_stats.first_time {
14681468- // Case 1: Mempool operations are BEFORE the last bundle (definitely stale)
14691469- if first_mempool_time < last_time {
14701470- log::warn!("Detected stale mempool data (operations before last bundle)");
14711471- log::warn!(
14721472- "First mempool op: {}, Last bundle op: {}",
14731473- first_mempool_time.format("%Y-%m-%d %H:%M:%S"),
14741474- last_time.format("%Y-%m-%d %H:%M:%S")
14751475- );
14761476- log::warn!("Clearing mempool to start fresh...");
14771477- self.clear_mempool()?;
14781478- return Ok(());
14791479- }
15651565+ && let Some(first_mempool_time) = mempool_stats.first_time
15661566+ {
15671567+ // Case 1: Mempool operations are BEFORE the last bundle (definitely stale)
15681568+ if first_mempool_time < last_time {
15691569+ log::warn!("Detected stale mempool data (operations before last bundle)");
15701570+ log::warn!(
15711571+ "First mempool op: {}, Last bundle op: {}",
15721572+ first_mempool_time.format("%Y-%m-%d %H:%M:%S"),
15731573+ last_time.format("%Y-%m-%d %H:%M:%S")
15741574+ );
15751575+ log::warn!("Clearing mempool to start fresh...");
15761576+ self.clear_mempool()?;
15771577+ return Ok(());
15781578+ }
1480157914811481- // Case 2: Mempool operations are slightly after last bundle, but way too close
14821482- // This indicates they're from a previous failed attempt at this bundle
14831483- // BUT: Only clear if the mempool file is old (modified > 1 hour ago)
14841484- // If it's recent, it might be a legitimate resume of a slow sync
14851485- let time_diff = first_mempool_time.signed_duration_since(last_time);
14861486- if time_diff
14871487- < chrono::Duration::seconds(constants::MIN_BUNDLE_CREATION_INTERVAL_SECS)
14881488- && mempool_stats.count < constants::BUNDLE_SIZE
14891489- {
14901490- // Check mempool file modification time
14911491- let mempool_filename = format!(
14921492- "{}{:06}.jsonl",
14931493- constants::MEMPOOL_FILE_PREFIX,
14941494- next_bundle_num
14951495- );
14961496- let mempool_path = self.directory.join(mempool_filename);
15801580+ // Case 2: Mempool operations are slightly after last bundle, but way too close
15811581+ // This indicates they're from a previous failed attempt at this bundle
15821582+ // BUT: Only clear if the mempool file is old (modified > 1 hour ago)
15831583+ // If it's recent, it might be a legitimate resume of a slow sync
15841584+ let time_diff = first_mempool_time.signed_duration_since(last_time);
15851585+ if time_diff < chrono::Duration::seconds(constants::MIN_BUNDLE_CREATION_INTERVAL_SECS)
15861586+ && mempool_stats.count < constants::BUNDLE_SIZE
15871587+ {
15881588+ // Check mempool file modification time
15891589+ let mempool_filename = format!(
15901590+ "{}{:06}.jsonl",
15911591+ constants::MEMPOOL_FILE_PREFIX,
15921592+ next_bundle_num
15931593+ );
15941594+ let mempool_path = self.directory.join(mempool_filename);
1497159514981498- let is_stale = if let Ok(metadata) = std::fs::metadata(&mempool_path) {
14991499- if let Ok(modified) = metadata.modified() {
15001500- let modified_time = std::time::SystemTime::now()
15011501- .duration_since(modified)
15021502- .unwrap_or(std::time::Duration::from_secs(0));
15031503- modified_time > std::time::Duration::from_secs(3600) // 1 hour
15041504- } else {
15051505- false // Can't get modified time, assume not stale
15061506- }
15961596+ let is_stale = if let Ok(metadata) = std::fs::metadata(&mempool_path) {
15971597+ if let Ok(modified) = metadata.modified() {
15981598+ let modified_time = std::time::SystemTime::now()
15991599+ .duration_since(modified)
16001600+ .unwrap_or(std::time::Duration::from_secs(0));
16011601+ modified_time > std::time::Duration::from_secs(3600) // 1 hour
15071602 } else {
15081508- false // File doesn't exist, assume not stale
15091509- };
16031603+ false // Can't get modified time, assume not stale
16041604+ }
16051605+ } else {
16061606+ false // File doesn't exist, assume not stale
16071607+ };
1510160815111511- if is_stale {
15121512- log::warn!(
15131513- "Detected potentially stale mempool data (too close to last bundle timestamp)"
15141514- );
15151515- log::warn!(
15161516- "Time difference: {}s, Operations: {}/{}",
15171517- time_diff.num_seconds(),
15181518- mempool_stats.count,
15191519- constants::BUNDLE_SIZE
15201520- );
15211521- log::warn!(
15221522- "This likely indicates a previous failed sync attempt. Clearing mempool..."
15231523- );
15241524- self.clear_mempool()?;
15251525- } else if *self.verbose.lock().unwrap() {
15261526- log::debug!(
15271527- "Mempool appears recent, allowing resume despite close timestamp"
15281528- );
15291529- }
15301530- return Ok(());
16091609+ if is_stale {
16101610+ log::warn!(
16111611+ "Detected potentially stale mempool data (too close to last bundle timestamp)"
16121612+ );
16131613+ log::warn!(
16141614+ "Time difference: {}s, Operations: {}/{}",
16151615+ time_diff.num_seconds(),
16161616+ mempool_stats.count,
16171617+ constants::BUNDLE_SIZE
16181618+ );
16191619+ log::warn!(
16201620+ "This likely indicates a previous failed sync attempt. Clearing mempool..."
16211621+ );
16221622+ self.clear_mempool()?;
16231623+ } else if *self.verbose.lock().unwrap() {
16241624+ log::debug!("Mempool appears recent, allowing resume despite close timestamp");
15311625 }
16261626+ return Ok(());
16271627+ }
15321628 }
1533162915341630 // Check if mempool has way too many operations (likely from failed previous attempt)
···1648174416491745 /// Fetch and save next bundle from PLC directory
16501746 /// DID index is updated on every bundle (fast with delta segments)
16511651- pub async fn sync_next_bundle(&self, client: &crate::plc_client::PLCClient) -> Result<SyncResult> {
17471747+ pub async fn sync_next_bundle(
17481748+ &self,
17491749+ client: &crate::plc_client::PLCClient,
17501750+ ) -> Result<SyncResult> {
16521751 use crate::sync::{get_boundary_cids, strip_boundary_duplicates};
16531752 use std::time::Instant;
16541753···16841783 // If mempool has operations, update cursor AND boundaries from mempool
16851784 // (mempool operations already had boundary dedup applied when they were added)
16861785 let mempool_stats = self.get_mempool_stats()?;
16871687- if mempool_stats.count > 0 && let Some(last_time) = mempool_stats.last_time {
17861786+ if mempool_stats.count > 0
17871787+ && let Some(last_time) = mempool_stats.last_time
17881788+ {
16881789 if *self.verbose.lock().unwrap() {
16891790 log::debug!(
16901791 "Mempool has {} ops, resuming from {}",
···20502151 synced += 1;
2051215220522153 // Check if we've reached the limit
20532053- if let Some(max) = max_bundles && synced >= max {
21542154+ if let Some(max) = max_bundles
21552155+ && synced >= max
21562156+ {
20542157 break;
20552158 }
20562159 }
···21022205 // Use multi-frame compression for better performance on large bundles
2103220621042207 // Compress operations to frames using parallel compression
21052105- let compress_result = crate::bundle_format::compress_operations_to_frames_parallel(&operations)?;
22082208+ let compress_result =
22092209+ crate::bundle_format::compress_operations_to_frames_parallel(&operations)?;
2106221021072107- let serialize_time = std::time::Duration::from_secs_f64(compress_result.serialize_time_ms / 1000.0);
21082108- let compress_time = std::time::Duration::from_secs_f64(compress_result.compress_time_ms / 1000.0);
22112211+ let serialize_time =
22122212+ std::time::Duration::from_secs_f64(compress_result.serialize_time_ms / 1000.0);
22132213+ let compress_time =
22142214+ std::time::Duration::from_secs_f64(compress_result.compress_time_ms / 1000.0);
2109221521102216 let uncompressed_size = compress_result.uncompressed_size;
21112217 let compressed_size = compress_result.compressed_size;
···24212527 // Serialize and write index in blocking task to avoid blocking async runtime
24222528 // Use Index::save() which does atomic write (temp file + rename)
24232529 let directory = self.directory.clone();
24242424- tokio::task::spawn_blocking(move || {
24252425- index_clone.save(directory)
24262426- })
24272427- .await
24282428- .context("Index write task failed")??;
25302530+ tokio::task::spawn_blocking(move || index_clone.save(directory))
25312531+ .await
25322532+ .context("Index write task failed")??;
24292533 let index_write_time = index_write_start.elapsed();
2430253424312535 Ok((
···2915301929163020 // Scan shards directory (.plcbundle/shards/)
29173021 let shards_dir = did_index_dir.join(constants::DID_INDEX_SHARDS);
29182918- if shards_dir.exists() && let Ok(entries) = fs::read_dir(&shards_dir) {
30223022+ if shards_dir.exists()
30233023+ && let Ok(entries) = fs::read_dir(&shards_dir)
30243024+ {
29193025 for entry in entries {
29203026 let entry = match entry {
29213027 Ok(e) => e,
···29583064 use std::fs;
2959306529603066 let verbose = *self.verbose.lock().unwrap();
29612961-30673067+29623068 if verbose {
29633069 log::info!("Starting repository cleanup...");
29643070 }
···29723078 if verbose {
29733079 log::info!("Scanning repository root directory: {}", root_dir.display());
29743080 }
29752975-30813081+29763082 if let Ok(entries) = fs::read_dir(root_dir) {
29773083 for entry in entries {
29783084 let entry = match entry {
···29953101 bytes_freed += size;
29963102 size
29973103 }
29982998- Err(_) => 0
31043104+ Err(_) => 0,
29993105 };
3000310630013107 match fs::remove_file(&path) {
30023108 Ok(_) => {
30033109 files_removed += 1;
30043110 if verbose {
30053005- log::info!(" ✓ Removed: {} ({})",
31113111+ log::info!(
31123112+ " ✓ Removed: {} ({})",
30063113 path.file_name().and_then(|n| n.to_str()).unwrap_or("?"),
30073007- crate::format::format_bytes(file_size));
31143114+ crate::format::format_bytes(file_size)
31153115+ );
30083116 }
30093117 }
30103118 Err(e) => {
···30253133 if verbose {
30263134 log::info!("Scanning DID index directory: {}", did_index_dir.display());
30273135 }
30283028-31363136+30293137 // Clean config.json.tmp
30303138 let config_tmp = did_index_dir.join(format!("{}.tmp", constants::DID_INDEX_CONFIG));
30313139 if config_tmp.exists() {
···30353143 bytes_freed += size;
30363144 size
30373145 }
30383038- Err(_) => 0
31463146+ Err(_) => 0,
30393147 };
3040314830413149 match fs::remove_file(&config_tmp) {
30423150 Ok(_) => {
30433151 files_removed += 1;
30443152 if verbose {
30453045- log::info!(" ✓ Removed: {} ({})",
30463046- config_tmp.file_name().and_then(|n| n.to_str()).unwrap_or("?"),
30473047- crate::format::format_bytes(file_size));
31533153+ log::info!(
31543154+ " ✓ Removed: {} ({})",
31553155+ config_tmp
31563156+ .file_name()
31573157+ .and_then(|n| n.to_str())
31583158+ .unwrap_or("?"),
31593159+ crate::format::format_bytes(file_size)
31603160+ );
30483161 }
30493162 }
30503163 Err(e) => {
···30683181 let entry = match entry {
30693182 Ok(e) => e,
30703183 Err(e) => {
30713071- errors.push(format!("Failed to read shards directory entry: {}", e));
31843184+ errors
31853185+ .push(format!("Failed to read shards directory entry: {}", e));
30723186 continue;
30733187 }
30743188 };
···30853199 bytes_freed += size;
30863200 size
30873201 }
30883088- Err(_) => 0
32023202+ Err(_) => 0,
30893203 };
3090320430913205 match fs::remove_file(&path) {
30923206 Ok(_) => {
30933207 files_removed += 1;
30943208 if verbose {
30953095- log::info!(" ✓ Removed: {} ({})",
30963096- path.file_name().and_then(|n| n.to_str()).unwrap_or("?"),
30973097- crate::format::format_bytes(file_size));
32093209+ log::info!(
32103210+ " ✓ Removed: {} ({})",
32113211+ path.file_name()
32123212+ .and_then(|n| n.to_str())
32133213+ .unwrap_or("?"),
32143214+ crate::format::format_bytes(file_size)
32153215+ );
30983216 }
30993217 }
31003218 Err(e) => {
31013101- let error_msg = format!("Failed to remove {}: {}", path.display(), e);
32193219+ let error_msg =
32203220+ format!("Failed to remove {}: {}", path.display(), e);
31023221 errors.push(error_msg.clone());
31033222 if verbose {
31043223 log::warn!(" ✗ {}", error_msg);
···31123231 log::debug!("Shards directory does not exist: {}", shards_dir.display());
31133232 }
31143233 } else if verbose {
31153115- log::debug!("DID index directory does not exist: {}", did_index_dir.display());
32343234+ log::debug!(
32353235+ "DID index directory does not exist: {}",
32363236+ did_index_dir.display()
32373237+ );
31163238 }
3117323931183240 // Summary logging
31193241 if verbose {
31203242 if files_removed > 0 {
31213121- log::info!("Cleanup complete: removed {} file(s), freed {}",
32433243+ log::info!(
32443244+ "Cleanup complete: removed {} file(s), freed {}",
31223245 files_removed,
31233123- crate::format::format_bytes(bytes_freed));
32463246+ crate::format::format_bytes(bytes_freed)
32473247+ );
31243248 } else {
31253249 log::info!("Cleanup complete: no temporary files found");
31263250 }
31273127-32513251+31283252 if !errors.is_empty() {
31293253 log::warn!("Encountered {} error(s) during cleanup", errors.len());
31303254 }
···31333257 Ok(CleanResult {
31343258 files_removed,
31353259 bytes_freed,
31363136- errors: if errors.is_empty() { None } else { Some(errors) },
32603260+ errors: if errors.is_empty() {
32613261+ None
32623262+ } else {
32633263+ Some(errors)
32643264+ },
31373265 })
31383266 }
31393267···3374350233753503 fn matches_filter(&self, op: &Operation, filter: &OperationFilter) -> bool {
33763504 if let Some(ref did) = filter.did
33773377- && &op.did != did {
35053505+ && &op.did != did
35063506+ {
33783507 return false;
33793508 }
3380350933813510 if let Some(ref op_type) = filter.operation_type
33823382- && &op.operation != op_type {
35113511+ && &op.operation != op_type
35123512+ {
33833513 return false;
33843514 }
33853515···34713601 F: Fn(u32, usize, usize, u64) + Send + Sync + 'static,
34723602 {
34733603 use crate::remote::RemoteClient;
34743474- use std::sync::atomic::{AtomicUsize, AtomicU64, Ordering};
36043604+ use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
3475360534763606 let target_dir = target_dir.as_ref();
34773607···35223652 }
3523365335243654 let count = downloaded.fetch_add(1, Ordering::SeqCst) + 1;
35253525- let bytes = bytes_downloaded.fetch_add(data_len, Ordering::SeqCst) + data_len;
36553655+ let bytes =
36563656+ bytes_downloaded.fetch_add(data_len, Ordering::SeqCst) + data_len;
3526365735273658 // Call progress callback
35283659 if let Some(ref cb) = progress_cb {
···36693800pub enum ExportFormat {
36703801 JsonLines,
36713802}
36723672-3673380336743804/// Statistics collected during export
36753805#[derive(Debug, Default)]
+57-43
src/mempool.rs
···11//! Persistent pre-bundle operation store with strict chronological validation, CID deduplication, incremental saving, and fast DID lookups
22// src/mempool.rs
33use crate::constants;
44+use crate::format::format_std_duration_ms;
45use crate::operations::Operation;
56use anyhow::{Result, bail};
67use chrono::{DateTime, Utc};
78use log::{debug, info};
88-use crate::format::format_std_duration_ms;
99use std::collections::{HashMap, HashSet};
1010use std::fs::{self, File, OpenOptions};
1111use std::io::{BufRead, BufReader, BufWriter, Write};
···104104 None => {
105105 skipped_no_cid += 1;
106106 continue;
107107- },
107107+ }
108108 };
109109110110 // Skip duplicates
···149149 // Add new operations and update DID index
150150 let start_idx = self.operations.len();
151151 self.operations.extend(new_ops);
152152-152152+153153 // Update DID index for new operations
154154 for (offset, op) in self.operations[start_idx..].iter().enumerate() {
155155 let idx = start_idx + offset;
156156 self.did_index.entry(op.did.clone()).or_default().push(idx);
157157 }
158158-158158+159159 self.validated = true;
160160 self.dirty = true;
161161···449449 let op = Operation::from_json(&line)?;
450450 let idx = self.operations.len();
451451 self.operations.push(op);
452452-452452+453453 // Update DID index
454454 let did = self.operations[idx].did.clone();
455455 self.did_index.entry(did).or_default().push(idx);
···562562 pub fn find_did_operations(&self, did: &str) -> Vec<Operation> {
563563 if let Some(indices) = self.did_index.get(did) {
564564 // Fast path: use index
565565- indices.iter().map(|&idx| self.operations[idx].clone()).collect()
565565+ indices
566566+ .iter()
567567+ .map(|&idx| self.operations[idx].clone())
568568+ .collect()
566569 } else {
567570 // DID not in index (shouldn't happen if index is maintained correctly)
568571 Vec::new()
569572 }
570573 }
571571-574574+572575 /// Rebuild DID index from operations (used after take/clear)
573576 fn rebuild_did_index(&mut self) {
574577 self.did_index.clear();
···584587 if let Some(indices) = self.did_index.get(did) {
585588 // Operations are in chronological order, so highest index = latest
586589 // Find the highest index that's not nullified
587587- indices
588588- .iter()
589589- .rev()
590590- .find_map(|&idx| {
591591- let op = &self.operations[idx];
592592- if !op.nullified {
593593- Some((op.clone(), idx))
594594- } else {
595595- None
596596- }
597597- })
590590+ indices.iter().rev().find_map(|&idx| {
591591+ let op = &self.operations[idx];
592592+ if !op.nullified {
593593+ Some((op.clone(), idx))
594594+ } else {
595595+ None
596596+ }
597597+ })
598598 } else {
599599 None
600600 }
···650650 .unwrap()
651651 .with_timezone(&Utc);
652652 let mut mempool = Mempool::new(tmp.path(), 1, min_time, false).unwrap();
653653-653653+654654 assert_eq!(mempool.count(), 0);
655655-655655+656656 let ops = vec![
657657 create_test_operation("did:plc:test1", "cid1", "2024-01-01T00:00:01Z"),
658658 create_test_operation("did:plc:test2", "cid2", "2024-01-01T00:00:02Z"),
···688688 .unwrap()
689689 .with_timezone(&Utc);
690690 let mut mempool = Mempool::new(tmp.path(), 1, min_time, false).unwrap();
691691-691691+692692 assert_eq!(mempool.get_first_time(), None);
693693-694694- let ops = vec![create_test_operation("did:plc:test1", "cid1", "2024-01-01T00:00:01Z")];
693693+694694+ let ops = vec![create_test_operation(
695695+ "did:plc:test1",
696696+ "cid1",
697697+ "2024-01-01T00:00:01Z",
698698+ )];
695699 mempool.add(ops).unwrap();
696696- assert_eq!(mempool.get_first_time(), Some("2024-01-01T00:00:01Z".to_string()));
700700+ assert_eq!(
701701+ mempool.get_first_time(),
702702+ Some("2024-01-01T00:00:01Z".to_string())
703703+ );
697704 }
698705699706 #[test]
···703710 .unwrap()
704711 .with_timezone(&Utc);
705712 let mut mempool = Mempool::new(tmp.path(), 1, min_time, false).unwrap();
706706-713713+707714 assert_eq!(mempool.get_last_time(), None);
708708-715715+709716 let ops = vec![
710717 create_test_operation("did:plc:test1", "cid1", "2024-01-01T00:00:01Z"),
711718 create_test_operation("did:plc:test2", "cid2", "2024-01-01T00:00:02Z"),
712719 ];
713720 mempool.add(ops).unwrap();
714714- assert_eq!(mempool.get_last_time(), Some("2024-01-01T00:00:02Z".to_string()));
721721+ assert_eq!(
722722+ mempool.get_last_time(),
723723+ Some("2024-01-01T00:00:02Z".to_string())
724724+ );
715725 }
716726717727 #[test]
···721731 .unwrap()
722732 .with_timezone(&Utc);
723733 let mut mempool = Mempool::new(tmp.path(), 1, min_time, false).unwrap();
724724-734734+725735 let ops = vec![
726736 create_test_operation("did:plc:test1", "cid1", "2024-01-01T00:00:01Z"),
727737 create_test_operation("did:plc:test2", "cid2", "2024-01-01T00:00:02Z"),
728738 create_test_operation("did:plc:test3", "cid3", "2024-01-01T00:00:03Z"),
729739 ];
730740 mempool.add(ops).unwrap();
731731-741741+732742 let peeked = mempool.peek(2);
733743 assert_eq!(peeked.len(), 2);
734744 assert_eq!(peeked[0].cid, Some("cid1".to_string()));
735745 assert_eq!(peeked[1].cid, Some("cid2".to_string()));
736736-746746+737747 // Peek should not remove operations
738748 assert_eq!(mempool.count(), 3);
739749 }
···745755 .unwrap()
746756 .with_timezone(&Utc);
747757 let mut mempool = Mempool::new(tmp.path(), 1, min_time, false).unwrap();
748748-749749- let ops = vec![create_test_operation("did:plc:test1", "cid1", "2024-01-01T00:00:01Z")];
758758+759759+ let ops = vec![create_test_operation(
760760+ "did:plc:test1",
761761+ "cid1",
762762+ "2024-01-01T00:00:01Z",
763763+ )];
750764 mempool.add(ops).unwrap();
751751-765765+752766 let peeked = mempool.peek(10);
753767 assert_eq!(peeked.len(), 1);
754768 }
···760774 .unwrap()
761775 .with_timezone(&Utc);
762776 let mut mempool = Mempool::new(tmp.path(), 1, min_time, false).unwrap();
763763-777777+764778 let ops = vec![
765779 create_test_operation("did:plc:test1", "cid1", "2024-01-01T00:00:01Z"),
766780 create_test_operation("did:plc:test2", "cid2", "2024-01-01T00:00:02Z"),
767781 ];
768782 mempool.add(ops).unwrap();
769783 assert_eq!(mempool.count(), 2);
770770-784784+771785 mempool.clear();
772786 assert_eq!(mempool.count(), 0);
773787 }
···779793 .unwrap()
780794 .with_timezone(&Utc);
781795 let mut mempool = Mempool::new(tmp.path(), 1, min_time, false).unwrap();
782782-796796+783797 let ops = vec![
784798 create_test_operation("did:plc:test1", "cid1", "2024-01-01T00:00:01Z"),
785799 create_test_operation("did:plc:test1", "cid2", "2024-01-01T00:00:02Z"),
786800 create_test_operation("did:plc:test2", "cid3", "2024-01-01T00:00:03Z"),
787801 ];
788802 mempool.add(ops).unwrap();
789789-803803+790804 let found = mempool.find_did_operations("did:plc:test1");
791805 assert_eq!(found.len(), 2);
792806 assert_eq!(found[0].cid, Some("cid1".to_string()));
793807 assert_eq!(found[1].cid, Some("cid2".to_string()));
794794-808808+795809 let found = mempool.find_did_operations("did:plc:test2");
796810 assert_eq!(found.len(), 1);
797811 assert_eq!(found[0].cid, Some("cid3".to_string()));
798798-812812+799813 let found = mempool.find_did_operations("did:plc:nonexistent");
800814 assert_eq!(found.len(), 0);
801815 }
···807821 .unwrap()
808822 .with_timezone(&Utc);
809823 let mempool = Mempool::new(tmp.path(), 1, min_time, false).unwrap();
810810-824824+811825 let stats = mempool.stats();
812826 assert_eq!(stats.count, 0);
813827 assert!(!stats.can_create_bundle);
···821835 .unwrap()
822836 .with_timezone(&Utc);
823837 let mut mempool = Mempool::new(tmp.path(), 1, min_time, false).unwrap();
824824-838838+825839 let ops = vec![
826840 create_test_operation("did:plc:test1", "cid1", "2024-01-01T00:00:01Z"),
827841 create_test_operation("did:plc:test2", "cid2", "2024-01-01T00:00:02Z"),
828842 ];
829843 mempool.add(ops).unwrap();
830830-844844+831845 let stats = mempool.stats();
832846 assert_eq!(stats.count, 2);
833847 assert!(!stats.can_create_bundle); // Need BUNDLE_SIZE (10000) ops
···844858 .unwrap()
845859 .with_timezone(&Utc);
846860 let mempool = Mempool::new(tmp.path(), 42, min_time, false).unwrap();
847847-861861+848862 let filename = mempool.get_filename();
849863 assert!(filename.contains("plc_mempool_"));
850864 assert!(filename.contains("000042"));
+42-25
src/operations.rs
···66/// PLC Operation
77///
88/// Represents a single operation from the PLC directory.
99-///
99+///
1010/// **IMPORTANT**: This struct uses `sonic_rs` for JSON parsing (not serde).
1111/// Serialization still uses serde for compatibility with JMESPath queries.
1212#[derive(Debug, Clone, Serialize, Deserialize)]
···39394040impl Operation {
4141 /// Parse an Operation from JSON using sonic_rs (not serde)
4242- ///
4242+ ///
4343 /// This method manually extracts fields from the JSON to avoid issues with
4444 /// serde attributes like `#[serde(flatten)]` that sonic_rs may not fully support.
4545 pub fn from_json(json: &str) -> anyhow::Result<Self> {
4646 use anyhow::Context;
4747 use sonic_rs::JsonValueTrait;
4848-4949- let value: Value = sonic_rs::from_str(json)
5050- .context("Failed to parse JSON")?;
5151-4848+4949+ let value: Value = sonic_rs::from_str(json).context("Failed to parse JSON")?;
5050+5251 // Extract required fields
5353- let did = value.get("did")
5252+ let did = value
5353+ .get("did")
5454 .and_then(|v| v.as_str())
5555 .ok_or_else(|| anyhow::anyhow!("missing 'did' field"))?
5656 .to_string();
5757-5858- let operation = value.get("operation")
5959- .cloned()
6060- .unwrap_or_else(Value::new);
6161-5757+5858+ let operation = value.get("operation").cloned().unwrap_or_else(Value::new);
5959+6260 // Extract optional fields with defaults
6363- let cid = value.get("cid")
6161+ let cid = value
6262+ .get("cid")
6463 .and_then(|v| v.as_str())
6564 .map(|s| s.to_string());
6666-6767- let nullified = value.get("nullified")
6565+6666+ let nullified = value
6767+ .get("nullified")
6868 .and_then(|v| v.as_bool())
6969 .unwrap_or(false);
7070-7070+7171 // Handle both "createdAt" and "created_at" field names
7272- let created_at = value.get("createdAt")
7272+ let created_at = value
7373+ .get("createdAt")
7374 .or_else(|| value.get("created_at"))
7475 .and_then(|v| v.as_str())
7576 .ok_or_else(|| anyhow::anyhow!("missing 'createdAt' or 'created_at' field"))?
7677 .to_string();
7777-7878+7879 // Extract extra fields (everything except known fields)
7980 // Since sonic_rs::Value doesn't provide easy iteration, we'll parse the JSON
8081 // and manually extract extra fields by checking for unknown keys
···8283 // The extra field was used with #[serde(flatten)] but we can reconstruct it if needed
8384 // For performance, we'll just use an empty Value since most operations don't have extra fields
8485 let extra = Value::new();
8585-8686+8687 Ok(Operation {
8788 did,
8889 operation,
···155156 assert_eq!(op.did, "did:plc:abcdefghijklmnopqrstuvwx");
156157 assert!(op.nullified);
157158 assert_eq!(op.created_at, "2024-01-01T12:34:56Z");
158158- assert_eq!(op.cid, Some("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi".to_string()));
159159-159159+ assert_eq!(
160160+ op.cid,
161161+ Some("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi".to_string())
162162+ );
163163+160164 // Check operation field
161165 let op_type = op.operation.get("type").and_then(|v| v.as_str()).unwrap();
162166 assert_eq!(op_type, "create");
···179183180184 let op1 = Operation::from_json(json1).unwrap();
181185 let op2 = Operation::from_json(json2).unwrap();
182182-186186+183187 assert_eq!(op1.created_at, "2024-01-01T00:00:00Z");
184188 assert_eq!(op2.created_at, "2024-01-01T00:00:00Z");
185189 }
···193197194198 let result = Operation::from_json(json);
195199 assert!(result.is_err());
196196- assert!(result.unwrap_err().to_string().contains("missing 'did' field"));
200200+ assert!(
201201+ result
202202+ .unwrap_err()
203203+ .to_string()
204204+ .contains("missing 'did' field")
205205+ );
197206 }
198207199208 #[test]
···205214206215 let result = Operation::from_json(json);
207216 assert!(result.is_err());
208208- assert!(result.unwrap_err().to_string().contains("missing 'createdAt' or 'created_at' field"));
217217+ assert!(
218218+ result
219219+ .unwrap_err()
220220+ .to_string()
221221+ .contains("missing 'createdAt' or 'created_at' field")
222222+ );
209223 }
210224211225 #[test]
···275289 assert_eq!(request.bundle, 1);
276290 assert_eq!(request.index, Some(5));
277291 assert!(request.filter.is_some());
278278- assert_eq!(request.filter.as_ref().unwrap().did, Some("did:plc:test".to_string()));
292292+ assert_eq!(
293293+ request.filter.as_ref().unwrap().did,
294294+ Some("did:plc:test".to_string())
295295+ );
279296 }
280297281298 #[test]
···205205206206 log::debug!("Fetching DID document from: {}", url);
207207 let request_start = Instant::now();
208208-208208+209209 let response = self
210210 .client
211211 .get(&url)
···215215 .context(format!("Failed to fetch DID document from {}", url))?;
216216217217 let request_duration = request_start.elapsed();
218218- log::debug!("HTTP request completed in {:?}, status: {}", request_duration, response.status());
218218+ log::debug!(
219219+ "HTTP request completed in {:?}, status: {}",
220220+ request_duration,
221221+ response.status()
222222+ );
219223220224 // Handle rate limiting (429)
221225 if response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
···226230 }
227231228232 if !response.status().is_success() {
229229- log::error!("Unexpected status code: {} for DID document at {}", response.status(), url);
233233+ log::error!(
234234+ "Unexpected status code: {} for DID document at {}",
235235+ response.status(),
236236+ url
237237+ );
230238 anyhow::bail!(
231239 "Unexpected status code: {} for DID document at {}",
232240 response.status(),
···247255 /// Uses the /did/{did} endpoint.
248256 pub async fn fetch_did_document(&self, did: &str) -> Result<DIDDocument> {
249257 let data = self.fetch_did_document_raw(did).await?;
250250- let document: DIDDocument = sonic_rs::from_str(&data)
251251- .context("Failed to parse DID document JSON")?;
258258+ let document: DIDDocument =
259259+ sonic_rs::from_str(&data).context("Failed to parse DID document JSON")?;
252260 Ok(document)
253261 }
254262}
···280288 // Default to 60 seconds if no header or parsing fails
281289 Duration::from_secs(MAX_RETRY_SECONDS)
282290}
283283-284291285292/// Simple token bucket rate limiter
286293/// Prevents burst requests by starting with 0 permits and refilling at steady rate
···347354 assert!(client.base_url.contains("plc.directory"));
348355 }
349356}
350350-
+85-19
src/processor.rs
···273273274274 // Handle "head~N" syntax
275275 if let Some(rest) = keyword.strip_prefix("head~") {
276276- let offset: u32 = rest.parse()
276276+ let offset: u32 = rest
277277+ .parse()
277278 .map_err(|_| anyhow::anyhow!("Invalid offset in 'head~{}': expected a number", rest))?;
278279 if offset >= max_bundle {
279279- anyhow::bail!("Offset {} in 'head~{}' exceeds maximum bundle {}", offset, rest, max_bundle);
280280+ anyhow::bail!(
281281+ "Offset {} in 'head~{}' exceeds maximum bundle {}",
282282+ offset,
283283+ rest,
284284+ max_bundle
285285+ );
280286 }
281287 return Ok(max_bundle - offset);
282288 }
283289284290 // Handle "~N" shorthand syntax
285291 if let Some(rest) = keyword.strip_prefix('~') {
286286- let offset: u32 = rest.parse()
292292+ let offset: u32 = rest
293293+ .parse()
287294 .map_err(|_| anyhow::anyhow!("Invalid offset in '~{}': expected a number", rest))?;
288295 if offset >= max_bundle {
289289- anyhow::bail!("Offset {} in '~{}' exceeds maximum bundle {}", offset, rest, max_bundle);
296296+ anyhow::bail!(
297297+ "Offset {} in '~{}' exceeds maximum bundle {}",
298298+ offset,
299299+ rest,
300300+ max_bundle
301301+ );
290302 }
291303 return Ok(max_bundle - offset);
292304 }
293305294306 // Not a keyword, try parsing as number
295295- let num: u32 = keyword.parse()
296296- .map_err(|_| anyhow::anyhow!("Invalid bundle specifier: '{}' (expected number, 'root', 'head', 'head~N', or '~N')", keyword))?;
307307+ let num: u32 = keyword.parse().map_err(|_| {
308308+ anyhow::anyhow!(
309309+ "Invalid bundle specifier: '{}' (expected number, 'root', 'head', 'head~N', or '~N')",
310310+ keyword
311311+ )
312312+ })?;
297313 Ok(num)
298314}
299315···328344 anyhow::bail!("Invalid range: {} > {} (start must be <= end)", start, end);
329345 }
330346 if start > max_bundle || end > max_bundle {
331331- anyhow::bail!("Invalid range: {}-{} (exceeds maximum bundle {})", start, end, max_bundle);
347347+ anyhow::bail!(
348348+ "Invalid range: {}-{} (exceeds maximum bundle {})",
349349+ start,
350350+ end,
351351+ max_bundle
352352+ );
332353 }
333354 bundles.extend(start..=end);
334355 } else {
···351372/// Operations are 0-indexed global positions (0 = first operation, bundle 1 position 0)
352373pub fn parse_operation_range(spec: &str, max_operation: u64) -> Result<Vec<u64>> {
353374 use anyhow::Context;
354354-375375+355376 if max_operation == 0 {
356377 anyhow::bail!("No operations available");
357378 }
···484505 let mut buffer = OutputBuffer::new(10);
485506 buffer.push("line1");
486507 buffer.push("line2");
487487-508508+488509 let flushed = buffer.flush();
489510 assert_eq!(flushed, "line1\nline2\n");
490511 assert!(buffer.is_empty());
···495516 let mut buffer = OutputBuffer::new(10);
496517 buffer.push("line1");
497518 buffer.push("line2");
498498-519519+499520 // Each line adds len + 1 (for newline)
500521 assert_eq!(buffer.get_matched_bytes(), 5 + 1 + 5 + 1);
501522 }
···534555 fn test_parse_bundle_range_empty_max() {
535556 let result = parse_bundle_range("1", 0);
536557 assert!(result.is_err());
537537- assert!(result.unwrap_err().to_string().contains("No bundles available"));
558558+ assert!(
559559+ result
560560+ .unwrap_err()
561561+ .to_string()
562562+ .contains("No bundles available")
563563+ );
538564 }
539565540566 #[test]
···548574 fn test_parse_bundle_range_invalid_range() {
549575 let result = parse_bundle_range("5-3", 10);
550576 assert!(result.is_err());
551551- assert!(result.unwrap_err().to_string().contains("start must be <= end"));
577577+ assert!(
578578+ result
579579+ .unwrap_err()
580580+ .to_string()
581581+ .contains("start must be <= end")
582582+ );
552583 }
553584554585 #[test]
555586 fn test_parse_bundle_range_invalid_format() {
556587 let result = parse_bundle_range("1-2-3", 10);
557588 assert!(result.is_err());
558558- assert!(result.unwrap_err().to_string().contains("Invalid range format"));
589589+ assert!(
590590+ result
591591+ .unwrap_err()
592592+ .to_string()
593593+ .contains("Invalid range format")
594594+ );
559595 }
560596561597 #[test]
···592628 fn test_parse_bundle_range_keyword_head_offset_invalid() {
593629 let result = parse_bundle_range("head~10", 10);
594630 assert!(result.is_err());
595595- assert!(result.unwrap_err().to_string().contains("exceeds maximum bundle"));
631631+ assert!(
632632+ result
633633+ .unwrap_err()
634634+ .to_string()
635635+ .contains("exceeds maximum bundle")
636636+ );
596637 }
597638598639 #[test]
599640 fn test_parse_bundle_range_keyword_shorthand_offset_invalid() {
600641 let result = parse_bundle_range("~10", 10);
601642 assert!(result.is_err());
602602- assert!(result.unwrap_err().to_string().contains("exceeds maximum bundle"));
643643+ assert!(
644644+ result
645645+ .unwrap_err()
646646+ .to_string()
647647+ .contains("exceeds maximum bundle")
648648+ );
603649 }
604650605651 #[test]
···643689 fn test_parse_operation_range_empty_max() {
644690 let result = parse_operation_range("0", 0);
645691 assert!(result.is_err());
646646- assert!(result.unwrap_err().to_string().contains("No operations available"));
692692+ assert!(
693693+ result
694694+ .unwrap_err()
695695+ .to_string()
696696+ .contains("No operations available")
697697+ );
647698 }
648699649700 #[test]
···657708 fn test_parse_operation_range_invalid_range() {
658709 let result = parse_operation_range("10-5", 1000);
659710 assert!(result.is_err());
660660- assert!(result.unwrap_err().to_string().contains("start must be <= end"));
711711+ assert!(
712712+ result
713713+ .unwrap_err()
714714+ .to_string()
715715+ .contains("start must be <= end")
716716+ );
661717 }
662718663719 #[test]
664720 fn test_parse_operation_range_invalid_format() {
665721 let result = parse_operation_range("1-2-3", 1000);
666722 assert!(result.is_err());
667667- assert!(result.unwrap_err().to_string().contains("Invalid range format"));
723723+ assert!(
724724+ result
725725+ .unwrap_err()
726726+ .to_string()
727727+ .contains("Invalid range format")
728728+ );
668729 }
669730670731 #[test]
671732 fn test_parse_operation_range_invalid_number() {
672733 let result = parse_operation_range("abc", 1000);
673734 assert!(result.is_err());
674674- assert!(result.unwrap_err().to_string().contains("Invalid operation number"));
735735+ assert!(
736736+ result
737737+ .unwrap_err()
738738+ .to_string()
739739+ .contains("Invalid operation number")
740740+ );
675741 }
676742677743 #[test]
+35-17
src/remote.rs
···2929 pub async fn fetch_index(&self) -> Result<Index> {
3030 // Try both common index file names
3131 let mut url = format!("{}/index.json", self.base_url);
3232-3232+3333 let response = self
3434 .client
3535 .get(&url)
···4848 .send()
4949 .await
5050 .context("Failed to fetch index")?;
5151-5151+5252 if !response2.status().is_success() {
5353 anyhow::bail!("Unexpected status code: {}", response2.status());
5454 }
5555-5555+5656 let data = response2.text().await?;
5757 let index: Index = sonic_rs::from_str(&data).context("Failed to parse index JSON")?;
5858 return Ok(index);
···155155 }
156156157157 let data = response.text().await?;
158158- let document: DIDDocument = sonic_rs::from_str(&data)
159159- .context("Failed to parse DID document JSON")?;
158158+ let document: DIDDocument =
159159+ sonic_rs::from_str(&data).context("Failed to parse DID document JSON")?;
160160161161 Ok(document)
162162 }
···227227 #[test]
228228 fn test_normalize_base_url() {
229229 // Test removing trailing slash
230230- assert_eq!(normalize_base_url("https://example.com/".to_string()), "https://example.com");
231231-230230+ assert_eq!(
231231+ normalize_base_url("https://example.com/".to_string()),
232232+ "https://example.com"
233233+ );
234234+232235 // Test removing index.json
233233- assert_eq!(normalize_base_url("https://example.com/index.json".to_string()), "https://example.com");
234234-236236+ assert_eq!(
237237+ normalize_base_url("https://example.com/index.json".to_string()),
238238+ "https://example.com"
239239+ );
240240+235241 // Test removing plc_bundles.json
236236- assert_eq!(normalize_base_url("https://example.com/plc_bundles.json".to_string()), "https://example.com");
237237-242242+ assert_eq!(
243243+ normalize_base_url("https://example.com/plc_bundles.json".to_string()),
244244+ "https://example.com"
245245+ );
246246+238247 // Test removing both trailing slash and index file
239239- assert_eq!(normalize_base_url("https://example.com/index.json/".to_string()), "https://example.com");
240240-248248+ assert_eq!(
249249+ normalize_base_url("https://example.com/index.json/".to_string()),
250250+ "https://example.com"
251251+ );
252252+241253 // Test already normalized URL
242242- assert_eq!(normalize_base_url("https://example.com".to_string()), "https://example.com");
243243-254254+ assert_eq!(
255255+ normalize_base_url("https://example.com".to_string()),
256256+ "https://example.com"
257257+ );
258258+244259 // Test with path
245245- assert_eq!(normalize_base_url("https://example.com/api/".to_string()), "https://example.com/api");
260260+ assert_eq!(
261261+ normalize_base_url("https://example.com/api/".to_string()),
262262+ "https://example.com/api"
263263+ );
246264 }
247265248266 #[test]
···256274 fn test_remote_client_new_normalizes_url() {
257275 let client = RemoteClient::new("https://example.com/").unwrap();
258276 assert!(!client.base_url.ends_with('/'));
259259-277277+260278 let client2 = RemoteClient::new("https://example.com/index.json").unwrap();
261279 assert!(!client2.base_url.ends_with("index.json"));
262280 }
+12-2
src/resolver.rs
···377377 fn test_validate_did_format_wrong_method() {
378378 let result = validate_did_format("did:web:example.com");
379379 assert!(result.is_err());
380380- assert!(result.unwrap_err().to_string().contains("invalid DID method"));
380380+ assert!(
381381+ result
382382+ .unwrap_err()
383383+ .to_string()
384384+ .contains("invalid DID method")
385385+ );
381386 }
382387383388 #[test]
···385390 // Too short
386391 let result = validate_did_format("did:plc:short");
387392 assert!(result.is_err());
388388- assert!(result.unwrap_err().to_string().contains("invalid DID length"));
393393+ assert!(
394394+ result
395395+ .unwrap_err()
396396+ .to_string()
397397+ .contains("invalid DID length")
398398+ );
389399390400 // Too long
391401 let result = validate_did_format("did:plc:abcdefghijklmnopqrstuvwxyz");
+15-10
src/runtime.rs
···11//! Graceful shutdown coordination for server and background tasks, with unified shutdown future and fatal-error handling
22// Runtime module - shutdown coordination for server and background tasks
33+use std::future::Future;
44+use std::sync::Arc;
55+use std::sync::atomic::{AtomicBool, Ordering};
36use tokio::signal;
47use tokio::sync::watch;
58use tokio::task::JoinSet;
66-use std::future::Future;
77-use std::sync::Arc;
88-use std::sync::atomic::{AtomicBool, Ordering};
991010/// Lightweight coordination for bundle operations shutdown and background tasks
1111/// Used by both server and sync continuous mode for graceful shutdown handling
···7777 }
78787979 /// Common shutdown cleanup handler for server and sync commands
8080- ///
8080+ ///
8181 /// This method handles the common pattern of:
8282 /// 1. Triggering shutdown (if not already triggered)
8383 /// 2. Aborting resolver tasks immediately (if any)
8484 /// 3. Handling background tasks based on shutdown type (fatal vs normal)
8585 /// 4. Printing completion message
8686- ///
8686+ ///
8787 /// # Arguments
8888 /// * `service_name` - Name of the service (e.g., "Server", "Sync") for messages
8989 /// * `resolver_tasks` - Optional resolver tasks to abort immediately
···9898 self.trigger_shutdown();
9999100100 // Always abort resolver tasks immediately - they're just keep-alive pings
101101- if let Some(resolver_tasks) = resolver_tasks && !resolver_tasks.is_empty() {
101101+ if let Some(resolver_tasks) = resolver_tasks
102102+ && !resolver_tasks.is_empty()
103103+ {
102104 resolver_tasks.abort_all();
103105 while let Some(result) = resolver_tasks.join_next().await {
104104- if let Err(e) = result && !e.is_cancelled() {
106106+ if let Err(e) = result
107107+ && !e.is_cancelled()
108108+ {
105109 eprintln!("Resolver task error: {}", e);
106110 }
107111 }
···115119 background_tasks.abort_all();
116120 // Wait briefly for aborted tasks to finish
117121 while let Some(result) = background_tasks.join_next().await {
118118- if let Err(e) = result && !e.is_cancelled() {
122122+ if let Err(e) = result
123123+ && !e.is_cancelled()
124124+ {
119125 eprintln!("Background task error: {}", e);
120126 }
121127 }
···145151#[cfg(test)]
146152mod tests {
147153 use super::*;
148148- use tokio::time::{sleep, Duration};
154154+ use tokio::time::{Duration, sleep};
149155150156 #[tokio::test]
151157 async fn test_programmatic_shutdown() {
···199205 assert!(*rx.borrow());
200206 }
201207}
202202-
+1-5
src/server/error.rs
···11// Error handling utilities and response helpers
2233-use axum::{
44- http::StatusCode,
55- response::IntoResponse,
66-};
33+use axum::{http::StatusCode, response::IntoResponse};
74use serde_json::json;
8596/// Helper to create a JSON error response
···4542 let msg = e.to_string();
4643 msg.contains("not found") || msg.contains("not in index")
4744}
4848-
···2828 assert!(raw.contains("did:plc:aaaaaaaaaaaaaaaaaaaaaaaa"));
29293030 // Build DID index so DID lookups work
3131- manager.batch_update_did_index_async(1, manager.get_last_bundle()).await?;
3131+ manager
3232+ .batch_update_did_index_async(1, manager.get_last_bundle())
3333+ .await?;
32343335 // Query DID operations and resolve DID
3436 let did = "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa";
···125127 common::setup_manager(&dir2_path)?;
126128 common::add_dummy_bundle(&dir2_path)?;
127129 let manager2 = plcbundle::BundleManager::new(dir2_path.clone(), ())?;
128128- manager2.batch_update_did_index_async(1, manager2.get_last_bundle()).await?;
130130+ manager2
131131+ .batch_update_did_index_async(1, manager2.get_last_bundle())
132132+ .await?;
129133130134 // Verify we can query DID operations from the newly built index
131135 let first_did = "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa";
+33-8
tests/server.rs
···5757 let manager = plcbundle::BundleManager::new(dir_path, ())?;
5858 let manager = Arc::new(manager);
5959 // Build DID index so the resolver can find operations in bundles
6060- manager.batch_update_did_index_async(1, manager.get_last_bundle()).await?;
6060+ manager
6161+ .batch_update_did_index_async(1, manager.get_last_bundle())
6262+ .await?;
6163 let port = 3032;
6264 let server_handle = common::start_test_server(Arc::clone(&manager), port).await?;
6365···66686769 // Test DID document endpoint (first DID from dummy bundle)
6870 let first_did = "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa";
6969- let res = client.get(format!("{}/{}", base_url, first_did)).send().await?;
7171+ let res = client
7272+ .get(format!("{}/{}", base_url, first_did))
7373+ .send()
7474+ .await?;
7075 let status = res.status();
7176 let header_x_request_type = res
7277 .headers()
···8893 assert_eq!(json["id"], first_did);
89949095 // Test DID data endpoint
9191- let res = client.get(format!("{}/did:plc:aaaaaaaaaaaaaaaaaaaaaaaa/data", base_url)).send().await?;
9696+ let res = client
9797+ .get(format!(
9898+ "{}/did:plc:aaaaaaaaaaaaaaaaaaaaaaaa/data",
9999+ base_url
100100+ ))
101101+ .send()
102102+ .await?;
92103 let status = res.status();
93104 let body_text = res.text().await?;
94105 if !status.is_success() {
···98109 assert_eq!(json["did"], "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa");
99110100111 // Test DID audit log endpoint
101101- let res = client.get(format!("{}/did:plc:aaaaaaaaaaaaaaaaaaaaaaaa/log/audit", base_url)).send().await?;
112112+ let res = client
113113+ .get(format!(
114114+ "{}/did:plc:aaaaaaaaaaaaaaaaaaaaaaaa/log/audit",
115115+ base_url
116116+ ))
117117+ .send()
118118+ .await?;
102119 let status = res.status();
103120 let body_text = res.text().await?;
104121 if !status.is_success() {
···107124 let json: serde_json::Value = serde_json::from_str(&body_text)?;
108125 assert!(json.is_array());
109126 assert!(!json.as_array().unwrap().is_empty());
110110- assert_eq!(json.as_array().unwrap()[0]["did"], "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa");
127127+ assert_eq!(
128128+ json.as_array().unwrap()[0]["did"],
129129+ "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa"
130130+ );
111131112132 server_handle.abort();
113133 Ok(())
···128148 let manager = plcbundle::BundleManager::new(dir_path, ())?;
129149 let manager = Arc::new(manager);
130150 // Ensure DID index is available for data/op lookups
131131- manager.batch_update_did_index_async(1, manager.get_last_bundle()).await?;
151151+ manager
152152+ .batch_update_did_index_async(1, manager.get_last_bundle())
153153+ .await?;
132154 let port = 3031;
133155 let server_handle = common::start_test_server(Arc::clone(&manager), port).await?;
134156···136158 let base_url = format!("http://127.0.0.1:{}", port);
137159138160 // Test index.json endpoint
139139- let res = client.get(format!("{}/index.json", base_url)).send().await?;
161161+ let res = client
162162+ .get(format!("{}/index.json", base_url))
163163+ .send()
164164+ .await?;
140165 println!("Testing endpoint: /index.json, status: {}", res.status());
141166 assert!(res.status().is_success());
142167 let json: serde_json::Value = res.json().await?;
···177202178203 server_handle.abort();
179204 Ok(())
180180-}205205+}