diff --git a/benchmarks/aft-search/slice-descriptors/land-views-f-lexical-routing.json b/benchmarks/aft-search/slice-descriptors/land-views-f-lexical-routing.json new file mode 100644 index 000000000..fb0f069b4 --- /dev/null +++ b/benchmarks/aft-search/slice-descriptors/land-views-f-lexical-routing.json @@ -0,0 +1,15 @@ +{ + "slice_class": "engine_unwired", + "targeted_mechanism": "none", + "kind": "harness", + "fixtures": [ + "crates/aft/tests/integration/tool_call_parity_test.rs", + "crates/aft/src/search_index.rs", + "crates/aft/src/commands/semantic_search/mod.rs", + "crates/aft/src/commands/semantic_search/exact_lane.rs" + ], + "slices": [ + "views-f-lexical-routing" + ], + "note": "Views-on lexical acquisition projects verified v3 payloads into the existing read-only query engine. Views are default-off, so the benchmark runs the views-off path, whose acquisition, scoring, fusion, admission and paging are unchanged; ranked rows must remain byte-identical to the committed reference." +} diff --git a/crates/aft/src/commands/configure.rs b/crates/aft/src/commands/configure.rs index 4f9167818..232c9bf47 100644 --- a/crates/aft/src/commands/configure.rs +++ b/crates/aft/src/commands/configure.rs @@ -4808,7 +4808,7 @@ fn handle_configure_inner( &canonical_cache_root, )); ctx.begin_configure_ack_phase("index_loading_state"); - let search_index = ctx.config().indexes.trigram; + let search_index = ctx.config().indexes.trigram && !ctx.config().views.enabled; let semantic_search = ctx.config().indexes.semantic; let mut search_index_cache_reused = false; @@ -5219,7 +5219,7 @@ fn semantic_status_is_not_opened(ctx: &AppContext) -> bool { fn missing_artifact_loads(ctx: &AppContext) -> ArtifactLoadNeeds { let config = ctx.config(); - let search_enabled = config.indexes.trigram; + let search_enabled = config.indexes.trigram && !config.views.enabled; let semantic_enabled = config.indexes.semantic; drop(config); let semantic_admitted = semantic_enabled && semantic_work_admitted(ctx); @@ -5355,6 +5355,9 @@ fn start_artifact_loads(starts: ArtifactLoadStarts) -> bool { /// dropped. Without the overlay, a read-only root serves the shared snapshot /// as-is. pub(crate) fn trigger_search_index_reload_if_evicted(ctx: &AppContext) -> bool { + if ctx.config().views.enabled { + return false; + } if ctx.canonical_cache_root_opt().is_none() || !ctx.search_index_query_reload_allowed() { return false; } @@ -5607,6 +5610,9 @@ fn schedule_artifact_loads_admitted( load_semantic: bool, semantic_view_work_allowed: bool, ) -> ArtifactLoadStarts { + // With views enabled, skip both loading another checkout's SearchIndex + // cache and building a new legacy search cache. + let load_search = load_search && !ctx.config().views.enabled; if let Some(reason) = ctx.cache_unavailable_reason() { let code = crate::root_cache::cache_status_code(&reason); *ctx.semantic_index_status() @@ -5700,6 +5706,7 @@ fn schedule_artifact_loads_admitted( let search_rx_terminal_guard = ctx.search_index_rx_terminal_guard(search_rx_epoch); let search_pending_install = ctx.search_pending_install(); let search_persist_epoch_flag = ctx.search_persist_epoch_flag(); + let search_publication_gate = ctx.legacy_search_publication_gate(); let (start_tx, start_rx) = crossbeam_channel::bounded::<()>(1); search_artifact_load_start = Some(start_tx); @@ -5707,6 +5714,7 @@ fn schedule_artifact_loads_admitted( mark_search_rebuild_spawn_for_debug(); thread::spawn(move || { let _terminal_guard = search_rx_terminal_guard; + let _publication = search_publication_gate.install(); if start_rx.recv().is_err() { #[cfg(test)] note_configure_artifact_load_cancellation_for_test(); @@ -5935,14 +5943,14 @@ fn schedule_artifact_loads_admitted( if search_persist_epoch_flag.current() != search_persist_epoch { return; } - let index = SearchIndex::rebuild_or_refresh_with_strategy( + let Some(index) = SearchIndex::build_with_limit_to_cache_dir_cancellable( &root_for_search, search_index_max_file_size, - current_head, - None, - Some(&cache_dir), - VerifyStrategy::StatFirst, - ); + &cache_dir, + &|| search_publication_gate.allowed(), + ) else { + return; + }; delay_search_rebuild_publish_for_debug(); index }; @@ -8527,10 +8535,17 @@ fn run_configure_maintenance_unit_inner( && ctx.config().views.enabled && !job.root_too_broad { - finish_configure_view_load( - ctx, - prepare_configure_view(&ConfigureViewInput::capture(ctx, job), job), - ); + if ctx.config().indexes.semantic || ctx.config().indexes.callgraph { + finish_configure_view_load( + ctx, + prepare_configure_view(&ConfigureViewInput::capture(ctx, job), job), + ); + } else { + ctx.clear_view_runtime(); + } + if let Some(Err(error)) = ctx.lexical_view_snapshot("bind") { + slog_warn!("lexical checkout view unavailable: {}", error); + } } else { ctx.clear_view_runtime(); } @@ -15274,6 +15289,433 @@ mod tests { })) } + fn configure_lexical_views(root: &Path, storage: &Path) -> RawRequest { + configure_request_with_params(json!({ + "project_root": root, "harness": "opencode", "storage_dir": storage, + "config": [user_tier(json!({ + "views": { "enabled": true }, + "indexes": { "trigram": true, "semantic": false, "callgraph": false } + }))] + })) + } + + fn lexical_views_oracle(root: &Path) -> TestContext { + let oracle = test_context(); + oracle.update_config(|config| { + config.project_root = Some(root.to_path_buf()); + config.views.enabled = false; + config.indexes.trigram = true; + config.indexes.semantic = false; + config.indexes.callgraph = false; + }); + oracle.set_canonical_cache_root(root.to_path_buf()); + *oracle.search_index().write().unwrap() = Some(SearchIndex::build_with_limit_serial( + root, + oracle.config().search_index_max_file_size, + )); + oracle + } + + fn lexical_views_assert_public_parity(ctx: &AppContext, root: &Path) { + let oracle = lexical_views_oracle(root); + for (command, params, field) in [ + ( + "grep", + json!({"pattern": "TOKEN|needle", "include": "**/*.ts"}), + "matches", + ), + ("glob", json!({"pattern": "**/*.ts"}), "files"), + ( + "semantic_search", + json!({"query": "OLDER_COMMIT_TOKEN"}), + "results", + ), + ( + "semantic_search", + json!({"query": "needle alpha"}), + "results", + ), + ( + "semantic_search", + json!({"query": "unfindable_token"}), + "results", + ), + ( + "semantic_search", + json!({"pattern": "TOKEN|needle"}), + "results", + ), + ( + "semantic_search", + json!({"query": "needle", "pattern": "TOKEN|needle"}), + "results", + ), + ] { + let request = RawRequest { + id: format!("lexical-views-{command}"), + command: command.into(), + lsp_hints: None, + session_id: None, + params, + }; + let run = |ctx: &AppContext| match command { + "grep" => crate::commands::grep::handle_grep(&request, ctx), + "glob" => crate::commands::glob::handle_glob(&request, ctx), + _ => crate::commands::semantic_search::handle_semantic_search(&request, ctx), + }; + let expected = run(&oracle); + let actual = run(ctx); + assert!(expected.success, "oracle {command}: {:?}", expected.data); + assert!(actual.success, "view {command}: {:?}", actual.data); + assert!( + expected.data[field].is_array(), + "{command}: {:?}", + expected.data + ); + assert_eq!( + actual.data[field], expected.data[field], + "{command} {field}" + ); + assert_eq!(actual.data["text"], expected.data["text"], "{command} text"); + } + assert!(ctx + .lexical_view_route_counts() + .iter() + .all(|count| *count > 0)); + assert_eq!(oracle.lexical_view_route_counts(), [0; 3]); + } + + fn lexical_views_public_checkout(kind: OlderCheckoutKind) { + let temp = tempfile::tempdir().unwrap(); + let (owner, root, storage) = older_checkout_beside_live_owner(temp.path(), kind); + let root = std::fs::canonicalize(root).unwrap(); + let cache = crate::search_index::resolve_cache_dir_with_key( + &owner.memoized_artifact_cache_key(&owner.canonical_cache_root()), + Some(&storage), + ); + let owner_cache = std::fs::read(cache.join("cache.bin")).unwrap(); + let builds = crate::write_ledger::register( + crate::write_ledger::Domain::SearchIndexBuild, + root.display().to_string(), + ); + let deltas = crate::write_ledger::register( + crate::write_ledger::Domain::SearchIndexDelta, + root.display().to_string(), + ); + let before = ( + builds.physical_total_for_test(), + deltas.physical_total_for_test(), + ); + let ctx = test_context(); + let bound = handle_configure_for_test(&configure_lexical_views(&root, &storage), &ctx); + assert!(bound.success, "{:?}", bound.data); + assert!(ctx.search_index().read().unwrap().is_none()); + assert!(ctx.search_index_rx().read().unwrap().is_none()); + lexical_views_assert_public_parity(&ctx, &root); + + std::fs::write( + root.join("shared.ts"), + "export const OLDER_COMMIT_TOKEN = 4;\nneedle alpha\n", + ) + .unwrap(); + std::fs::write(root.join("new.ts"), "needle alpha beta\n").unwrap(); + std::fs::write(root.join(".gitignore"), "new.ts\n").unwrap(); + ctx.record_checkout_watcher_change(&root.join("shared.ts")); + lexical_views_assert_public_parity(&ctx, &root); + let stamp = filetime::FileTime::from_last_modification_time( + &std::fs::metadata(root.join("shared.ts")).unwrap(), + ); + std::fs::write( + root.join("shared.ts"), + "export const OLDER_COMMIT_TOKEN = 5;\nneedle alpha\n", + ) + .unwrap(); + filetime::set_file_mtime(root.join("shared.ts"), stamp).unwrap(); + lexical_views_assert_public_parity(&ctx, &root); + std::fs::remove_file(root.join("shared.ts")).unwrap(); + std::fs::write(root.join(".gitignore"), "").unwrap(); + lexical_views_assert_public_parity(&ctx, &root); + std::fs::rename(root.join("new.ts"), root.join("moved.ts")).unwrap(); + lexical_views_assert_public_parity(&ctx, &root); + ctx.record_checkout_watcher_overflow(); + std::fs::write(root.join("moved.ts"), "needle alpha after overflow\n").unwrap(); + lexical_views_assert_public_parity(&ctx, &root); + + crate::runtime_drain::spawn_search_corpus_refresh(&ctx, root.clone(), ctx.config()); + crate::runtime_drain::refresh_search_index_after_write(&ctx, &root.join("moved.ts")); + assert!(ctx.search_index_rx().read().unwrap().is_none()); + ctx.flush_search_index_on_graceful_shutdown(); + assert_eq!( + ( + builds.physical_total_for_test(), + deltas.physical_total_for_test() + ), + before + ); + // The owner fixture already wrote a cache. A routed checkout must not + // replace it with its own content, even after refresh and shutdown. + assert_eq!(std::fs::read(cache.join("cache.bin")).unwrap(), owner_cache); + assert!(!storage.join("blobs/v2").exists()); + ctx.mark_subc_unbound(); + ctx.cancel_unbound_artifact_work(); + drop(ctx); + let restarted = test_context(); + assert!( + handle_configure_for_test(&configure_lexical_views(&root, &storage), &restarted) + .success + ); + lexical_views_assert_public_parity(&restarted, &root); + assert_eq!(std::fs::read(cache.join("cache.bin")).unwrap(), owner_cache); + } + + #[test] + fn lexical_views_public_linked_worktree_parity_edits_restart_and_writer_stop() { + lexical_views_public_checkout(OlderCheckoutKind::LinkedWorktree); + } + + #[test] + fn lexical_views_public_plain_clone_parity_edits_restart_and_writer_stop() { + lexical_views_public_checkout(OlderCheckoutKind::PlainClone); + } + + #[test] + fn lexical_views_public_empty_generation_and_live_only_addition_match_legacy() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("project"); + let storage = temp.path().join("storage"); + std::fs::create_dir_all(&root).unwrap(); + let ctx = test_context(); + assert!(handle_configure_for_test(&configure_lexical_views(&root, &storage), &ctx).success); + lexical_views_assert_public_parity(&ctx, &root); + std::fs::write( + root.join("new.ts"), + "export const OLDER_COMMIT_TOKEN = 1;\nneedle alpha\n", + ) + .unwrap(); + lexical_views_assert_public_parity(&ctx, &root); + assert!(!storage.join("blobs/v2").exists()); + } + + #[test] + fn lexical_views_public_late_legacy_completion_and_views_off_boundary() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("project"); + init_git_fixture(&root); + let storage = temp.path().join("storage"); + let ctx = test_context(); + assert!(handle_configure_for_test(&configure_with_storage(&root, &storage), &ctx).success); + wait_for_search_index_ready(&ctx, Duration::from_secs(10)); + assert_eq!(ctx.lexical_view_route_counts(), [0; 3]); + let floor = std::fs::read(storage.join("reader-floor.json")).ok(); + assert!(crate::commands::grep::handle_grep(&grep_request("needle"), &ctx).success); + let off_glob: RawRequest = serde_json::from_value(json!({ + "id": "lexical-off-glob", "command": "glob", "pattern": "**/*" + })) + .unwrap(); + assert!(crate::commands::glob::handle_glob(&off_glob, &ctx).success); + assert!( + crate::commands::semantic_search::handle_semantic_search( + &semantic_search_request("needle"), + &ctx + ) + .success + ); + assert_eq!(std::fs::read(storage.join("reader-floor.json")).ok(), floor); + assert_eq!(ctx.lexical_view_route_counts(), [0; 3]); + assert!(!storage.join("blobs/v3").exists()); + let stale = + SearchIndex::build_with_limit_serial(&root, ctx.config().search_index_max_file_size); + std::fs::write( + root.join("shared.ts"), + "export const OLDER_COMMIT_TOKEN = 7;\nneedle alpha\n", + ) + .unwrap(); + assert!(handle_configure_for_test(&configure_lexical_views(&root, &storage), &ctx).success); + let (tx, rx) = crossbeam_channel::unbounded(); + ctx.install_search_index_rx(rx, ctx.configure_generation()); + tx.send(stale).unwrap(); + crate::runtime_drain::drain_search_index_events(&ctx); + assert!(ctx.search_index().read().unwrap().is_none()); + assert!(ctx.search_index_rx().read().unwrap().is_none()); + lexical_views_assert_public_parity(&ctx, &root); + assert!(!storage.join("blobs/v2").exists()); + } + + #[test] + fn lexical_views_public_acquisition_failure_never_uses_legacy_index() { + struct Refused; + impl crate::views::trigram::LexicalSnapshotSource for Refused { + fn acquire(&self) -> Result { + Err("lexical source deliberately refused".into()) + } + fn record_change(&self, _: &Path) {} + } + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("project"); + init_git_fixture(&root); + std::fs::write(root.join("visible.ts"), "needle\n").unwrap(); + let ctx = test_context(); + assert!( + handle_configure_for_test( + &configure_lexical_views(&root, &temp.path().join("storage")), + &ctx + ) + .success + ); + ctx.install_lexical_source_for_test(Arc::new(Refused)); + *ctx.search_index().write().unwrap() = Some(SearchIndex::empty_ready_for_root(&root)); + let grep = crate::commands::grep::handle_grep(&grep_request("needle"), &ctx); + assert!( + !grep.success && grep.data.to_string().contains("deliberately refused"), + "{:?}", + grep.data + ); + let search = crate::commands::semantic_search::handle_semantic_search( + &semantic_search_request("needle"), + &ctx, + ); + assert!( + !search.success && search.data.to_string().contains("deliberately refused"), + "{:?}", + search.data + ); + let glob = crate::commands::glob::handle_glob( + &RawRequest { + id: "lexical-refused-glob".into(), + command: "glob".into(), + lsp_hints: None, + session_id: None, + params: json!({"pattern":"**/*.ts"}), + }, + &ctx, + ); + assert!( + glob.success && glob.data["text"].as_str().unwrap().contains("visible.ts"), + "{:?}", + glob.data + ); + assert_eq!(glob.data["fallback"], "filesystem"); + assert_eq!(ctx.lexical_view_route_counts(), [0; 3]); + } + + #[test] + fn lexical_views_public_search_pins_one_snapshot_for_all_lexical_lanes() { + struct Counted { + source: Arc, + calls: std::sync::atomic::AtomicUsize, + } + impl crate::views::trigram::LexicalSnapshotSource for Counted { + fn acquire(&self) -> Result { + self.calls.fetch_add(1, Ordering::SeqCst); + crate::views::trigram::LexicalSnapshotSource::acquire(self.source.as_ref()) + } + fn record_change(&self, path: &Path) { + crate::views::trigram::LexicalSnapshotSource::record_change( + self.source.as_ref(), + path, + ); + } + } + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("project"); + init_git_fixture(&root); + std::fs::write( + root.join("shared.ts"), + "export const OLDER_COMMIT_TOKEN = 1;\n", + ) + .unwrap(); + let ctx = test_context(); + assert!( + handle_configure_for_test( + &configure_lexical_views(&root, &temp.path().join("storage")), + &ctx + ) + .success + ); + let initialized = ctx.lexical_view_snapshot("bind").unwrap(); + assert!(initialized.is_ok(), "{:?}", initialized.err()); + let source = ctx.lexical_source_for_test(); + let counted = Arc::new(Counted { + source, + calls: Default::default(), + }); + ctx.install_lexical_source_for_test(counted.clone()); + let response = crate::commands::semantic_search::handle_semantic_search( + &semantic_search_request("OLDER_COMMIT_TOKEN"), + &ctx, + ); + assert!(response.success, "{:?}", response.data); + assert!(!response.data["results"].as_array().unwrap().is_empty()); + assert_eq!(counted.calls.load(Ordering::SeqCst), 1); + assert_eq!(ctx.lexical_view_route_counts(), [0, 0, 1]); + } + + #[test] + fn lexical_views_public_kill_child() { + let Some(root) = std::env::var_os("AFT_LEXICAL_KILL_ROOT") else { + return; + }; + let storage = PathBuf::from(std::env::var_os("AFT_LEXICAL_KILL_STORAGE").unwrap()); + let root = PathBuf::from(root); + let ctx = test_context(); + assert!(handle_configure_for_test(&configure_lexical_views(&root, &storage), &ctx).success); + lexical_views_assert_public_parity(&ctx, &root); + println!("LEXICAL_VIEW_PUBLISHED"); + use std::io::{Read, Write}; + std::io::stdout().flush().unwrap(); + let _ = std::io::stdin().read(&mut [0u8; 1]); + } + + #[test] + fn lexical_views_public_forced_kill_reopens_current_tree() { + use std::io::BufRead; + use std::process::Stdio; + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("project"); + init_git_fixture(&root); + std::fs::write( + root.join("shared.ts"), + "export const OLDER_COMMIT_TOKEN = 1;\nneedle alpha\n", + ) + .unwrap(); + let root = std::fs::canonicalize(root).unwrap(); + let storage = temp.path().join("storage"); + let mut child = Command::new(std::env::current_exe().unwrap()) + .args([ + "--exact", + "commands::configure::tests::lexical_views_public_kill_child", + "--nocapture", + ]) + .env("AFT_LEXICAL_KILL_ROOT", &root) + .env("AFT_LEXICAL_KILL_STORAGE", &storage) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + let output = child.stdout.take().unwrap(); + let mut published = false; + for line in std::io::BufReader::new(output).lines() { + if line.unwrap().contains("LEXICAL_VIEW_PUBLISHED") { + published = true; + break; + } + } + if !published { + let status = child.wait().unwrap(); + panic!("child exited before publication handshake: {status}"); + } + child.kill().unwrap(); + assert!(!child.wait().unwrap().success()); + std::fs::write( + root.join("shared.ts"), + "export const OLDER_COMMIT_TOKEN = 2;\nneedle alpha changed\n", + ) + .unwrap(); + let ctx = test_context(); + assert!(handle_configure_for_test(&configure_lexical_views(&root, &storage), &ctx).success); + lexical_views_assert_public_parity(&ctx, &root); + } + fn configure_search_with_max_file_size( root: &std::path::Path, storage: &std::path::Path, diff --git a/crates/aft/src/commands/glob.rs b/crates/aft/src/commands/glob.rs index 8f7b82e05..bc9e36fcb 100644 --- a/crates/aft/src/commands/glob.rs +++ b/crates/aft/src/commands/glob.rs @@ -38,6 +38,8 @@ const MAX_DIRECTORY_SECTIONS: usize = 8; const MAX_DISPLAY_DIRECTORIES: usize = 6; pub fn handle_glob(req: &RawRequest, ctx: &AppContext) -> Response { + let config = ctx.config(); + let _pin = config.views.enabled.then(|| ctx.pin_config_to(config)); ctx.with_read_policy_snapshot(&req.id, || { crate::context::confinement::with_floor_disclosure(|| { crate::bounded_io::command(req, ctx, handle_glob_inner) @@ -590,7 +592,14 @@ fn glob_root( } let search_root_text = search_root.to_string_lossy(); let search_scope = resolve_search_scope(project_root, Some(search_root_text.as_ref())); - let indexed_snapshot = { + let view_snapshot = (search_scope.use_index && !ignored_target) + .then(|| ctx.lexical_view_snapshot("glob")) + .flatten(); + let indexed_snapshot = if let Some(snapshot) = view_snapshot { + // Failed view acquisition uses the bounded current-tree walk, never a + // borrowed legacy snapshot that may describe another checkout. + snapshot.ok() + } else { let search_index = ctx .search_index() .read() diff --git a/crates/aft/src/commands/grep.rs b/crates/aft/src/commands/grep.rs index 286a8bbd3..5b3ce6b75 100644 --- a/crates/aft/src/commands/grep.rs +++ b/crates/aft/src/commands/grep.rs @@ -133,6 +133,8 @@ pub(crate) mod work_probe { } pub fn handle_grep(req: &RawRequest, ctx: &AppContext) -> Response { + let config = ctx.config(); + let _pin = config.views.enabled.then(|| ctx.pin_config_to(config)); ctx.with_read_policy_snapshot(&req.id, || { crate::context::confinement::with_floor_disclosure(|| { crate::bounded_io::command(req, ctx, handle_grep_inner) diff --git a/crates/aft/src/commands/semantic_search/exact_lane.rs b/crates/aft/src/commands/semantic_search/exact_lane.rs index a9a063c3b..13a839c17 100644 --- a/crates/aft/src/commands/semantic_search/exact_lane.rs +++ b/crates/aft/src/commands/semantic_search/exact_lane.rs @@ -521,7 +521,36 @@ impl ExactLane { fallback_options: Option<&FallbackExactOptions>, scope: &ExactEvidenceScope, ) -> Result { - let is_ready = index.is_some_and(SearchIndex::is_ready); + let snapshot = index + .filter(|index| index.is_ready()) + .map(SearchIndex::snapshot); + self.search_scoped_snapshot( + snapshot.as_ref(), + project_root, + snapshot_generation, + query, + include_tests, + offset, + top_k, + fallback_options, + scope, + ) + } + + /// Accepts an optional SearchIndexSnapshot without requiring a mutable index. + pub(crate) fn search_scoped_snapshot( + &self, + snapshot: Option<&crate::search_index::SearchIndexSnapshot>, + project_root: &Path, + snapshot_generation: GenerationToken, + query: &str, + include_tests: bool, + offset: usize, + top_k: usize, + fallback_options: Option<&FallbackExactOptions>, + scope: &ExactEvidenceScope, + ) -> Result { + let is_ready = snapshot.is_some(); // Index loading can finish without changing snapshot_generation. Give // fallback results a separate memo key so they cannot be reused once // the full index is ready. @@ -538,9 +567,8 @@ impl ExactLane { self.memo.get_or_verify(&key, offset, top_k, || { if is_ready { - let snapshot = index.unwrap().snapshot(); Ok(self.execute_ready_mode_scoped( - &snapshot, + snapshot.unwrap(), project_root, query, include_tests, diff --git a/crates/aft/src/commands/semantic_search/mod.rs b/crates/aft/src/commands/semantic_search/mod.rs index 7ee71f76e..785e61d0b 100644 --- a/crates/aft/src/commands/semantic_search/mod.rs +++ b/crates/aft/src/commands/semantic_search/mod.rs @@ -581,7 +581,22 @@ impl extensions::ReadinessSource for RuntimeReadinessSource<'_> { }, }; - let trigram = + let trigram = if let Some(snapshot) = self.ctx.lexical_view_snapshot("aft_search") { + match snapshot { + Ok(snapshot) => TrigramReadiness { + status: IndexStatus::Ready, + snapshot: Some(Arc::new(snapshot)), + evicted: false, + lock_contended: false, + }, + Err(_) => TrigramReadiness { + status: IndexStatus::Fallback, + snapshot: None, + evicted: false, + lock_contended: false, + }, + } + } else { match try_read_with_budget(self.ctx.search_index(), INTERACTIVE_ARTIFACT_READ_BUDGET) { Some(index) if index.as_ref().is_some_and(SearchIndex::is_ready) => { TrigramReadiness { @@ -629,7 +644,8 @@ impl extensions::ReadinessSource for RuntimeReadinessSource<'_> { evicted: false, lock_contended: true, }, - }; + } + }; let symbol_cache = self.ctx.symbol_cache(); let symbol = match try_read_with_budget(&symbol_cache, INTERACTIVE_ARTIFACT_READ_BUDGET) { @@ -661,6 +677,9 @@ impl extensions::ReadinessSource for RuntimeReadinessSource<'_> { } fn bounded_first_search_wait(&self) -> extensions::ReadinessWait { + if self.ctx.lexical_views_enabled() { + return extensions::ReadinessWait::Completed; + } // A writer-held pointer cannot be advanced by the loader drain below; // preserve the interactive contention bound instead of sleeping 2.5s. if matches!( @@ -791,6 +810,8 @@ fn handle_semantic_search_with_disk_fallback( ctx: &AppContext, allow_disk_fallback: bool, ) -> Response { + let config = ctx.config(); + let _pin = config.views.enabled.then(|| ctx.pin_config_to(config)); use extensions::{RawQuery, Root, Token}; let page_request = match paging::parse_public_page_request(&req.params) { @@ -926,6 +947,11 @@ fn handle_semantic_search_with_disk_fallback( // The extensions come from the search_b2 install point: A-side defaults // until campaign B2 installs its router, plans, variants and readiness. let extensions = crate::search_b2::install_defaults(); + if external_root.is_none() { + if let Some(Err(error)) = ctx.lexical_view_snapshot("aft_search") { + return Response::error(&req.id, "internal_error", error); + } + } if let Some((pattern_text, compiled)) = pattern { return handle_pattern_request( req, @@ -3994,7 +4020,12 @@ fn regex_route_collection( compiled: &pattern_compile::CompiledPattern, include_tests: bool, ) -> Option> { - let snapshot = { + let snapshot = if let Some(snapshot) = ctx.lexical_view_snapshot("aft_search") { + match snapshot { + Ok(snapshot) => snapshot, + Err(error) => return Some(Err(std::io::Error::other(error))), + } + } else { #[cfg(test)] regex_route_lock_hook(false); let Some(guard) = @@ -4836,8 +4867,12 @@ fn run_engine_ranking( return Err("request_cancelled".to_string()); } } - let context_index = borrowed_index - .is_none() + let view_snapshot = if borrowed_index.is_none() { + ctx.lexical_view_snapshot("aft_search").transpose()? + } else { + None + }; + let context_index = (borrowed_index.is_none() && view_snapshot.is_none()) .then(|| try_read_with_budget(ctx.search_index(), INTERACTIVE_ARTIFACT_READ_BUDGET)); let empty_index = SearchIndex::new(); let index = borrowed_index @@ -4849,8 +4884,10 @@ fn run_engine_ranking( .and_then(|guard| guard.as_ref()) }) .unwrap_or(&empty_index); - let generation = borrowed_index - .map(|(_, generation)| generation.clone()) + let generation = view_snapshot + .as_ref() + .map(|snapshot| GenerationToken::new_with_str(&snapshot.content_generation())) + .or_else(|| borrowed_index.map(|(_, generation)| generation.clone())) .unwrap_or_else(|| { GenerationToken::new_with_str(&format!( "{}:{}", @@ -4858,7 +4895,8 @@ fn run_engine_ranking( ctx.semantic_index_rx_generation() )) }); - let snapshot = index.snapshot(); + let index_ready = view_snapshot.is_some() || index.is_ready(); + let snapshot = view_snapshot.unwrap_or_else(|| index.snapshot()); let content_tokens = query_shape::extract_content_tokens(query); let token_refs = content_tokens .iter() @@ -4944,8 +4982,8 @@ fn run_engine_ranking( let mut exact_candidates = if plan.contains(SearchLaneKind::Exact) || plan.shape == SearchShape::Identifier { let outcome = exact_lane::ExactLane::with_memo(ctx.search_exact_memo()) - .search_scoped( - Some(&index), + .search_scoped_snapshot( + index_ready.then_some(&snapshot), project_root, generation.clone(), exact_input, @@ -5012,8 +5050,8 @@ fn run_engine_ranking( for (fact_input, exact_form) in fact_inputs { // Only phrase evidence is kept from these passes. let outcome = lane - .search_scoped( - Some(&index), + .search_scoped_snapshot( + index_ready.then_some(&snapshot), project_root, generation.clone(), &fact_input, @@ -5082,35 +5120,35 @@ fn run_engine_ranking( && exact_candidates.is_empty() && exact_disclosures.is_empty() && index_disclosures.is_empty() - && index.is_ready()) - .then(|| nearest_names::looked_up_identifiers(exact_input)) - .flatten() - .filter(|identifiers| { - // Absence is checked over every indexed file, tests included: a name - // that only a test uses still exists, and the ordinary ranking says - // more about it than a not-found answer would. - identifiers.iter().all(|identifier| { - snapshot - .whole_corpus_exact_pass(identifier, project_root, None) - .map(|matches| matches.is_empty()) - .unwrap_or_else(|error| { - index_disclosures.push(error.to_string()); - false - }) + && index_ready) + .then(|| nearest_names::looked_up_identifiers(exact_input)) + .flatten() + .filter(|identifiers| { + // Absence is checked over every indexed file, tests included: a name + // that only a test uses still exists, and the ordinary ranking says + // more about it than a not-found answer would. + identifiers.iter().all(|identifier| { + snapshot + .whole_corpus_exact_pass(identifier, project_root, None) + .map(|matches| matches.is_empty()) + .unwrap_or_else(|error| { + index_disclosures.push(error.to_string()); + false + }) + }) }) - }) - .map(|identifiers| { - let target = identifiers.last().expect("at least one identifier"); - nearest_names::NotFoundAnswer { - query: exact_lane::exact_phrase(exact_input).to_string(), - names: nearest_names::nearest_names( - target, - lexical_candidates - .iter() - .map(|candidate| candidate.path.as_path()), - ), - } - }); + .map(|identifiers| { + let target = identifiers.last().expect("at least one identifier"); + nearest_names::NotFoundAnswer { + query: exact_lane::exact_phrase(exact_input).to_string(), + names: nearest_names::nearest_names( + target, + lexical_candidates + .iter() + .map(|candidate| candidate.path.as_path()), + ), + } + }); // A code literal that no indexed file contains (tests included) gets the // same kind of answer: the reply says so first, and the files that only // share its words are labelled similar rather than presented as matches. @@ -5125,26 +5163,26 @@ fn run_engine_ranking( let literal_absent = (not_found.is_none() && plan.shape == SearchShape::CodeLiteral && exact_disclosures.is_empty() - && index.is_ready()) - .then(|| exact_lane::exact_phrase(exact_input)) - .filter(|phrase| phrase.len() >= 3) - .filter(|phrase| { - match literal_presence( - ctx, - &snapshot, - project_root, - &generation, - phrase, - &exact_candidates, - ) { - Ok(presence) => presence == memo::LiteralPresence::Absent, - Err(error) => { - index_disclosures.push(error.to_string()); - false + && index_ready) + .then(|| exact_lane::exact_phrase(exact_input)) + .filter(|phrase| phrase.len() >= 3) + .filter(|phrase| { + match literal_presence( + ctx, + &snapshot, + project_root, + &generation, + phrase, + &exact_candidates, + ) { + Ok(presence) => presence == memo::LiteralPresence::Absent, + Err(error) => { + index_disclosures.push(error.to_string()); + false + } } - } - }) - .map(str::to_string); + }) + .map(str::to_string); let path_lookup_candidates = if plan.contains(SearchLaneKind::PathLookup) { let query_path_tokens = query @@ -7377,6 +7415,9 @@ fn search_index_ready_with_budget( ctx: &AppContext, wait_budget: Duration, ) -> Result { + if let Some(snapshot) = ctx.lexical_view_snapshot("aft_search") { + return Ok(snapshot.is_ok()); + } let deadline = Instant::now() + wait_budget; loop { if search_cancellation_requested() { diff --git a/crates/aft/src/context.rs b/crates/aft/src/context.rs index 715f94599..a8c9d47d7 100644 --- a/crates/aft/src/context.rs +++ b/crates/aft/src/context.rs @@ -294,6 +294,8 @@ thread_local! { thread_local! { static CHECKOUT_QUERY_OUTCOMES: std::cell::RefCell)>> = const { std::cell::RefCell::new(Vec::new()) }; + static LEXICAL_QUERY_SNAPSHOTS: std::cell::RefCell)>> = + const { std::cell::RefCell::new(Vec::new()) }; } #[cfg(test)] @@ -384,6 +386,8 @@ pub struct ConfigPinGuard { impl Drop for ConfigPinGuard { fn drop(&mut self) { if let Some(key) = self.key { + LEXICAL_QUERY_SNAPSHOTS + .with(|snapshots| snapshots.borrow_mut().retain(|(id, _)| *id != key)); CHECKOUT_QUERY_OUTCOMES.with(|outcomes| { outcomes .borrow_mut() @@ -4549,6 +4553,8 @@ pub struct AppContext { #[cfg(test)] view_publication_attempt_starts: parking_lot::Mutex>, checkout_driver: crate::views::first_load::InstalledDriver, + lexical_view: parking_lot::Mutex>>, + lexical_routes: [AtomicU64; 3], /// The views-on semantic lane of this root (see `views::semantic_runtime`). checkout_semantic: Arc, /// Wall-clock milliseconds at which the completion drain should next @@ -4606,6 +4612,8 @@ pub struct AppContext { // probe again after repeated load disconnects. search_index_disconnect_reschedule: parking_lot::Mutex<(u64, u32, Option)>, search_persist_epoch: crate::root_cache::ArtifactPublishEpoch, + legacy_search_publication: + parking_lot::Mutex>, pub(crate) search_corpus_refresh_lane: Arc, pending_search_index_paths: parking_lot::Mutex>, symbol_cache: SharedSymbolCache, @@ -5396,6 +5404,8 @@ impl AppContext { #[cfg(test)] view_publication_attempt_starts: parking_lot::Mutex::new(Vec::new()), checkout_driver: crate::views::first_load::InstalledDriver::default(), + lexical_view: parking_lot::Mutex::new(None), + lexical_routes: Default::default(), checkout_semantic: Arc::default(), view_disk_limits_next_ms, checkout_query_runtime: RwLock::new(None), @@ -5430,6 +5440,7 @@ impl AppContext { search_pending_install: Arc::default(), search_index_disconnect_reschedule: parking_lot::Mutex::new((0, 0, None)), search_persist_epoch: crate::root_cache::ArtifactPublishEpoch::default(), + legacy_search_publication: Default::default(), search_corpus_refresh_lane: Arc::default(), pending_search_index_paths: parking_lot::Mutex::new(BTreeSet::new()), symbol_cache, @@ -8950,6 +8961,11 @@ impl AppContext { return None; }; let mut next = next; + if next.views.enabled { + self.legacy_search_publication.lock().revoke(); + } else if guard.views.enabled { + *self.legacy_search_publication.lock() = Arc::default(); + } next.aft_search_registered = next.tool_registered("aft_search"); #[cfg(test)] let next = self.app.isolate_test_config(next); @@ -8978,6 +8994,9 @@ impl AppContext { }); self.reconcile_rerank_backend(&next); self.arm_view_disk_limits(next.views.enabled); + if !next.views.enabled || project_root_changed { + self.lexical_view.lock().take(); + } if project_root_changed { self.path_restriction_root_memo.lock().take(); *self @@ -10512,6 +10531,9 @@ impl AppContext { pub(crate) fn record_checkout_watcher_change(&self, path: &Path) { self.reset_view_publication_retry(); + if let Some(view) = self.lexical_view.lock().as_ref() { + view.record_change(path); + } if let Some(driver) = self.checkout_driver.get() { driver.record_absolute_change(path); } @@ -10522,6 +10544,9 @@ impl AppContext { pub(crate) fn record_checkout_watcher_overflow(&self) { self.reset_view_publication_retry(); + if let Some(view) = self.lexical_view.lock().as_ref() { + view.record_overflow(); + } if let Some(driver) = self.checkout_driver.get() { driver.record_watcher_overflow(); } @@ -10530,6 +10555,9 @@ impl AppContext { pub(crate) fn record_checkout_watcher_cookie(&self, path: &Path) { self.record_checkout_pending_watcher_gap(); + if let Some(view) = self.lexical_view.lock().as_ref() { + view.record_cookie(path); + } if let Some(driver) = self.checkout_driver.get() { driver.record_watcher_cookie(path); } @@ -11921,6 +11949,118 @@ impl AppContext { &self.search_index } + pub(crate) fn legacy_search_publication_gate( + &self, + ) -> Arc { + self.legacy_search_publication.lock().clone() + } + + pub(crate) fn lexical_views_enabled(&self) -> bool { + let config = self.config(); + config.views.enabled + && config.indexes.trigram + && config.project_root.is_some() + && self.heavy_root_work_allowed() + && self.cache_unavailable_reason().is_none() + } + + pub(crate) fn lexical_view_is_ready(&self) -> bool { + self.lexical_view + .lock() + .as_ref() + .is_some_and(|view| view.is_ready()) + } + + #[cfg(test)] + pub(crate) fn lexical_source_for_test( + &self, + ) -> Arc { + self.lexical_view.lock().as_ref().unwrap().source_for_test() + } + + #[cfg(test)] + pub(crate) fn install_lexical_source_for_test( + &self, + source: Arc, + ) { + let root = self.canonical_cache_root(); + let family = self.memoized_artifact_cache_key(&root); + *self.lexical_view.lock() = + Some(Arc::new(crate::views::trigram::LexicalView::with_source( + root, + self.storage_paths(), + family, + crate::blob_store::v2::TrigramPolicy { + max_file_size: self.config().search_index_max_file_size, + }, + source, + ))); + } + + /// With views enabled, lexical queries use the checkout-local view rather + /// than the legacy SearchIndex cache. The separate lexical driver keeps + /// lexical service independent of semantic models. + pub(crate) fn lexical_view_snapshot( + &self, + tool: &str, + ) -> Option> { + if !self.lexical_views_enabled() { + return None; + } + if let Some(snapshot) = LEXICAL_QUERY_SNAPSHOTS.with(|snapshots| { + snapshots + .borrow() + .iter() + .find(|(id, _)| *id == self.config_pin_key()) + .map(|(_, snapshot)| snapshot.clone()) + }) { + return Some(snapshot); + } + let config = self.config(); + let root = self.canonical_cache_root(); + let storage = self.storage_paths(); + let policy = crate::blob_store::v2::TrigramPolicy { + max_file_size: config.search_index_max_file_size, + }; + let family = self.memoized_artifact_cache_key(&root); + let view = { + let mut slot = self.lexical_view.lock(); + if !slot + .as_ref() + .is_some_and(|view| view.matches(&root, &storage, &family, policy)) + { + match crate::views::trigram::LexicalView::new(root, storage, family, policy) { + Ok(view) => *slot = Some(Arc::new(view)), + Err(error) => return Some(Err(error)), + } + } + slot.as_ref().unwrap().clone() + }; + self.record_checkout_pending_watcher_gap(); + let result = view.snapshot(); + if pinned_config_for(self.config_pin_key()).is_some() { + LEXICAL_QUERY_SNAPSHOTS.with(|snapshots| { + snapshots + .borrow_mut() + .push((self.config_pin_key(), result.clone())) + }); + } + if result.is_ok() && tool != "bind" { + let index = match tool { + "grep" => 0, + "glob" => 1, + _ => 2, + }; + self.lexical_routes[index].fetch_add(1, Ordering::Relaxed); + } + Some(result) + } + + /// Executed v3 lexical acquisitions, ordered as grep, glob, aft_search. + pub fn lexical_view_route_counts(&self) -> [u64; 3] { + std::array::from_fn(|index| self.lexical_routes[index].load(Ordering::Relaxed)) + } + pub(crate) fn search_exact_memo( &self, ) -> Arc { @@ -12364,6 +12504,10 @@ impl AppContext { } fn search_index_shutdown_outcome(&self) -> SearchIndexShutdownOutcome { + if self.config().views.enabled { + return SearchIndexShutdownOutcome::Skipped; + } + let _publication = self.legacy_search_publication_gate().install(); use SearchIndexShutdownOutcome::{Failed, Flushed, Skipped}; if self.cache_unavailable_reason().is_some() { return Failed("cache root is unavailable"); diff --git a/crates/aft/src/feature_status.rs b/crates/aft/src/feature_status.rs index 64e001439..0a2f255dc 100644 --- a/crates/aft/src/feature_status.rs +++ b/crates/aft/src/feature_status.rs @@ -271,6 +271,13 @@ pub fn storage_refusal( // index is being replaced or updated, which reports as building. fn observe_trigram(ctx: &AppContext) -> IndexObservation { + if ctx.lexical_views_enabled() { + return if ctx.lexical_view_is_ready() { + IndexObservation::ready() + } else { + IndexObservation::building() + }; + } let resident = match ctx.search_index().try_read() { Ok(guard) => guard.as_ref().map(|index| index.ready), Err(std::sync::TryLockError::Poisoned(poisoned)) => { diff --git a/crates/aft/src/grep_executor.rs b/crates/aft/src/grep_executor.rs index fa4f043df..080e42bce 100644 --- a/crates/aft/src/grep_executor.rs +++ b/crates/aft/src/grep_executor.rs @@ -836,7 +836,20 @@ fn execute_root_profiled( let snapshot_started = Instant::now(); let ignored_target = root.ignored_target; let mut snapshot_timed_out = false; - let indexed_snapshot = + let view_snapshot = (root.use_index && !ignored_target) + .then(|| ctx.lexical_view_snapshot("grep")) + .flatten(); + let indexed_snapshot = if let Some(snapshot) = view_snapshot { + match snapshot { + Ok(snapshot) => Some(snapshot), + Err(error) => { + return ( + failed_index_grep(std::io::Error::other(error)), + GrepExecutionPhaseTimings::default(), + ) + } + } + } else { match try_read_with_budget(ctx.search_index(), INTERACTIVE_ARTIFACT_READ_BUDGET) { Some(search_index) => match search_index.as_ref() { Some(index) if index.ready && root.use_index && !ignored_target => { @@ -848,7 +861,8 @@ fn execute_root_profiled( snapshot_timed_out = true; None } - }; + } + }; let snapshot_acquire = snapshot_started.elapsed(); let mut index_lacks_scope = ignored_target; if let Some(snapshot) = indexed_snapshot { diff --git a/crates/aft/src/runtime_drain.rs b/crates/aft/src/runtime_drain.rs index 8dbac6988..2f400c001 100644 --- a/crates/aft/src/runtime_drain.rs +++ b/crates/aft/src/runtime_drain.rs @@ -814,6 +814,10 @@ pub fn mark_semantic_corpus_refresh_success(ctx: &AppContext) { } pub fn drain_search_index_events(ctx: &AppContext) { + if ctx.config().views.enabled { + ctx.retire_search_index_rx("lexical views replace the legacy search loader"); + return; + } let (latest, disconnected, receiver_generation, receiver_epoch) = { let rx_ref = ctx .search_index_rx() @@ -2378,6 +2382,9 @@ fn spawn_search_corpus_refresh_admitted( config: Arc, generation: u64, ) { + if config.views.enabled || ctx.config().views.enabled { + return; + } { let mut search_index = ctx .search_index() @@ -2404,12 +2411,14 @@ fn spawn_search_corpus_refresh_admitted( let content_generation_flag = ctx.configure_content_generation_flag(); let persist_epoch_flag = ctx.search_persist_epoch_flag(); let persist_epoch = ctx.next_search_persist_epoch(); + let publication_gate = ctx.legacy_search_publication_gate(); let lifecycle = ctx.subc_lifecycle_admission(); let cold_build_limiter = ctx.cold_build_limiter(); ctx.search_corpus_refresh_lane.submit( root.clone(), Box::new(move |cancel| { let _terminal_guard = receiver_terminal_guard; + let _publication = publication_gate.install(); log_ctx::with_session(session_id, || { let Some(_permit) = crate::cold_build_limiter::acquire_blocking_while_with_limiter( &cold_build_limiter, @@ -2446,6 +2455,7 @@ fn spawn_search_corpus_refresh_admitted( }; let keep_going = || { !cancel.load(std::sync::atomic::Ordering::Acquire) + && publication_gate.allowed() && lifecycle.is_current(&generation_flag, generation) && persist_epoch_flag.current() == persist_epoch }; @@ -2533,7 +2543,7 @@ pub fn refresh_project_corpus(ctx: &AppContext, reason: &str, ignore_rules_chang } } - if config.indexes.trigram && !ctx.shared_artifacts_read_only() { + if config.indexes.trigram && !config.views.enabled && !ctx.shared_artifacts_read_only() { spawn_search_corpus_refresh_admitted(ctx, root.clone(), config.clone(), generation); status_changed = true; aft::slog_info!("{}", search_refresh_started_log(reason)); @@ -2923,6 +2933,9 @@ fn next_watcher_apply_phase(stage: WatcherDrainApplyPhase) -> WatcherDrainApplyP } fn refresh_search_index_path(ctx: &AppContext, path: &Path) { + if ctx.config().views.enabled { + return; + } let max_file_size = ctx .search_index() .read() @@ -3118,6 +3131,9 @@ impl Drop for FileMutationGuard<'_> { /// read-after-write ordering. Preparation reads at most the index's file-size /// limit, just as it does for a watcher update. pub(crate) fn refresh_search_index_after_write(ctx: &AppContext, path: &Path) { + if ctx.config().views.enabled { + return; + } if !ctx.heavy_root_work_allowed() || (ctx.shared_artifacts_read_only() && !ctx.ram_overlay_active()) { diff --git a/crates/aft/src/search_index.rs b/crates/aft/src/search_index.rs index e96cafd0c..d4d9d2aff 100644 --- a/crates/aft/src/search_index.rs +++ b/crates/aft/src/search_index.rs @@ -394,6 +394,86 @@ fn artifact_write_allowed(project_root: &Path, cache_dir: &Path, write_path: &Pa && SearchIndex::check_disk_cache_format(cache_dir).is_ok() } +/// Revokes only legacy search publication when a checkout switches to views. +/// Shared holders cover the final rename, not the build or serialization, so +/// ordinary legacy publications never wait for one another. +pub(crate) struct LegacySearchPublicationGate { + allowed: std::sync::atomic::AtomicBool, + publication: parking_lot::RwLock<()>, + #[cfg(test)] + before_publish: parking_lot::Mutex>>, +} + +impl Default for LegacySearchPublicationGate { + fn default() -> Self { + Self { + allowed: std::sync::atomic::AtomicBool::new(true), + publication: parking_lot::RwLock::new(()), + #[cfg(test)] + before_publish: Default::default(), + } + } +} + +thread_local! { + static LEGACY_SEARCH_PUBLICATION: std::cell::RefCell>> = const { std::cell::RefCell::new(None) }; +} + +pub(crate) struct LegacySearchPublicationScope { + previous: Option>, + _not_send: std::marker::PhantomData<*const ()>, +} + +impl Drop for LegacySearchPublicationScope { + fn drop(&mut self) { + LEGACY_SEARCH_PUBLICATION.with(|slot| slot.replace(self.previous.take())); + } +} + +impl LegacySearchPublicationGate { + pub(crate) fn allowed(&self) -> bool { + self.allowed.load(std::sync::atomic::Ordering::Acquire) + } + + pub(crate) fn revoke(&self) { + let _publication = self.publication.write(); + self.allowed + .store(false, std::sync::atomic::Ordering::Release); + } + + pub(crate) fn install(self: &Arc) -> LegacySearchPublicationScope { + LegacySearchPublicationScope { + previous: LEGACY_SEARCH_PUBLICATION.with(|slot| slot.replace(Some(self.clone()))), + _not_send: std::marker::PhantomData, + } + } + + fn publish(&self, operation: impl FnOnce() -> std::io::Result<()>) -> std::io::Result<()> { + #[cfg(test)] + if let Some(hook) = self.before_publish.lock().take() { + hook(); + } + let _publication = self.publication.read(); + if !self.allowed() { + return Err(std::io::Error::new( + std::io::ErrorKind::Interrupted, + "legacy search publication revoked by views", + )); + } + operation() + } +} + +fn publish_legacy_search_cache( + operation: impl FnOnce() -> std::io::Result<()>, +) -> std::io::Result<()> { + let gate = LEGACY_SEARCH_PUBLICATION.with(|slot| slot.borrow().clone()); + match gate { + Some(gate) => gate.publish(operation), + None => operation(), + } +} + #[derive(Clone, Debug)] pub struct SearchIndex { base: Option>, @@ -742,6 +822,61 @@ impl SearchIndex { } impl SearchIndexSnapshot { + pub(crate) fn content_generation(&self) -> String { + let mut digest = blake3::Hasher::new(); + digest.update(&self.max_file_size.to_be_bytes()); + for file in self.files.iter() { + let path = file.path.as_os_str().as_encoded_bytes(); + digest.update(&(path.len() as u64).to_be_bytes()); + digest.update(path); + digest.update(file.content_hash.as_bytes()); + } + digest.finalize().to_hex().to_string() + } + + /// Reuses the query engine over already verified, in-memory posting rows. + /// This is a read projection, not a mutable index: it has no cache file or + /// persistence path. Missing postings require direct verification by grep. + pub(crate) fn from_verified_rows( + root: &Path, + max_file_size: u64, + rows: impl IntoIterator>)>, + ) -> Self { + let mut files = Vec::new(); + let mut paths = HashMap::new(); + let mut counts = Vec::new(); + let mut unindexed = HashSet::new(); + let mut delta = DeltaState::default(); + for (file, records) in rows { + let id = u32::try_from(files.len()).expect("view file id exceeds u32"); + paths.insert(file.path.clone(), id); + files.push(file); + counts.push(records.as_ref().map_or(0, |records| records.len() as u32)); + if let Some(records) = records { + for (trigram, next_mask, loc_mask) in records { + delta.postings.entry(trigram).or_default().push(Posting { + file_id: id, + next_mask, + loc_mask, + }); + } + } else { + unindexed.insert(id); + } + } + Self { + base: None, + delta: Arc::new(delta), + files: Arc::new(files), + path_to_id: Arc::new(paths), + ready: true, + project_root: root.to_path_buf(), + max_file_size, + file_trigram_count: Arc::new(counts), + unindexed_files: Arc::new(unindexed), + } + } + pub(crate) fn file_modified(&self, path: &Path) -> Option { self.path_to_id .get(path) @@ -3059,6 +3194,7 @@ impl SearchIndex { ) } + #[cfg(test)] pub(crate) fn rebuild_or_refresh_with_strategy( root: &Path, max_file_size: u64, @@ -5309,14 +5445,18 @@ fn write_cache_file_from_sources( drop(writer); check_search_build(keep_going)?; - fs::rename(&tmp_cache, &cache_path)?; - // Credit the bytes handed to the kernel; this cache is not a durable log. - crate::write_ledger::credit( - domain, - plan.project_root.display().to_string(), - file_bytes, - file_bytes, - ); + publish_legacy_search_cache(|| { + fs::rename(&tmp_cache, &cache_path)?; + // Account inside the publication fence so a completed views bind + // cannot be followed by credit for an older legacy publication. + crate::write_ledger::credit( + domain, + plan.project_root.display().to_string(), + file_bytes, + file_bytes, + ); + Ok(()) + })?; let file = open_cache_file_read(&cache_path)?; Ok(BasePostings { file: Arc::new(file), @@ -10905,6 +11045,84 @@ pub(crate) fn audit_work() -> AuditWork { #[cfg(test)] mod tests { + fn lexical_views_paused_legacy_publication(turn_views_on: bool) { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("project"); + let cache = temp.path().join("cache"); + fs::create_dir_all(&root).unwrap(); + fs::write(root.join("a.rs"), "old_marker\n").unwrap(); + let mut initial = SearchIndex::build_with_limit_serial(&root, 1024); + assert!(initial.write_to_disk(&cache, None)); + let original = fs::read(cache.join("cache.bin")).unwrap(); + fs::write(root.join("a.rs"), "new_marker\n").unwrap(); + let context = crate::context::AppContext::new( + Box::new(crate::parser::TreeSitterProvider::new()), + crate::config::Config { + project_root: Some(root.clone()), + ..Default::default() + }, + ); + let gate = context.legacy_search_publication_gate(); + let (ready_tx, ready_rx) = std::sync::mpsc::channel(); + let (release_tx, release_rx) = std::sync::mpsc::channel(); + *gate.before_publish.lock() = Some(Box::new(move || { + ready_tx.send(()).unwrap(); + release_rx.recv().unwrap(); + })); + let build_root = root.clone(); + let build_cache = cache.clone(); + let build_gate = gate.clone(); + let build = std::thread::spawn(move || { + let _publication = build_gate.install(); + SearchIndex::build_with_limit_to_cache_dir_cancellable( + &build_root, + 1024, + &build_cache, + &|| true, + ) + }); + ready_rx.recv_timeout(Duration::from_secs(30)).unwrap(); + if turn_views_on { + context.update_config(|config| config.views.enabled = true); + assert!(!gate.allowed()); + } else { + context.update_config(|config| config.search_index_max_file_size += 1); + context.advance_configure_generation(); + context.mark_subc_unbound(); + assert!(gate.allowed()); + } + release_tx.send(()).unwrap(); + let result = build.join().unwrap(); + if turn_views_on { + assert!(result.is_none()); + assert_eq!(fs::read(cache.join("cache.bin")).unwrap(), original); + context.update_config(|config| config.views.enabled = false); + assert!(context.legacy_search_publication_gate().allowed()); + assert!( + !gate.allowed(), + "old workers stay revoked after views are disabled" + ); + } else { + assert!(result.is_some_and(|index| index.is_ready())); + assert_ne!(fs::read(cache.join("cache.bin")).unwrap(), original); + } + assert!(fs::read_dir(&cache).unwrap().all(|entry| !entry + .unwrap() + .file_name() + .to_string_lossy() + .starts_with("cache.bin.tmp."))); + } + + #[test] + fn lexical_views_cutover_revokes_admitted_legacy_cache_rename() { + lexical_views_paused_legacy_publication(true); + } + + #[test] + fn lexical_views_off_preserves_admitted_publication_after_reconfigure_and_unbind() { + lexical_views_paused_legacy_publication(false); + } + struct IsolatedLegacyMemoIndexes { indexes: Arc>, previous: Option>>, diff --git a/crates/aft/src/views/trigram.rs b/crates/aft/src/views/trigram.rs index f178485dd..5765c01f3 100644 --- a/crates/aft/src/views/trigram.rs +++ b/crates/aft/src/views/trigram.rs @@ -1,10 +1,11 @@ //! Opt-in trigram views: shared segment, generation overlay, and live delta. //! -//! No adapter is registered in `contracts::process_hooks` by this module; -//! existing search commands keep using `SearchIndex` by default. +//! Public lexical queries select a checkout-local v3 reader only with views on. +//! Views off retains the legacy `SearchIndex` route. //! -//! Runtime call sequence: obtain current bytes with `live_delta::strict_walk`, -//! project those entries into the complete v2 manifest, call `materialize` +//! `LexicalView` acquires v3 snapshots through `CheckoutDriver::verified_bind`. +//! The lower-level segment API also supports v2 fixtures: obtain current bytes +//! with `live_delta::strict_walk`, project a complete v2 manifest, call `materialize` //! while keeping its returned pin alive, materialize the other planes, and //! publish with the core generation CAS. Open the verified pinned generation //! with `TrigramAdapter::open_generation`. Supply `TrigramDriver` with a @@ -26,6 +27,385 @@ use super::segment_store::{rel_path_to_os, SegmentReader, TrigramFlag, TrigramPa use super::snapshot::{DiskState, Snapshot, Source, WatcherState}; use super::RelPath; +/// Checkout-local lexical authority, independent of the semantic worker's +/// lifetime and model availability. Queries serialize reconciliation so an +/// installed membership and its payloads always come from the same snapshot. +pub(crate) struct LexicalView { + pub(crate) root: PathBuf, + storage: crate::root_cache::StoragePaths, + family: String, + policy: TrigramPolicy, + source: Arc, + query: std::sync::Mutex<()>, + ready: std::sync::atomic::AtomicBool, +} + +/// Supplies a verified checkout snapshot without exposing its storage layout +/// to the lexical query engine. Implementations must never publish v1/v2 data. +pub(crate) trait LexicalSnapshotSource: Send + Sync { + fn acquire(&self) -> Result; + fn record_change(&self, path: &Path); + fn record_overflow(&self) {} + fn record_cookie(&self, _: &Path) {} +} + +struct CheckoutLexicalSource { + driver: Arc, + access: super::contracts::ViewAccess, + storage: PathBuf, + family: String, + scope: String, + producers: super::manifest_v2::Producers, +} + +impl LexicalSnapshotSource for CheckoutLexicalSource { + fn acquire(&self) -> Result { + use super::first_load::FirstLoadDriver; + let installed = self.driver.installed_snapshot(); + let seed = if installed.generation().name() == "empty" { + match super::base_refs::open_foreign_v3( + &self.storage, + &self.family, + &self.scope, + &self.producers, + ) { + Ok(Some(generation)) => generation, + Ok(None) => installed.generation().clone(), + Err(error) => { + crate::slog_debug!( + "lexical v3 seed unavailable; reconciling current tree: {}", + error + ); + installed.generation().clone() + } + } + } else { + installed.generation().clone() + }; + let (snapshot, revision) = self + .driver + .verified_bind(&self.access, seed) + .map_err(|error| error.to_string())?; + self.driver + .install(&self.access, &snapshot, revision) + .map_err(|error| error.to_string())?; + Ok(self.driver.installed_snapshot()) + } + + fn record_change(&self, path: &Path) { + self.driver.record_absolute_change(path); + } + + fn record_overflow(&self) { + self.driver.record_watcher_overflow(); + } + fn record_cookie(&self, path: &Path) { + self.driver.record_watcher_cookie(path); + } +} + +impl LexicalView { + pub(crate) fn matches( + &self, + root: &Path, + storage: &crate::root_cache::StoragePaths, + family: &str, + policy: TrigramPolicy, + ) -> bool { + self.root == root + && self.storage == *storage + && self.family == family + && self.policy == policy + } + + pub(crate) fn new( + root: PathBuf, + storage: crate::root_cache::StoragePaths, + family: String, + policy: TrigramPolicy, + ) -> Result { + let scope = crate::path_identity::project_scope_key(&root); + let producers = super::manifest_v2::Producers { + trigram: policy.fingerprint_hex(), + semantic: None, + callgraph: super::semantic_runtime::UNREGISTERED_PRODUCER.into(), + }; + let bridge = Arc::new(super::first_load::TrigramBridge::new( + storage.artifacts.clone(), + policy, + )); + let adapter: Arc = bridge.adapter.clone(); + let driver = Arc::new( + super::first_load::CheckoutDriver::new_v3( + &storage.artifacts, + &family, + &scope, + &root, + producers.clone(), + Arc::new(super::first_load::ConfiguredMembershipWalker), + vec![bridge], + ) + .map_err(|error| error.to_string())? + .with_adapters(vec![adapter]), + ); + driver.register_write_intent(); + let source = Arc::new(CheckoutLexicalSource { + access: driver.access(), + driver, + storage: storage.artifacts.clone(), + family: family.clone(), + scope, + producers, + }); + Ok(Self::with_source(root, storage, family, policy, source)) + } + + pub(crate) fn with_source( + root: PathBuf, + storage: crate::root_cache::StoragePaths, + family: String, + policy: TrigramPolicy, + source: Arc, + ) -> Self { + Self { + root, + storage, + family, + policy, + source, + query: std::sync::Mutex::new(()), + ready: std::sync::atomic::AtomicBool::new(false), + } + } + + pub(crate) fn record_change(&self, path: &Path) { + self.ready + .store(false, std::sync::atomic::Ordering::Release); + self.source.record_change(path); + } + + pub(crate) fn record_overflow(&self) { + self.ready + .store(false, std::sync::atomic::Ordering::Release); + self.source.record_overflow(); + } + + pub(crate) fn record_cookie(&self, path: &Path) { + self.source.record_cookie(path); + } + + pub(crate) fn is_ready(&self) -> bool { + self.ready.load(std::sync::atomic::Ordering::Acquire) + } + + #[cfg(test)] + pub(crate) fn source_for_test(&self) -> Arc { + self.source.clone() + } + + /// Keep acquisition separate from the read projection so a source that + /// verifies a saved generation can supply the same query snapshot. + fn acquire(&self) -> Result { + self.source.acquire() + } + + pub(crate) fn snapshot(&self) -> Result { + let result = self.project(); + self.ready + .store(result.is_ok(), std::sync::atomic::Ordering::Release); + result + } + + fn project(&self) -> Result { + use super::segment_store::PayloadSource; + let _query = self + .query + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let snapshot = self.acquire()?; + if snapshot.pending_intent().next().is_some() || snapshot.watcher() != WatcherState::Healthy + { + return Err("lexical view has unverified writes".into()); + } + let mut source = None; + let mut rows = Vec::new(); + for (path, disk) in snapshot.membership() { + let DiskState::Present { content, size } = disk else { + continue; + }; + let absolute = self + .root + .join(rel_path_to_os(&path).map_err(|error| error.to_string())?); + let key = crate::blob_store::v2::TrigramKey { + content, + policy: self.policy, + } + .family_key(); + let payload = match snapshot.source(&path) { + Source::Live(entry) => entry + .attachments + .get(&FamilyPlane::Trigram) + .and_then(|value| value.downcast_ref::()) + .filter(|attachment| { + attachment.content == content + && attachment.policy == self.policy.fingerprint() + }) + .map(|attachment| attachment.payload.clone()), + _ => None, + }; + let payload = match payload { + Some(payload) => payload, + None => { + if snapshot.plane_readiness(&path, FamilyPlane::Trigram) + != Some(&super::readiness::PlaneState::ready(&key)) + { + return Err(format!("lexical payload not ready: {}", absolute.display())); + } + if source.is_none() { + source = Some( + crate::blob_store::v3::PlaneReader::open_existing( + &self.storage.artifacts, + &self.family, + FamilyPlane::Trigram, + ) + .map_err(|error| error.to_string())?, + ); + } + let bytes = source + .as_ref() + .unwrap() + .payload(&key) + .map_err(|error| error.to_string())? + .ok_or_else(|| { + format!("lexical payload missing: {}", absolute.display()) + })?; + TrigramPayload::decode(&bytes).map_err(|error| error.to_string())? + } + }; + let metadata = fs::metadata(&absolute).map_err(|error| error.to_string())?; + rows.push(( + crate::search_index::FileEntry { + path: absolute, + size, + modified: metadata.modified().map_err(|error| error.to_string())?, + content_hash: blake3::Hash::from_bytes(*content.as_bytes()), + }, + (payload.flag == TrigramFlag::Indexed).then(|| { + payload + .records + .into_iter() + .map(|record| (record.trigram, record.next_mask, record.loc_mask)) + .collect() + }), + )); + } + Ok( + crate::search_index::SearchIndexSnapshot::from_verified_rows( + &self.root, + self.policy.max_file_size, + rows, + ), + ) + } +} + +#[cfg(test)] +pub(crate) struct BaseBuilderTestSource { + root: PathBuf, + storage: PathBuf, + family: String, + policy: TrigramPolicy, + snapshots: std::sync::Mutex>, +} + +#[cfg(test)] +impl BaseBuilderTestSource { + pub(crate) fn new( + root: &Path, + storage: &Path, + family: &str, + policy: TrigramPolicy, + ) -> Arc { + Arc::new(Self { + root: root.into(), + storage: storage.into(), + family: family.into(), + policy, + snapshots: Default::default(), + }) + } +} + +#[cfg(test)] +impl LexicalSnapshotSource for BaseBuilderTestSource { + fn acquire(&self) -> Result { + let walk = strict_walk(&self.root, &self.policy, 0); + if !walk.gaps.is_empty() { + return Err(format!("strict test walk gaps: {:?}", walk.gaps)); + } + let mut hash = blake3::Hasher::new(); + for (path, entry) in &walk.entries { + hash.update(&(path.as_bytes().len() as u64).to_be_bytes()); + hash.update(path.as_bytes()); + if let DiskState::Present { content, .. } = entry.disk { + hash.update(content.as_bytes()); + } + } + let key = *hash.finalize().as_bytes(); + let mut snapshots = self.snapshots.lock().unwrap(); + if let Some((snapshot, _)) = snapshots.get(&key) { + return Ok(snapshot.clone()); + } + let paths = walk.entries.into_keys().collect::>(); + let built = super::base_builder::build_base( + super::base_builder::BaseBuildInput { + storage: &self.storage, + family: &self.family, + root: &self.root, + paths: &paths, + policy: self.policy, + semantic_producer: None, + callgraph_producer: super::callgraph::PRODUCER.into(), + ignore_fingerprint: None, + history: None, + budget: super::eviction::DiskBudget::default(), + }, + None, + ) + .map_err(|error| error.to_string())?; + let super::base_builder::BaseBuild::Published { + identity, claim, .. + } = built + else { + return Err(format!("test base build refused: {built:?}")); + }; + let base = super::base_refs::resolve_base(&self.storage, &self.family, &identity.scope) + .map_err(|error| error.to_string())?; + let worker = super::manifest_v2::ManifestV2::new_v3( + base.manifest().header().clone(), + Some(base.header()), + super::manifest_v2::PublishedFrontier::default(), + ); + let composed = super::snapshot::ComposedGeneration::compose( + "lexical-test-worker", + worker, + base.into_generation(&self.storage, &self.family), + None, + ) + .map_err(|error| error.to_string())?; + let composed = composed.with_v3_route(super::snapshot::V3Route { + storage: self.storage.clone(), + family: self.family.clone(), + scope: "lexical-test-worker".into(), + }); + let snapshot = super::snapshot::LiveDelta::new(Arc::new(composed)).snapshot(); + snapshots.insert(key, (snapshot.clone(), claim)); + Ok(snapshot) + } + + fn record_change(&self, _: &Path) {} +} + /// Generation-local replacements and tombstones relative to the shared segment. /// Missing payloads are gaps, not empty postings. #[derive(Debug)] @@ -826,6 +1206,20 @@ fn put_v3_payloads( type Residents = BTreeMap<(String, String), Arc>; +struct V3Payloads(Option); + +impl super::segment_store::PayloadSource for V3Payloads { + fn payload( + &self, + key: &crate::blob_store::v2::FamilyKey, + ) -> Result>, crate::blob_store::v2::StoreError> { + match &self.0 { + Some(source) => super::segment_store::PayloadSource::payload(source, key), + None => Ok(None), + } + } +} + /// Resident immutable data shared across checkout opens. Register explicitly /// with `PlaneHooks::register_adapter`; construction alone changes no routing. pub struct TrigramAdapter { @@ -842,6 +1236,33 @@ thread_local! { } impl TrigramAdapter { + /// When the generation and its base have no content, a plane DB is optional. + /// Live-only additions still supply their own attachments to the reader. + fn v3_payloads( + &self, + route: &super::snapshot::V3Route, + snapshot: &Snapshot, + ) -> Result { + if !snapshot + .generation() + .entries() + .any(|(_, entry)| entry.content().is_some()) + && !snapshot + .generation() + .base() + .is_some_and(|base| base.entries().any(|(_, entry)| entry.content().is_some())) + { + return Ok(V3Payloads(None)); + } + crate::blob_store::v3::PlaneReader::open_existing( + &route.storage, + &route.family, + FamilyPlane::Trigram, + ) + .map(|source| V3Payloads(Some(source))) + .map_err(|error| plane_error(error.to_string())) + } + pub fn new(storage: PathBuf, policy: TrigramPolicy) -> Self { Self { storage, @@ -943,12 +1364,7 @@ impl TrigramAdapter { if !generation.segment_ids().is_empty() { return Err(plane_error("a v3 generation names a trigram segment")); } - let source = crate::blob_store::v3::PlaneReader::open_existing( - &route.storage, - &route.family, - FamilyPlane::Trigram, - ) - .map_err(|e| plane_error(e.to_string()))?; + let source = self.v3_payloads(route, snapshot)?; let base = generation .base() .map_or(generation.as_ref(), |base| base.as_ref()); @@ -1083,6 +1499,133 @@ impl super::contracts::PlaneAdapter for TrigramAdapter { #[cfg(test)] mod tests { + struct FixedSource(Snapshot); + impl LexicalSnapshotSource for FixedSource { + fn acquire(&self) -> Result { + Ok(self.0.clone()) + } + fn record_change(&self, _: &Path) {} + } + + #[test] + fn lexical_views_projection_refuses_uncertain_or_wrong_policy_snapshots() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("project"); + let storage = temp.path().join("storage"); + fs::create_dir_all(&root).unwrap(); + fs::write(root.join("a.rs"), "needle\n").unwrap(); + let policy = TrigramPolicy { + max_file_size: 1024, + }; + let source = BaseBuilderTestSource::new(&root, &storage, "lexical-guards", policy); + let snapshot = source.acquire().unwrap(); + let make_view = |snapshot, policy| { + LexicalView::with_source( + root.clone(), + crate::root_cache::StoragePaths::same(&storage), + "lexical-guards".into(), + policy, + Arc::new(FixedSource(snapshot)), + ) + }; + for state in [ + WatcherState::Overflowed, + WatcherState::Stopped, + WatcherState::Reconciling, + ] { + let mut delta = super::super::snapshot::LiveDelta::new(snapshot.generation().clone()); + delta.set_watcher(state); + assert!(make_view(delta.snapshot(), policy) + .snapshot() + .unwrap_err() + .contains("unverified writes")); + } + let mut delta = super::super::snapshot::LiveDelta::new(snapshot.generation().clone()); + delta.record_intent(RelPath::new(b"a.rs".to_vec()).unwrap()); + assert!(make_view(delta.snapshot(), policy) + .snapshot() + .unwrap_err() + .contains("unverified writes")); + assert!(make_view( + snapshot, + TrigramPolicy { + max_file_size: 1025 + } + ) + .snapshot() + .unwrap_err() + .contains("not ready")); + } + + #[test] + fn lexical_views_projection_matches_legacy_masks_ranks_and_membership() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("project"); + let storage = temp.path().join("storage"); + fs::create_dir_all(root.join("src")).unwrap(); + fs::write(root.join("src/a.rs"), "fn needle_alpha() {}\nneedle beta\n").unwrap(); + fs::write(root.join("src/b.rs"), "fn needle_beta() {}\n").unwrap(); + fs::write(root.join("binary.dat"), [0, 1, 2, 3]).unwrap(); + fs::write(root.join(".gitignore"), "ignored.rs\n").unwrap(); + fs::write(root.join("ignored.rs"), "needle ignored\n").unwrap(); + let policy = TrigramPolicy { + max_file_size: 1024 * 1024, + }; + let source = BaseBuilderTestSource::new(&root, &storage, "lexical-parity", policy); + let view = LexicalView::with_source( + root.clone(), + crate::root_cache::StoragePaths::same(&storage), + "lexical-parity".into(), + policy, + source.clone(), + ); + let projected = view.snapshot().unwrap(); + let legacy = + crate::search_index::SearchIndex::build_with_limit_serial(&root, policy.max_file_size) + .snapshot(); + assert_eq!(projected.glob("**/*", &root), legacy.glob("**/*", &root)); + for pattern in [ + "needle", + "needle_(alpha|beta)", + "n.e.*beta", + "^fn", + "NEEDLE", + "a", + ] { + for insensitive in [false, true] { + assert_eq!( + projected + .grep(pattern, !insensitive, &[], &[], &root, 100) + .unwrap() + .matches, + legacy + .grep(pattern, !insensitive, &[], &[], &root, 100) + .unwrap() + .matches, + "{pattern} insensitive={insensitive}" + ); + } + } + for query in ["needle", "needle alpha", "beta", "absent"] { + let trigrams = crate::search_index::query_trigrams_from_tokens( + &query.split_whitespace().collect::>(), + ); + assert_eq!( + projected + .lexical_rank_at_depth(&trigrams, None, 20) + .unwrap() + .files, + legacy + .lexical_rank_at_depth(&trigrams, None, 20) + .unwrap() + .files + ); + } + assert!(source.acquire().unwrap().generation().v3_route().is_some()); + assert!(!storage.join("blobs/v2").exists()); + assert!(!storage.join("views/v3").exists()); + } + #[test] fn unrepresentable_query_gap_keeps_the_exact_relative_bytes() { let path = RelPath::new(b"a-\xff.rs".to_vec()).unwrap();