diff --git a/src/cli/batch/handlers/analysis.rs b/src/cli/batch/handlers/analysis.rs index 8baa9323..82584509 100644 --- a/src/cli/batch/handlers/analysis.rs +++ b/src/cli/batch/handlers/analysis.rs @@ -655,6 +655,7 @@ mod parity_tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/src/cli/batch/handlers/dispatch_tests.rs b/src/cli/batch/handlers/dispatch_tests.rs index 7da18e9e..5d5c32ea 100644 --- a/src/cli/batch/handlers/dispatch_tests.rs +++ b/src/cli/batch/handlers/dispatch_tests.rs @@ -71,6 +71,7 @@ fn make_chunk( window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } @@ -547,6 +548,7 @@ fn make_chunk_lang(id: &str, file: &str, name: &str, lang: Language) -> Chunk { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/src/cli/batch/handlers/graph.rs b/src/cli/batch/handlers/graph.rs index db1a6c37..e8b557c0 100644 --- a/src/cli/batch/handlers/graph.rs +++ b/src/cli/batch/handlers/graph.rs @@ -613,6 +613,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } @@ -1851,6 +1852,7 @@ mod tests { window_idx: None, parent_type_name: parent.map(String::from), parser_version: 0, + is_test: false, } } diff --git a/src/cli/batch/handlers/info.rs b/src/cli/batch/handlers/info.rs index 7c2fe3bb..8fff6add 100644 --- a/src/cli/batch/handlers/info.rs +++ b/src/cli/batch/handlers/info.rs @@ -314,6 +314,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } @@ -536,6 +537,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/src/cli/batch/handlers/search.rs b/src/cli/batch/handlers/search.rs index 79da2273..e4dd22d6 100644 --- a/src/cli/batch/handlers/search.rs +++ b/src/cli/batch/handlers/search.rs @@ -638,6 +638,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/src/cli/batch/json_args.rs b/src/cli/batch/json_args.rs index 49dc1946..c9cf963a 100644 --- a/src/cli/batch/json_args.rs +++ b/src/cli/batch/json_args.rs @@ -1002,6 +1002,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/src/cli/commands/graph/callers.rs b/src/cli/commands/graph/callers.rs index 49f76b1d..5258f9a9 100644 --- a/src/cli/commands/graph/callers.rs +++ b/src/cli/commands/graph/callers.rs @@ -1717,6 +1717,7 @@ mod tests { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, vendored: false, } } diff --git a/src/cli/commands/graph/deps.rs b/src/cli/commands/graph/deps.rs index 8f7eb705..42847148 100644 --- a/src/cli/commands/graph/deps.rs +++ b/src/cli/commands/graph/deps.rs @@ -572,6 +572,7 @@ mod tests { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, vendored: false, } } @@ -637,6 +638,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store .upsert_chunks_batch(&[(chunk, mock_embedding)], Some(100)) @@ -716,6 +718,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; s.upsert_chunks_batch(&[(chunk, embedding.clone())], Some(0))?; } diff --git a/src/cli/commands/graph/explain.rs b/src/cli/commands/graph/explain.rs index 5230fc11..6bde76d6 100644 --- a/src/cli/commands/graph/explain.rs +++ b/src/cli/commands/graph/explain.rs @@ -603,6 +603,7 @@ mod output_tests { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, vendored: false, } } @@ -1020,6 +1021,7 @@ mod output_tests { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, vendored: false, } } diff --git a/src/cli/commands/graph/impact.rs b/src/cli/commands/graph/impact.rs index 0c143e76..dfeb78da 100644 --- a/src/cli/commands/graph/impact.rs +++ b/src/cli/commands/graph/impact.rs @@ -646,6 +646,7 @@ mod tests { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, vendored: false, } } @@ -905,6 +906,7 @@ mod tests { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, vendored: false, } } diff --git a/src/cli/commands/graph/mod.rs b/src/cli/commands/graph/mod.rs index a5aebf54..8e9753f5 100644 --- a/src/cli/commands/graph/mod.rs +++ b/src/cli/commands/graph/mod.rs @@ -511,6 +511,7 @@ mod tests { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, vendored, } } diff --git a/src/cli/commands/graph/test_map.rs b/src/cli/commands/graph/test_map.rs index 2e26e378..ae7247bf 100644 --- a/src/cli/commands/graph/test_map.rs +++ b/src/cli/commands/graph/test_map.rs @@ -550,6 +550,7 @@ mod output_tests { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, vendored: false, } } diff --git a/src/cli/commands/graph/trace.rs b/src/cli/commands/graph/trace.rs index 67e9ff61..e57277f2 100644 --- a/src/cli/commands/graph/trace.rs +++ b/src/cli/commands/graph/trace.rs @@ -990,6 +990,7 @@ mod tests { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, vendored: false, } } @@ -1056,6 +1057,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store .upsert_chunks_batch(&[(chunk, mock_embedding)], Some(100)) diff --git a/src/cli/commands/index/build.rs b/src/cli/commands/index/build.rs index 53c926f1..2004167c 100644 --- a/src/cli/commands/index/build.rs +++ b/src/cli/commands/index/build.rs @@ -1814,6 +1814,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; // Distinct unit-ish vectors per chunk so the HNSW builder has // something non-degenerate to insert. diff --git a/src/cli/commands/index/umap.rs b/src/cli/commands/index/umap.rs index 136417d1..0488e83d 100644 --- a/src/cli/commands/index/umap.rs +++ b/src/cli/commands/index/umap.rs @@ -1022,6 +1022,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; // Pad/truncate the synthetic embedding to the store dim so the // wire bytes match what `embedding_batches` expects. diff --git a/src/cli/commands/io/blame.rs b/src/cli/commands/io/blame.rs index 654ab283..f1e55f60 100644 --- a/src/cli/commands/io/blame.rs +++ b/src/cli/commands/io/blame.rs @@ -487,6 +487,7 @@ mod tests { content_hash: String::new(), window_idx: None, parser_version: 0, + is_test: false, vendored: false, }, commits: vec![BlameEntry { @@ -537,6 +538,7 @@ mod tests { content_hash: String::new(), window_idx: None, parser_version: 0, + is_test: false, vendored: false, }, commits: vec![], diff --git a/src/cli/commands/io/context.rs b/src/cli/commands/io/context.rs index 900d6619..66210355 100644 --- a/src/cli/commands/io/context.rs +++ b/src/cli/commands/io/context.rs @@ -896,6 +896,7 @@ mod tests { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, vendored: false, } } @@ -937,6 +938,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/src/cli/commands/io/reconstruct.rs b/src/cli/commands/io/reconstruct.rs index 4af53bdd..3651acf5 100644 --- a/src/cli/commands/io/reconstruct.rs +++ b/src/cli/commands/io/reconstruct.rs @@ -110,6 +110,7 @@ mod tests { content_hash: String::new(), window_idx: None, parser_version: 0, + is_test: false, vendored: false, } } diff --git a/src/cli/commands/review/dead.rs b/src/cli/commands/review/dead.rs index 0250c593..380b41b3 100644 --- a/src/cli/commands/review/dead.rs +++ b/src/cli/commands/review/dead.rs @@ -906,6 +906,7 @@ mod tests { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, vendored: false, }, confidence: DeadConfidence::High, diff --git a/src/cli/commands/search/query.rs b/src/cli/commands/search/query.rs index 68dbb428..67c060b5 100644 --- a/src/cli/commands/search/query.rs +++ b/src/cli/commands/search/query.rs @@ -2102,6 +2102,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; { let s = Store::open(&db).expect("open ref store"); @@ -2275,6 +2276,7 @@ mod tests { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, vendored: false, }; UnifiedResult::Code(cqs::store::SearchResult::new(summary, score)) @@ -2726,6 +2728,7 @@ mod tests { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, vendored: false, }; SearchResult::new(summary, score) @@ -2761,6 +2764,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store .upsert_chunks_batch(&[(chunk, one_hot(*slot))], Some(0)) @@ -2821,6 +2825,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store .upsert_chunks_batch(&[(chunk, emb.clone())], Some(0)) diff --git a/src/cli/commands/search/similar.rs b/src/cli/commands/search/similar.rs index 663646aa..6fd55121 100644 --- a/src/cli/commands/search/similar.rs +++ b/src/cli/commands/search/similar.rs @@ -345,6 +345,7 @@ mod tests { content_hash: String::new(), window_idx: None, parser_version: 0, + is_test: false, vendored: false, }, score, diff --git a/src/cli/commands/train/train_pairs.rs b/src/cli/commands/train/train_pairs.rs index 5058e032..1a69abcb 100644 --- a/src/cli/commands/train/train_pairs.rs +++ b/src/cli/commands/train/train_pairs.rs @@ -188,6 +188,7 @@ mod tests { content_hash: format!("hash_{}", name), window_idx: None, parser_version: 0, + is_test: false, vendored: false, } } diff --git a/src/cli/display.rs b/src/cli/display.rs index 5305c19e..31b49633 100644 --- a/src/cli/display.rs +++ b/src/cli/display.rs @@ -1042,6 +1042,7 @@ mod output_schema_tests { parent_id: Some("parent-id".to_string()), // → has_parent: true parent_type_name: None, parser_version: 0, + is_test: false, vendored: false, }; let mut sr = SearchResult::new(summary, 0.87); diff --git a/src/cli/enrichment.rs b/src/cli/enrichment.rs index d251f54c..19b27b67 100644 --- a/src/cli/enrichment.rs +++ b/src/cli/enrichment.rs @@ -722,6 +722,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; let chunk_b = cqs::parser::Chunk { id: "src/svc.rs:10:b".to_string(), @@ -741,6 +742,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; // Sentinel-mode upsert → needs_embedding=1, zero-vec embeddings. diff --git a/src/cli/pipeline/embedding.rs b/src/cli/pipeline/embedding.rs index 8cda7519..79e94da3 100644 --- a/src/cli/pipeline/embedding.rs +++ b/src/cli/pipeline/embedding.rs @@ -636,6 +636,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/src/cli/pipeline/mod.rs b/src/cli/pipeline/mod.rs index acb8e435..a95f3a35 100644 --- a/src/cli/pipeline/mod.rs +++ b/src/cli/pipeline/mod.rs @@ -287,6 +287,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/src/cli/pipeline/parsing.rs b/src/cli/pipeline/parsing.rs index d74474b6..05f11c8f 100644 --- a/src/cli/pipeline/parsing.rs +++ b/src/cli/pipeline/parsing.rs @@ -710,6 +710,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } @@ -1189,6 +1190,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: cqs::parser_version(), + is_test: false, }; let emb = cqs::Embedding::new(vec![0.5; cqs::EMBEDDING_DIM]); @@ -1323,6 +1325,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: stale_version, + is_test: false, }; let emb = cqs::Embedding::new(vec![0.5; cqs::EMBEDDING_DIM]); @@ -1420,6 +1423,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: stale_version, + is_test: false, }; let emb = cqs::Embedding::new(vec![0.5; cqs::EMBEDDING_DIM]); let disk_fp = FileFingerprint::read_disk( @@ -1675,6 +1679,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: cqs::parser_version(), + is_test: false, }; let emb = cqs::Embedding::new(vec![0.5; cqs::EMBEDDING_DIM]); let divergent_fp = cqs::store::FileFingerprint { @@ -1771,6 +1776,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: cqs::parser_version(), + is_test: false, }; let emb = cqs::Embedding::new(vec![0.5; cqs::EMBEDDING_DIM]); let divergent_fp = cqs::store::FileFingerprint { diff --git a/src/cli/pipeline/reuse.rs b/src/cli/pipeline/reuse.rs index 8d758c85..94c56a85 100644 --- a/src/cli/pipeline/reuse.rs +++ b/src/cli/pipeline/reuse.rs @@ -243,6 +243,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/src/cli/pipeline/upsert.rs b/src/cli/pipeline/upsert.rs index 85892375..e165cc5e 100644 --- a/src/cli/pipeline/upsert.rs +++ b/src/cli/pipeline/upsert.rs @@ -711,6 +711,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/src/cli/pipeline/windowing.rs b/src/cli/pipeline/windowing.rs index 18b8ff51..bcf9f712 100644 --- a/src/cli/pipeline/windowing.rs +++ b/src/cli/pipeline/windowing.rs @@ -100,6 +100,9 @@ pub(crate) fn apply_windowing(chunks: Vec, embedder: &Embedder) -> Vec ( window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } }; { diff --git a/src/cli/watch/reconcile.rs b/src/cli/watch/reconcile.rs index 3b13a314..4678a1ae 100644 --- a/src/cli/watch/reconcile.rs +++ b/src/cli/watch/reconcile.rs @@ -647,6 +647,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }, placeholder_embedding(i as f32), )); @@ -746,6 +747,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: cqs::parser_version(), // current — exercise fingerprint path, not drift + is_test: false, }; let store = open_store(&cqs_dir); @@ -912,6 +914,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; let store = open_store(&cqs_dir); @@ -992,6 +995,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: cqs::parser_version(), // current — exercise fingerprint path, not drift + is_test: false, }; let store = open_store(&cqs_dir); @@ -1079,6 +1083,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; let store = open_store(&cqs_dir); @@ -1161,6 +1166,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: cqs::parser_version(), // current — exercise fingerprint path, not drift + is_test: false, }; let store = open_store(&cqs_dir); @@ -1288,6 +1294,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: cqs::parser_version(), // current — exercise fingerprint path, not drift + is_test: false, }; let store = open_store(&cqs_dir); diff --git a/src/cli/watch/reindex.rs b/src/cli/watch/reindex.rs index 7b80a80f..5f5e2d67 100644 --- a/src/cli/watch/reindex.rs +++ b/src/cli/watch/reindex.rs @@ -1065,6 +1065,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: version, + is_test: false, }; let emb = Embedding::new(vec![0.5; cqs::EMBEDDING_DIM]); store diff --git a/src/cli/watch/tests.rs b/src/cli/watch/tests.rs index 077e02ba..82d87889 100644 --- a/src/cli/watch/tests.rs +++ b/src/cli/watch/tests.rs @@ -3068,6 +3068,7 @@ fn make_gc_test_chunk(file: &Path, name: &str, line_start: u32) -> cqs::parser:: window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/src/drift.rs b/src/drift.rs index 22db9d1f..0be3d820 100644 --- a/src/drift.rs +++ b/src/drift.rs @@ -243,6 +243,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/src/health.rs b/src/health.rs index 1b5a353e..9638803c 100644 --- a/src/health.rs +++ b/src/health.rs @@ -188,6 +188,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/src/hnsw/persist.rs b/src/hnsw/persist.rs index 9682b144..063e3a13 100644 --- a/src/hnsw/persist.rs +++ b/src/hnsw/persist.rs @@ -3016,6 +3016,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store .upsert_chunks_batch(&[(chunk, make_embedding(42))], Some(0)) diff --git a/src/impact/analysis.rs b/src/impact/analysis.rs index 2c5d1b45..de613d49 100644 --- a/src/impact/analysis.rs +++ b/src/impact/analysis.rs @@ -801,6 +801,7 @@ mod tests { content_hash: String::new(), window_idx: None, parser_version: 0, + is_test: false, vendored: false, }, ChunkSummary { @@ -819,6 +820,7 @@ mod tests { content_hash: String::new(), window_idx: None, parser_version: 0, + is_test: false, vendored: false, }, ]; diff --git a/src/impact/diff.rs b/src/impact/diff.rs index b471d149..e6883aa7 100644 --- a/src/impact/diff.rs +++ b/src/impact/diff.rs @@ -477,6 +477,7 @@ mod tests { content_hash: String::new(), window_idx: None, parser_version: 0, + is_test: false, vendored: false, } } @@ -921,6 +922,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store .upsert_chunks_batch( diff --git a/src/impact/hints.rs b/src/impact/hints.rs index 92e648db..85b2acc9 100644 --- a/src/impact/hints.rs +++ b/src/impact/hints.rs @@ -429,6 +429,7 @@ mod tests { content_hash: String::new(), window_idx: None, parser_version: 0, + is_test: false, vendored: false, }]; let hints = compute_hints_with_graph(&graph, &test_chunks, "target", None); @@ -493,6 +494,7 @@ mod tests { content_hash: String::new(), window_idx: None, parser_version: 0, + is_test: false, vendored: false, }]; let scores = compute_risk_batch(&["target"], &graph, &test_chunks); @@ -545,6 +547,7 @@ mod tests { content_hash: String::new(), window_idx: None, parser_version: 0, + is_test: false, vendored: false, }, crate::store::ChunkSummary { @@ -563,6 +566,7 @@ mod tests { content_hash: String::new(), window_idx: None, parser_version: 0, + is_test: false, vendored: false, }, crate::store::ChunkSummary { @@ -581,6 +585,7 @@ mod tests { content_hash: String::new(), window_idx: None, parser_version: 0, + is_test: false, vendored: false, }, ]; @@ -661,6 +666,7 @@ mod tests { content_hash: String::new(), window_idx: None, parser_version: 0, + is_test: false, vendored: false, }]; @@ -761,6 +767,7 @@ mod tests { content_hash: String::new(), window_idx: None, parser_version: 0, + is_test: false, vendored: false, }]; let scores = compute_risk_batch(&["target"], &graph, &test_chunks); @@ -804,6 +811,7 @@ mod tests { content_hash: String::new(), window_idx: None, parser_version: 0, + is_test: false, vendored: false, } } diff --git a/src/impact/test_map.rs b/src/impact/test_map.rs index 2755de51..0e161841 100644 --- a/src/impact/test_map.rs +++ b/src/impact/test_map.rs @@ -131,6 +131,7 @@ mod tests { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, vendored: false, } } diff --git a/src/kind.rs b/src/kind.rs index fb8e4ead..4f66f70c 100644 --- a/src/kind.rs +++ b/src/kind.rs @@ -447,6 +447,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } @@ -554,6 +555,7 @@ mod tests { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, vendored: false, }; let hit = KindHit::from(&summary); diff --git a/src/language/languages.rs b/src/language/languages.rs index f0222ebd..ea7e3cd7 100644 --- a/src/language/languages.rs +++ b/src/language/languages.rs @@ -6474,7 +6474,15 @@ static LANG_RUST: LanguageDef = LanguageDef { post_process_chunk: Some(post_process_rust_rust as PostProcessChunkFn), test_markers: &["#[test]", "#[cfg(test)]"], endpoint_markers: &[], - test_path_patterns: &["%/tests/%", "%\\_test.rs"], + // `%/tests.rs` / `%/test.rs`: a Rust submodule-test FILE (the `mod tests;` + // → `tests.rs` convention, e.g. `src/serve/tests.rs`). The prior patterns + // only matched a `/tests/` DIRECTORY segment, so a same-file `tests.rs` + // module read as production. Anchored on the leading `/` so substring traps + // (`contest.rs`, `latest.rs`) do not match. In the registry (not only the + // parse-time flag) so every `is_test_chunk` consumer — chunk_importance, + // scout/impact filtering, the dead-code SQL filter — agrees on one + // definition of "test file". + test_path_patterns: &["%/tests/%", "%\\_test.rs", "%/tests.rs", "%/test.rs"], entry_point_names: &["main"], trait_method_names: &[ // std::fmt diff --git a/src/llm/doc_comments.rs b/src/llm/doc_comments.rs index 22455e16..78c97d2d 100644 --- a/src/llm/doc_comments.rs +++ b/src/llm/doc_comments.rs @@ -393,6 +393,7 @@ mod tests { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, vendored: false, } } @@ -414,6 +415,7 @@ mod tests { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, vendored: false, } } diff --git a/src/llm/summary.rs b/src/llm/summary.rs index 7d938412..49a1937c 100644 --- a/src/llm/summary.rs +++ b/src/llm/summary.rs @@ -356,6 +356,7 @@ mod tests { content_hash: content_hash.to_string(), window_idx, parser_version: 0, + is_test: false, vendored: false, } } diff --git a/src/nl/mod.rs b/src/nl/mod.rs index 42f9cd37..9a3e4a7e 100644 --- a/src/nl/mod.rs +++ b/src/nl/mod.rs @@ -167,6 +167,7 @@ pub fn generate_nl_with_call_context_and_summary( /// window_idx: None, /// parent_type_name: None, /// parser_version: 0, +/// is_test: false, /// }; /// /// let nl = generate_nl_description_with_seq_len(&chunk, 512); @@ -598,6 +599,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; let nl = generate_nl_description_with_seq_len(&chunk, 512); @@ -635,6 +637,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; let nl = generate_nl_description_with_seq_len(&chunk, 512); @@ -675,6 +678,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } @@ -728,6 +732,7 @@ mod tests { window_idx: None, parent_type_name: Some("CircuitBreaker".to_string()), parser_version: 0, + is_test: false, }; let nl = generate_nl_description_with_seq_len(&chunk, 512); assert!( @@ -758,6 +763,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; let nl = generate_nl_description_with_seq_len(&chunk, 512); // Compact: no "A method named" prefix, just tokenized name @@ -790,6 +796,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; let nl = generate_nl_description_with_seq_len(&chunk, 512); assert!(nl.contains("standalone")); @@ -815,6 +822,7 @@ mod tests { window_idx: None, parent_type_name: Some("CircuitBreaker".to_string()), parser_version: 0, + is_test: false, }; let nl = generate_nl_with_template_and_seq_len(&chunk, NlTemplate::DocFirst, 512); // DocFirst returns early: doc + name only, no parent type context @@ -870,6 +878,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/src/parser/chunk.rs b/src/parser/chunk.rs index dcda4089..2babda72 100644 --- a/src/parser/chunk.rs +++ b/src/parser/chunk.rs @@ -93,7 +93,15 @@ use super::Parser; /// byte-identical Ruby/ST file re-parsed under v17 gains `parent_type_name` on /// those method chunks where it was `None` before, so a refresh is required /// even when the file's bytes are unchanged. -pub const PARSER_VERSION: u32 = 17; +/// 18: every chunk now carries a parse-time `is_test` flag +/// ([`Chunk::is_test`], persisted in the v33 `chunks.is_test` column). A Rust +/// item is flagged test when it carries a `#[test]`-family attribute, when it +/// or any ancestor is gated by `#[cfg(test)]` / `#[cfg(all(test, …))]`, or when +/// the canonical `is_test_chunk` registry patterns match (all languages get the +/// registry layer). A byte-identical file re-parsed under v18 populates the new +/// flag it did not carry under v17, so a refresh is required even when the +/// file's bytes are unchanged. +pub const PARSER_VERSION: u32 = 18; /// Build the canonical chunk id from its identifying coordinates. /// @@ -314,6 +322,163 @@ fn collect_comment_ranges<'a>( } } +/// Whether a freshly-parsed chunk is test code — the parse-time single source +/// of truth persisted in `chunks.is_test`. +/// +/// Layered (any layer true ⇒ test): +/// - **Rust AST**: the item, or any ancestor, carries a `#[test]`-family +/// attribute or a `#[cfg(test)]` / `#[cfg(all(test, …))]` gate — see +/// [`rust_node_is_test`]. This is the layer that catches the +/// `#[cfg(test)]`-in-`src/` case rank time cannot: the attribute text is a +/// prev-sibling node OUTSIDE the chunk's byte range, so it is invisible to +/// any content sniff. +/// - **Registry (all languages)**: the canonical [`crate::is_test_chunk`] +/// path/name patterns (`%/tests/%`, `%_test.rs`, `%/tests.rs` / `%/test.rs`, +/// the `test_`/`_test` name conventions). The AST layer is Rust-only in this +/// pass; every other language relies on the registry patterns. +fn compute_is_test( + node: tree_sitter::Node, + source: &str, + language: Language, + name: &str, + file: &str, +) -> bool { + (language == Language::Rust && rust_node_is_test(node, source)) + || crate::is_test_chunk(name, file) +} + +/// Rust-only AST test detection: walk `node` and every ancestor, returning true +/// as soon as one carries a `#[test]`-family attribute or a `#[cfg(test)]` / +/// `#[cfg(all(test, …))]` gate. +/// +/// The ancestor walk is load-bearing: a `fn` inside `#[cfg(test)] mod tests {}` +/// has the gate on the `mod_item` (the attribute is a prev-sibling of the mod, +/// not of the fn), so only walking up to the mod finds it. +fn rust_node_is_test(node: tree_sitter::Node, source: &str) -> bool { + let mut current = Some(node); + while let Some(n) = current { + if node_has_test_attr(n, source) { + return true; + } + current = n.parent(); + } + false +} + +/// Whether any `attribute_item` immediately preceding `node` is a `#[test]`- +/// family attribute or a `#[cfg(test)]`-family gate. +/// +/// In tree-sitter-rust the attributes decorating an item are its prev-siblings +/// (outside the item's byte range), chaining as consecutive `attribute_item` +/// nodes. Doc/line comments may interleave between the attributes and the item, +/// so comment siblings are skipped; the first non-attribute, non-comment +/// sibling ends the scan. +fn node_has_test_attr(node: tree_sitter::Node, source: &str) -> bool { + let mut sib = node.prev_sibling(); + while let Some(s) = sib { + match s.kind() { + "attribute_item" => { + if let Some(text) = source.get(s.byte_range()) { + if attr_text_is_test_family(text) || attr_text_is_cfg_test(text) { + return true; + } + } + } + k if k.contains("comment") => {} + _ => break, + } + sib = s.prev_sibling(); + } + false +} + +/// Whether an outer-attribute source string is a `#[test]`-family attribute: +/// its path's terminal segment is `test` (`#[test]`, `#[tokio::test]`, +/// `#[tokio::test(flavor = "multi_thread")]`). The `test_case` / `rstest` +/// families do not appear in this repo and are intentionally out of scope — +/// their terminal segment is not `test`. +fn attr_text_is_test_family(attr_text: &str) -> bool { + let Some(inner) = attr_text + .trim() + .strip_prefix("#[") + .and_then(|s| s.strip_suffix(']')) + else { + return false; + }; + // Path is everything before the first `(` (arguments). Terminal `::` segment. + let path = inner.split('(').next().unwrap_or(inner).trim(); + let terminal = path.rsplit("::").next().unwrap_or(path).trim(); + terminal == "test" +} + +/// Whether an outer-attribute source string is a `#[cfg(test)]`-family gate — +/// the bare `#[cfg(test)]` or the composed `#[cfg(all(test, …))]`. Mirrors the +/// semantics of `is_cfg_test_attr` in `cli/watch/socket.rs`: `all(test, …)` +/// gates on `test` (the whole item is test-only), while `any(test, …)` and +/// `not(test)` do NOT (those items still compile in production builds). +fn attr_text_is_cfg_test(attr_text: &str) -> bool { + let Some(inner) = attr_text + .trim() + .strip_prefix("#[") + .and_then(|s| s.strip_suffix(']')) + else { + return false; + }; + let Some(args) = inner + .trim() + .strip_prefix("cfg(") + .and_then(|s| s.strip_suffix(')')) + else { + return false; + }; + let args = args.trim(); + if args == "test" { + return true; + } + if let Some(all_args) = args.strip_prefix("all(").and_then(|s| s.strip_suffix(')')) { + return split_top_level_commas(all_args) + .into_iter() + .any(|a| a.trim() == "test"); + } + false +} + +/// Split `s` on top-level commas — commas nested inside parens/brackets or +/// string literals do not split. Reads the direct members of an `all( … )` cfg +/// predicate so `all(test, feature = "x")` yields `test` as one member. Mirrors +/// `split_top_level` in `cli/watch/socket.rs`. +fn split_top_level_commas(s: &str) -> Vec<&str> { + let mut parts = Vec::new(); + let mut depth: i32 = 0; + let mut in_str = false; + let mut escaped = false; + let mut start = 0; + for (idx, ch) in s.char_indices() { + if in_str { + if escaped { + escaped = false; + } else if ch == '\\' { + escaped = true; + } else if ch == '"' { + in_str = false; + } + continue; + } + match ch { + '"' => in_str = true, + '(' | '[' | '{' => depth += 1, + ')' | ']' | '}' => depth -= 1, + ',' if depth == 0 => { + parts.push(&s[start..idx]); + start = idx + 1; + } + _ => {} + } + } + parts.push(&s[start..]); + parts +} + impl Parser { /// Extracts a code chunk from a source file using Tree-sitter query matches. /// # Arguments @@ -450,6 +615,12 @@ impl Parser { // force a needless re-embed. let canonical = canonical_hash_spanning(node, body_node, &content); + // Parse-time test-code flag. `node` is the captured definition; its + // preceding `#[test]` / `#[cfg(test)]` attributes are prev-sibling nodes + // OUTSIDE `content`, so this AST walk is the only place they are + // reachable. Registry path/name patterns cover every language. + let is_test = compute_is_test(node, source, language, &name, &path.to_string_lossy()); + Ok(Chunk { id, file: path.to_path_buf(), @@ -468,6 +639,7 @@ impl Parser { window_idx: None, parent_type_name, parser_version: PARSER_VERSION, + is_test, }) } } @@ -3615,4 +3787,161 @@ CREATE TABLE accounts ( ); } } + + /// Parse-time `is_test` flag: the R@5-crowder cases the old rank-time + /// path/name sniff could not see (a `#[cfg(test)]` fn in a `src/` file). + mod is_test_flag { + use super::super::*; + use std::collections::HashMap; + use std::path::Path; + + /// Parse `source` as Rust at `path` and map each chunk name to its + /// `is_test` flag. Panics on a duplicate name so a fixture with two + /// same-named chunks fails loudly instead of silently overwriting. + fn is_test_by_name(source: &str, path: &str) -> HashMap { + let parser = Parser::new().unwrap(); + let chunks = parser + .parse_source(source, Language::Rust, Path::new(path)) + .unwrap(); + let mut map = HashMap::new(); + for c in chunks { + assert!( + map.insert(c.name.clone(), c.is_test).is_none(), + "duplicate chunk name in fixture: {}", + c.name + ); + } + map + } + + /// The audit's exact case plus every attribute/gate shape: a + /// `#[cfg(test)] mod tests { fn foo }` member and a bare `#[test]` fn + /// carry `is_test`, a production sibling in the SAME `src/` file does + /// not. All at a `src/` path AND with names that carry NO registry + /// name pattern (no `test_` / `_test` / `_test_`), so the flag can only + /// come from the Rust AST layer — this isolates it from the registry. + #[test] + fn rust_ast_attribute_and_cfg_test_shapes() { + let source = r#" +pub fn production_fn() {} + +#[test] +fn plain_attr_fn() {} + +#[tokio::test] +async fn async_attr_fn() {} + +#[cfg(test)] +fn cfg_gated_fn() {} + +#[cfg(test)] +mod inner_tests { + pub fn helper_in_mod() {} + + #[test] + fn nested_case_fn() {} +} + +#[cfg(all(test, feature = "cuda-index"))] +mod composed_tests { + pub fn helper_in_composed() {} +} + +#[cfg(any(test, feature = "x"))] +mod any_mod { + pub fn helper_in_any() {} +} + +#[cfg(not(test))] +fn not_gated_fn() {} +"#; + let m = is_test_by_name(source, "src/parser/chunk.rs"); + + // Production code: not test. + assert_eq!(m.get("production_fn"), Some(&false)); + // (a) #[test]-family attributes. + assert_eq!(m.get("plain_attr_fn"), Some(&true)); + assert_eq!(m.get("async_attr_fn"), Some(&true)); + // (b) #[cfg(test)] gate, on the item and via a mod ancestor. + assert_eq!(m.get("cfg_gated_fn"), Some(&true)); + assert_eq!(m.get("helper_in_mod"), Some(&true)); + assert_eq!(m.get("nested_case_fn"), Some(&true)); + // Composed all(test, …) gates on test. + assert_eq!(m.get("helper_in_composed"), Some(&true)); + // any(test, …) and not(test) do NOT gate — still production. + assert_eq!(m.get("helper_in_any"), Some(&false)); + assert_eq!(m.get("not_gated_fn"), Some(&false)); + } + + /// (c)+(d) registry path patterns, exercised through the parse-time + /// flag on non-attributed production-looking code. + #[test] + fn registry_path_patterns_flag_test_files() { + let src = "pub fn run() {}\n"; + + // (d) `tests.rs` / `test.rs` FILENAME — the newly-covered A/B shape. + assert_eq!( + is_test_by_name(src, "src/serve/tests.rs").get("run"), + Some(&true), + "a same-file tests.rs module must flag as test" + ); + assert_eq!( + is_test_by_name(src, "src/serve/test.rs").get("run"), + Some(&true) + ); + // (c) `/tests/` directory segment. + assert_eq!( + is_test_by_name(src, "src/foo/tests/helpers.rs").get("run"), + Some(&true) + ); + // `_test.rs` suffix. + assert_eq!( + is_test_by_name(src, "src/foo_test.rs").get("run"), + Some(&true) + ); + + // Substring traps must NOT match: the `/tests.rs` / `/test.rs` + // anchor requires a leading `/`, so `contest.rs` / `latest.rs` / + // `manifest.rs` stay production. + for trap in ["src/contest.rs", "src/latest.rs", "src/manifest.rs"] { + assert_eq!( + is_test_by_name(src, trap).get("run"), + Some(&false), + "{trap} must not read as a test file" + ); + } + } + + // ===== attribute-string predicate units (no parse) ===== + + #[test] + fn test_family_predicate() { + assert!(attr_text_is_test_family("#[test]")); + assert!(attr_text_is_test_family("#[tokio::test]")); + assert!(attr_text_is_test_family( + "#[tokio::test(flavor = \"multi_thread\")]" + )); + // Not test-family: + assert!(!attr_text_is_test_family("#[cfg(test)]")); + assert!(!attr_text_is_test_family("#[derive(Debug)]")); + assert!(!attr_text_is_test_family("#[allow(clippy::all)]")); + // `test_case` terminal segment is not `test` (out of scope shape). + assert!(!attr_text_is_test_family("#[test_case(1)]")); + } + + #[test] + fn cfg_test_predicate() { + assert!(attr_text_is_cfg_test("#[cfg(test)]")); + assert!(attr_text_is_cfg_test("#[cfg(all(test, unix))]")); + assert!(attr_text_is_cfg_test( + "#[cfg(all(test, feature = \"cuda-index\"))]" + )); + assert!(attr_text_is_cfg_test("#[cfg(all(feature = \"x\", test))]")); + // any(test, …) / not(test) do NOT gate on test. + assert!(!attr_text_is_cfg_test("#[cfg(any(test, unix))]")); + assert!(!attr_text_is_cfg_test("#[cfg(not(test))]")); + assert!(!attr_text_is_cfg_test("#[cfg(feature = \"test\")]")); + assert!(!attr_text_is_cfg_test("#[test]")); + } + } } diff --git a/src/parser/l5x.rs b/src/parser/l5x.rs index 0a9ad52d..46e175a0 100644 --- a/src/parser/l5x.rs +++ b/src/parser/l5x.rs @@ -235,6 +235,9 @@ fn parse_st_regions( window_idx: None, parent_type_name: region.program_name.clone(), parser_version: super::chunk::PARSER_VERSION, + // ST has no `#[test]` / `#[cfg(test)]` grammar; the flag + // rides only the registry path/name patterns here. + is_test: crate::is_test_chunk(name, &path.to_string_lossy()), }); } } @@ -315,6 +318,10 @@ fn extract_st_chunk( let region_byte_start: u32 = node.byte_range().start.try_into().unwrap_or(u32::MAX); let byte_start = region.byte_offset.saturating_add(region_byte_start); + // ST has no `#[test]` / `#[cfg(test)]` grammar; the flag rides only the + // registry path/name patterns here. + let is_test = crate::is_test_chunk(&mutable_name, &file_path.to_string_lossy()); + Ok(Chunk { id: super::chunk::chunk_id( &file_path.display().to_string(), @@ -338,6 +345,7 @@ fn extract_st_chunk( window_idx: None, parent_type_name: region.program_name.clone(), parser_version: super::chunk::PARSER_VERSION, + is_test, }) } diff --git a/src/parser/markdown/mod.rs b/src/parser/markdown/mod.rs index 9caa0637..0173730c 100644 --- a/src/parser/markdown/mod.rs +++ b/src/parser/markdown/mod.rs @@ -75,6 +75,9 @@ struct ChunkFields<'a> { /// Build a markdown Chunk with common fields pre-filled. fn make_markdown_chunk(fields: ChunkFields<'_>) -> Chunk { + // Markdown has no `#[test]` / `#[cfg(test)]` grammar; the flag rides only + // the registry path/name patterns (e.g. a doc under a `/tests/` dir). + let is_test = crate::is_test_chunk(&fields.name, &fields.path.to_string_lossy()); Chunk { id: fields.id, file: fields.path.to_path_buf(), @@ -96,6 +99,7 @@ fn make_markdown_chunk(fields: ChunkFields<'_>) -> Chunk { window_idx: None, parent_type_name: None, parser_version: super::chunk::PARSER_VERSION, + is_test, } } diff --git a/src/parser/markdown/tables.rs b/src/parser/markdown/tables.rs index fa37380f..8d606739 100644 --- a/src/parser/markdown/tables.rs +++ b/src/parser/markdown/tables.rs @@ -105,6 +105,7 @@ pub(super) fn extract_table_chunks(ctx: &TableContext<'_>, chunks: &mut Vec, chunks: &mut Vec { pub parent_boosts: &'a HashMap<&'a str, f32>, /// Test-code demotion factor (`CQS_TESTCODE_DEMOTE`) applied in /// `finalize_results` step 4b2. `1.0` (the default) means the demotion was - /// inert — no `test_demote` signal is recorded. Reconstructed per result via - /// [`crate::is_test_chunk`], mirroring the application in - /// [`super::apply_testcode_demote`]. + /// inert — no `test_demote` signal is recorded. Per-result test-origin is + /// read from the chunk's parse-time `is_test` flag — the SAME flag the + /// application in [`super::apply_testcode_demote`] reads, so record and + /// application never diverge. pub testcode_demote_factor: f32, /// Whether the query was test-seeking. When true the demotion did not run, /// so `test_demote` is never recorded regardless of the factor. Mirrors the @@ -261,14 +262,12 @@ pub(crate) fn signals_for(result: &SearchResult, ctx: &RankSignalCtx<'_>) -> Vec // test_demote: the test-code demotion multiplier (finalize step 4b2). // Recorded only when it actually moved the score: the knob is engaged // (factor < 1.0), the query was not test-seeking, and this chunk is - // test-origin. Reconstructed via the canonical `is_test_chunk` — the same - // predicate the demotion applied — so the record faithfully mirrors the - // score move. Not gated on `had_dense`: the demotion runs over the final - // result set (like parent_boost/type_boost), so it can move an FTS-sole hit. - if ctx.testcode_demote_factor < 1.0 - && !ctx.query_is_test_seeking - && crate::is_test_chunk(name, file_part) - { + // test-origin. Reads the SAME parse-time `is_test` flag the demotion + // applied (`apply_testcode_demote`), so the record faithfully mirrors the + // score move — application and provenance can never disagree. Not gated on + // `had_dense`: the demotion runs over the final result set (like + // parent_boost/type_boost), so it can move an FTS-sole hit. + if ctx.testcode_demote_factor < 1.0 && !ctx.query_is_test_seeking && result.chunk.is_test { discriminative.push(RankSignal { signal: "test_demote", value: ctx.testcode_demote_factor, @@ -318,6 +317,17 @@ mod tests { window_idx: None, parser_version: 0, vendored: false, + is_test: false, + } + } + + /// A test-origin chunk: identical to [`chunk`] but with the parse-time + /// `is_test` flag set, so `test_demote` sees it (detection now reads the + /// stored flag, not a path/name sniff). + fn test_chunk(id: &str, name: &str, chunk_type: ChunkType) -> ChunkSummary { + ChunkSummary { + is_test: true, + ..chunk(id, name, chunk_type) } } @@ -610,7 +620,7 @@ mod tests { ..base }; let r = SearchResult::new( - chunk("src/lib.rs:1:test_foo", "test_foo", ChunkType::Function), + test_chunk("src/lib.rs:1:test_foo", "test_foo", ChunkType::Function), 0.9, ); let sigs = signals_for(&r, &c); @@ -630,8 +640,8 @@ mod tests { dense.insert("src/lib.rs:1:test_foo", 2usize); dense.insert("src/lib.rs:1:parse", 3usize); let (fts, sparse, pb) = (HashMap::new(), HashMap::new(), HashMap::new()); - let test_chunk = SearchResult::new( - chunk("src/lib.rs:1:test_foo", "test_foo", ChunkType::Function), + let test_result = SearchResult::new( + test_chunk("src/lib.rs:1:test_foo", "test_foo", ChunkType::Function), 0.9, ); @@ -641,7 +651,7 @@ mod tests { query_is_test_seeking: false, ..ctx(¬e_index, &dense, &fts, &sparse, &pb, None, false, false) }; - assert!(!signals_for(&test_chunk, &inert) + assert!(!signals_for(&test_result, &inert) .iter() .any(|s| s.signal == "test_demote")); @@ -651,7 +661,7 @@ mod tests { query_is_test_seeking: true, ..ctx(¬e_index, &dense, &fts, &sparse, &pb, None, false, false) }; - assert!(!signals_for(&test_chunk, &seeking) + assert!(!signals_for(&test_result, &seeking) .iter() .any(|s| s.signal == "test_demote")); diff --git a/src/search/scoring/testcode.rs b/src/search/scoring/testcode.rs index 9039edf6..9bff4967 100644 --- a/src/search/scoring/testcode.rs +++ b/src/search/scoring/testcode.rs @@ -12,10 +12,18 @@ //! (ordering-stable) to pre-knob output. The default flip to a real demotion //! factor happens only after a paired A/B on both eval fixtures, not here. //! -//! Test-origin detection reuses the canonical [`crate::is_test_chunk`] -//! path+name classifier — the same one `chunk_importance`, scout, and impact -//! use — so there is one definition of "test file", not a second. The -//! test-seeking query gate is a small fixed vocabulary ([`TEST_SEEKING_TERMS`]); +//! Test-origin detection reads the chunk's parse-time [`is_test`] flag +//! (`chunks.is_test`, decided in the parser as the single source of truth). +//! This is strictly stronger than the old [`crate::is_test_chunk`] path+name +//! sniff it replaced: the flag also carries the Rust AST signal (`#[test]` / +//! `#[cfg(test)]` attributes, which are prev-sibling nodes OUTSIDE the chunk +//! body and thus invisible to any path/name/content check at rank time), so the +//! knob finally sees the dominant self-corpus crowder — a `#[cfg(test)]` fn in a +//! `src/` file. The `test_demote` rank-signal record in `provenance::signals_for` +//! reads the SAME flag, so application and provenance never diverge. +//! +//! [`is_test`]: crate::store::helpers::ChunkSummary::is_test +//! The test-seeking query gate is a small fixed vocabulary ([`TEST_SEEKING_TERMS`]); //! it is deliberately NOT wired into the runtime-extensible classifier vocab in //! `search::router` (scope control — this is a fixed floor, not an //! operator-tunable list). @@ -96,8 +104,10 @@ pub(crate) fn testcode_demote_factor() -> f32 { /// (ordering-stable) to pre-knob. Multiplicative like the type/parent boosts so /// it stays scale-invariant across cosine `[0,1]` and re-ranker scores. /// -/// Test-origin detection is the canonical [`crate::is_test_chunk`] on the -/// chunk's authoritative `origin` path and name — never a second definition. +/// Test-origin detection reads the chunk's parse-time `is_test` flag — the +/// single source of truth decided in the parser — so it sees `#[cfg(test)]`-in- +/// `src/` chunks whose attribute text is outside the chunk body. The +/// `test_demote` rank-signal record reads the same flag (they must not diverge). pub(crate) fn apply_testcode_demote( results: &mut [SearchResult], factor: f32, @@ -118,10 +128,7 @@ pub(crate) fn apply_testcode_demote( } let mut demoted_any = false; for result in results.iter_mut() { - if crate::is_test_chunk( - result.chunk.name.as_str(), - result.chunk.file.to_str().unwrap_or_default(), - ) { + if result.chunk.is_test { result.score *= factor; demoted_any = true; } @@ -136,7 +143,11 @@ mod tests { use crate::store::helpers::ChunkSummary; use std::path::PathBuf; - fn result(name: &str, file: &str, score: f32) -> SearchResult { + /// Build a result with an explicit `is_test` flag. Detection now reads the + /// stored parse-time flag, not a path/name sniff, so the tests set it + /// directly — the classification itself is exercised in the parser's + /// `is_test` tests. + fn result(name: &str, file: &str, score: f32, is_test: bool) -> SearchResult { SearchResult::new( ChunkSummary { id: format!("{file}:1:{name}"), @@ -155,6 +166,7 @@ mod tests { window_idx: None, parser_version: 0, vendored: false, + is_test, }, score, ) @@ -219,8 +231,11 @@ mod tests { #[test] fn demote_multiplies_test_origin_on_plain_query() { let mut results = vec![ - result("parse_config", "src/parser.rs", 0.9), - result("test_parse_config", "src/parser.rs", 0.8), + result("parse_config", "src/parser.rs", 0.9, false), + // A `#[cfg(test)]` fn in a `src/` file: `is_test` is true from the + // parse-time flag even though its path/name look production — the + // exact case the old path/name sniff could not see. + result("schema_sql_chunks", "src/parser/chunk.rs", 0.8, true), ]; let demoted = apply_testcode_demote(&mut results, 0.5, false); assert!(demoted, "a test-origin chunk should have been demoted"); @@ -236,9 +251,10 @@ mod tests { } #[test] - fn demote_detects_test_by_path() { - // Name is not test-shaped, but the file lives under tests/ → test-origin. - let mut results = vec![result("helper", "tests/integration.rs", 0.8)]; + fn demote_reads_stored_is_test_flag() { + // Detection is the stored flag, not a path/name sniff: a chunk flagged + // `is_test` is demoted regardless of how production its path/name look. + let mut results = vec![result("helper", "src/serve/mod.rs", 0.8, true)]; let demoted = apply_testcode_demote(&mut results, 0.5, false); assert!(demoted); assert!((results[0].score - 0.4).abs() < 1e-6); @@ -250,8 +266,8 @@ mod tests { // are byte-identical and nothing is reported demoted, so the caller // skips the re-sort. This is the "inert when off" contract. let before = vec![ - result("parse_config", "src/parser.rs", 0.9), - result("test_parse_config", "src/parser.rs", 0.8), + result("parse_config", "src/parser.rs", 0.9, false), + result("test_parse_config", "src/parser.rs", 0.8, true), ]; let mut after = before.clone(); let demoted = apply_testcode_demote(&mut after, 1.0, false); @@ -272,7 +288,7 @@ mod tests { // `partial_cmp(&1.0)` is `None`, not `Some(Less)`), leaving scores // untouched. Guards the `partial_cmp` demotion gate against a regression // to a bare `factor >= 1.0` (which NaN would fall through). - let mut results = vec![result("test_parse_config", "src/parser.rs", 0.8)]; + let mut results = vec![result("test_parse_config", "src/parser.rs", 0.8, true)]; let demoted = apply_testcode_demote(&mut results, f32::NAN, false); assert!(!demoted, "a NaN factor must not demote"); assert_eq!(results[0].score, 0.8); @@ -281,7 +297,7 @@ mod tests { #[test] fn test_seeking_query_suppresses_demotion() { // Even with an active factor, a test-seeking query demotes nothing. - let mut results = vec![result("test_parse_config", "src/parser.rs", 0.8)]; + let mut results = vec![result("test_parse_config", "src/parser.rs", 0.8, true)]; let demoted = apply_testcode_demote(&mut results, 0.5, true); assert!(!demoted, "test-seeking query must suppress demotion"); assert_eq!(results[0].score, 0.8); @@ -289,29 +305,19 @@ mod tests { #[test] fn no_test_chunks_reports_no_demotion() { - // Active factor, non-test query, but no test-origin chunks → nothing - // moves and the caller skips the re-sort. + // Active factor, non-test query, but no `is_test` chunks → nothing + // moves and the caller skips the re-sort. A production chunk whose PATH + // contains "test" as a substring (`src/contest.rs`) carries `is_test = + // false` from the parser and must not be demoted here. let mut results = vec![ - result("parse_config", "src/parser.rs", 0.9), - result("build_index", "src/index.rs", 0.8), + result("parse_config", "src/parser.rs", 0.9, false), + result("build_index", "src/index.rs", 0.8, false), + result("run", "src/contest.rs", 0.7, false), ]; let demoted = apply_testcode_demote(&mut results, 0.5, false); assert!(!demoted); assert_eq!(results[0].score, 0.9); assert_eq!(results[1].score, 0.8); - } - - #[test] - fn substring_trap_paths_are_not_test_origin() { - // `src/contest.rs` and `latest_results.rs` contain "test" as a - // substring but are NOT test files — the canonical classifier uses - // anchored patterns (`_test.`, `/tests/`, `.spec.`), never a bare - // substring. A production chunk in either must not be demoted. - for file in ["src/contest.rs", "src/latest_results.rs", "src/manifest.rs"] { - let mut results = vec![result("run", file, 0.9)]; - let demoted = apply_testcode_demote(&mut results, 0.5, false); - assert!(!demoted, "{file} must not read as test-origin"); - assert_eq!(results[0].score, 0.9); - } + assert_eq!(results[2].score, 0.7); } } diff --git a/src/serve/tests.rs b/src/serve/tests.rs index cd02ca4e..9ab00187 100644 --- a/src/serve/tests.rs +++ b/src/serve/tests.rs @@ -177,6 +177,7 @@ fn populated_fixture(n_chunks: usize, with_umap: bool) -> Fixture { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; // Unit-vector embedding of the store's dim — content doesn't // matter for these tests, only that upsert succeeds. @@ -295,6 +296,7 @@ fn single_chunk_fixture(content: &str, vendored: bool) -> (Fixture, String) { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; let mut v = vec![0.0f32; dim]; if !v.is_empty() { @@ -2645,6 +2647,7 @@ fn eval_gold_fixture(chunks: &[(&str, &str)], eval_json: &str) -> (Fixture, Temp window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; let mut v = vec![0.0f32; dim]; if !v.is_empty() { @@ -2872,6 +2875,7 @@ fn impact_fixture() -> (Fixture, String) { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; let mut v = vec![0.0f32; dim]; if !v.is_empty() { diff --git a/src/store/calls/dead_code.rs b/src/store/calls/dead_code.rs index 87a0c6fa..4d94cc92 100644 --- a/src/store/calls/dead_code.rs +++ b/src/store/calls/dead_code.rs @@ -1242,6 +1242,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store.upsert_chunk(&chunk, &emb, Some(12345)).unwrap(); } @@ -1291,6 +1292,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store.upsert_chunk(&css_rule, &emb, Some(1)).unwrap(); @@ -1334,6 +1336,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store.upsert_chunk(&md_func, &emb, Some(1)).unwrap(); @@ -1374,6 +1377,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store.upsert_chunk(&func_chunk, &emb, Some(12345)).unwrap(); @@ -1395,6 +1399,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store .upsert_chunk(&method_chunk, &emb, Some(12345)) @@ -1458,6 +1463,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store .upsert_chunk(&elixir_macro, &emb, Some(12345)) @@ -1508,6 +1514,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store .upsert_chunk(&recursive_macro, &emb, Some(12345)) @@ -1554,6 +1561,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store.upsert_chunk(¯o_def, &emb, Some(12345)).unwrap(); @@ -1576,6 +1584,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store.upsert_chunk(&caller, &emb, Some(12345)).unwrap(); @@ -1620,6 +1629,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store.upsert_chunk(¯o_lower, &emb, Some(12345)).unwrap(); @@ -1642,6 +1652,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store.upsert_chunk(&caller, &emb, Some(12345)).unwrap(); @@ -1695,6 +1706,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store .upsert_chunk(¯o_with_underscore, &emb, Some(12345)) @@ -1720,6 +1732,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store.upsert_chunk(&caller, &emb, Some(12345)).unwrap(); @@ -1762,6 +1775,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/src/store/chunks/async_helpers.rs b/src/store/chunks/async_helpers.rs index 847584a0..4965f6d0 100644 --- a/src/store/chunks/async_helpers.rs +++ b/src/store/chunks/async_helpers.rs @@ -340,8 +340,8 @@ pub(super) async fn batch_insert_chunks( needs_embedding: bool, ) -> Result<(), StoreError> { use crate::store::helpers::sql::max_rows_per_statement; - // 24 binds per row (canonical_hash comes after needs_embedding). - const CHUNK_INSERT_BATCH: usize = max_rows_per_statement(24); + // 25 binds per row (is_test comes after canonical_hash). + const CHUNK_INSERT_BATCH: usize = max_rows_per_statement(25); debug_assert_eq!( chunks.len(), vendored_per_chunk.len(), @@ -356,7 +356,7 @@ pub(super) async fn batch_insert_chunks( for (batch_idx, batch) in chunks.chunks(CHUNK_INSERT_BATCH).enumerate() { let emb_offset = batch_idx * CHUNK_INSERT_BATCH; let mut qb: sqlx::QueryBuilder = sqlx::QueryBuilder::new( - "INSERT INTO chunks (id, origin, source_type, language, chunk_type, name, signature, content, content_hash, doc, line_start, line_end, embedding, embedding_base, source_mtime, created_at, updated_at, parent_id, window_idx, parent_type_name, parser_version, vendored, needs_embedding, canonical_hash)", + "INSERT INTO chunks (id, origin, source_type, language, chunk_type, name, signature, content, content_hash, doc, line_start, line_end, embedding, embedding_base, source_mtime, created_at, updated_at, parent_id, window_idx, parent_type_name, parser_version, vendored, needs_embedding, canonical_hash, is_test)", ); qb.push_values(batch.iter().enumerate(), |mut b, (i, (chunk, _))| { b.push_bind(&chunk.id) @@ -421,7 +421,12 @@ pub(super) async fn batch_insert_chunks( None::<&str> } else { Some(chunk.canonical_hash.as_str()) - }); + }) + // v33: parse-time test-code flag. Decided in `extract_chunk` + // from the AST (#[test] / #[cfg(test)] attributes are + // prev-sibling nodes outside the chunk body) and the registry + // path/name patterns; read at rank time by CQS_TESTCODE_DEMOTE. + .push_bind(if chunk.is_test { 1_i64 } else { 0_i64 }); }); // ON CONFLICT upsert preserves enrichment_hash and enrichment_version. // @@ -462,13 +467,15 @@ pub(super) async fn batch_insert_chunks( vendored=excluded.vendored, \ needs_embedding=excluded.needs_embedding, \ canonical_hash=excluded.canonical_hash, \ + is_test=excluded.is_test, \ umap_x=CASE WHEN chunks.content_hash != excluded.content_hash \ THEN NULL ELSE chunks.umap_x END, \ umap_y=CASE WHEN chunks.content_hash != excluded.content_hash \ THEN NULL ELSE chunks.umap_y END \ WHERE chunks.content_hash != excluded.content_hash \ OR chunks.parser_version != excluded.parser_version \ - OR chunks.vendored != excluded.vendored", + OR chunks.vendored != excluded.vendored \ + OR chunks.is_test != excluded.is_test", ); qb.build().execute(&mut **tx).await?; } diff --git a/src/store/chunks/crud.rs b/src/store/chunks/crud.rs index 64c1f5ca..d6c39991 100644 --- a/src/store/chunks/crud.rs +++ b/src/store/chunks/crud.rs @@ -2774,6 +2774,76 @@ mod tests { ); } + /// End-to-end `is_test` wiring: a parse-time test-flagged chunk in a + /// `src/` file upserts with `is_test = 1`, hydrates back as + /// `ChunkSummary { is_test: true, .. }` through the same SELECT the search + /// path uses, and is then demoted by `apply_testcode_demote` — the full + /// chain the R@5 self-corpus crowder needs (parse-flag → store column → + /// hydrate → rank consumer). Proves the flag is not silently dropped at any + /// hop. A production sibling in the same file stays `is_test = false` and is + /// untouched by the demotion. + #[test] + fn test_upsert_round_trips_is_test_flag_and_demotes() { + use crate::search::scoring::apply_testcode_demote; + use crate::store::helpers::SearchResult; + use crate::store::ChunkSummary; + let (store, _dir) = setup_store(); + + // A cfg-test-in-src chunk: path + name both look production, so only + // the stored parse-time flag marks it test. + let mut c_test = make_chunk("schema_sql_chunks", "src/parser/chunk.rs"); + c_test.is_test = true; + let c_prod = make_chunk("parse_schema", "src/parser/chunk.rs"); + + store + .upsert_chunks_batch( + &[ + (c_test.clone(), mock_embedding(1.0)), + (c_prod.clone(), mock_embedding(1.0)), + ], + Some(100), + ) + .expect("upsert"); + + let map: std::collections::HashMap = store + .rt + .block_on(async { + store + .fetch_chunks_by_ids_async(&[c_test.id.as_str(), c_prod.id.as_str()]) + .await + }) + .expect("fetch") + .into_iter() + .map(|(id, row)| (id, ChunkSummary::from(row))) + .collect(); + + let test_summary = map.get(&c_test.id).expect("test chunk round-trips"); + let prod_summary = map.get(&c_prod.id).expect("prod chunk round-trips"); + assert!( + test_summary.is_test, + "parse-time is_test=1 must survive upsert+SELECT (the column is bound and read at ordinal 16)" + ); + assert!( + !prod_summary.is_test, + "a production sibling in the same file stays is_test=0" + ); + + // The rank consumer reads the hydrated flag: on a non-test query at + // factor 0.5 the test chunk is demoted, the production sibling is not. + let mut results = vec![ + SearchResult::new(prod_summary.clone(), 0.9), + SearchResult::new(test_summary.clone(), 0.9), + ]; + let demoted = apply_testcode_demote(&mut results, 0.5, false); + assert!(demoted, "the round-tripped test chunk must be demoted"); + assert_eq!(results[0].score, 0.9, "production sibling untouched"); + assert!( + (results[1].score - 0.45).abs() < 1e-6, + "test chunk demoted 0.9 * 0.5 = 0.45, got {}", + results[1].score + ); + } + /// An empty prefix list (operator opt-out via /// `[index].vendored_paths = []` in `.cqs.toml`) disables vendored /// detection — even chunks under `vendor/` are stored with diff --git a/src/store/chunks/embeddings.rs b/src/store/chunks/embeddings.rs index 0097ba1c..8791289c 100644 --- a/src/store/chunks/embeddings.rs +++ b/src/store/chunks/embeddings.rs @@ -285,6 +285,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/src/store/chunks/mod.rs b/src/store/chunks/mod.rs index 6136daaf..4096e051 100644 --- a/src/store/chunks/mod.rs +++ b/src/store/chunks/mod.rs @@ -46,6 +46,7 @@ pub(super) mod test_utils { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } } diff --git a/src/store/chunks/query.rs b/src/store/chunks/query.rs index 13ddab19..c9069ef7 100644 --- a/src/store/chunks/query.rs +++ b/src/store/chunks/query.rs @@ -676,7 +676,7 @@ impl Store { let _span = tracing::debug_span!("chunks_paged", after_rowid, limit).entered(); self.rt.block_on(async { // rowid appended AFTER the pinned ChunkRow columns so the - // ordinal contract for `ChunkRow::from_row` (0..15) holds. + // ordinal contract for `ChunkRow::from_row` (0..16) holds. let sql = format!( "SELECT {cols}, rowid FROM chunks WHERE rowid > ?1 \ ORDER BY rowid ASC LIMIT ?2", @@ -692,7 +692,7 @@ impl Store { let chunks: Vec = rows .iter() .map(|row| { - let rowid: i64 = row.get(16); + let rowid: i64 = row.get(17); if rowid > max_rowid { max_rowid = rowid; } diff --git a/src/store/chunks/staleness.rs b/src/store/chunks/staleness.rs index 0bf95bdd..7cc18659 100644 --- a/src/store/chunks/staleness.rs +++ b/src/store/chunks/staleness.rs @@ -1214,6 +1214,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; // Get current mtime @@ -1266,6 +1267,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; // Store with an old mtime (before the file was created) @@ -1327,6 +1329,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; // Store with None mtime (will be NULL in DB) @@ -1382,6 +1385,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; let mtime = crate::duration_to_mtime_millis( @@ -1430,6 +1434,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; let fresh_mtime = crate::duration_to_mtime_millis( @@ -1469,6 +1474,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store @@ -1527,6 +1533,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; // Stored mtime sits well above the disk mtime — the disk file was @@ -1793,6 +1800,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } @@ -2162,6 +2170,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; // Store with the exact mtime currently on disk. @@ -2227,6 +2236,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; // Store with an mtime 10_000_000 ms (~2.7 hours) in the future @@ -2293,6 +2303,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; let disk_mtime = crate::duration_to_mtime_millis( @@ -2390,6 +2401,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; let gone = make_chunk("gone", "/no/such/file.rs"); @@ -2453,6 +2465,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } }; let c1 = mk(&p1, "a", "ahash"); @@ -2509,6 +2522,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store @@ -2616,6 +2630,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store diff --git a/src/store/helpers/mod.rs b/src/store/helpers/mod.rs index 70c4e0db..06059041 100644 --- a/src/store/helpers/mod.rs +++ b/src/store/helpers/mod.rs @@ -185,7 +185,13 @@ pub(crate) fn bm25_ordering_expr() -> String { /// exclusion, so a candidate stored there would read as a false caller. This /// table is callee-name-keyed and never joined by a graph query. Empty on /// migrate; no PARSER_VERSION bump (nothing emits rows yet). -pub const CURRENT_SCHEMA_VERSION: i32 = 32; +/// - v33: chunks.is_test INTEGER NOT NULL DEFAULT 0 — parse-time test-code flag +/// (#[test]-family attribute, #[cfg(test)] / #[cfg(all(test, …))] gate, or the +/// canonical is_test_chunk registry patterns). Read at rank time by +/// CQS_TESTCODE_DEMOTE so it sees #[cfg(test)]-in-src/ chunks whose attribute +/// text is outside the chunk body. Pre-v33 rows default 0 until the +/// PARSER_VERSION 17→18 bump re-parses them and populates the real flag. +pub const CURRENT_SCHEMA_VERSION: i32 = 33; /// Default model name for metadata checks (used by test-only `check_model_version`). /// Canonical definition is `embedder::DEFAULT_MODEL_REPO`. diff --git a/src/store/helpers/rows.rs b/src/store/helpers/rows.rs index 154f9402..adc9f3c4 100644 --- a/src/store/helpers/rows.rs +++ b/src/store/helpers/rows.rs @@ -11,22 +11,22 @@ /// 0 id, 1 origin, 2 language, 3 chunk_type, 4 name, 5 signature, /// 6 content, 7 doc, 8 line_start, 9 line_end, 10 content_hash, /// 11 window_idx, 12 parent_id, 13 parent_type_name, -/// 14 parser_version, 15 vendored +/// 14 parser_version, 15 vendored, 16 is_test /// /// SELECTs that need additional columns (rowid, embedding, target_type_name) -/// must append them AFTER the 16 pinned columns so the ChunkRow ordinals -/// stay stable. +/// must append them AFTER the 17 pinned columns so the ChunkRow ordinals +/// stay stable — appended columns are read by ordinal 17+. pub(crate) const CHUNK_ROW_SELECT_COLUMNS: &str = "id, origin, language, chunk_type, name, signature, content, doc, \ line_start, line_end, content_hash, window_idx, parent_id, parent_type_name, \ - parser_version, vendored"; + parser_version, vendored, is_test"; /// `c.`-prefixed variant of [`CHUNK_ROW_SELECT_COLUMNS`] for joins where /// `chunks` is aliased as `c`. Same ordinal contract. pub(crate) const CHUNK_ROW_SELECT_COLUMNS_PREFIXED: &str = "c.id, c.origin, c.language, c.chunk_type, c.name, c.signature, c.content, c.doc, \ c.line_start, c.line_end, c.content_hash, c.window_idx, c.parent_id, c.parent_type_name, \ - c.parser_version, c.vendored"; + c.parser_version, c.vendored, c.is_test"; /// Clamp i64 to valid u32 line number range (1-indexed) /// @@ -91,15 +91,19 @@ pub(crate) struct ChunkRow { /// `try_get` so SELECTs that omit the column still construct a valid row /// (defaults to false). pub vendored: bool, + /// Parse-time test-code flag (`chunks.is_test`, v33). `try_get` so a SELECT + /// that omits the column still constructs a valid row (defaults to false); + /// read at rank time by the `CQS_TESTCODE_DEMOTE` knob. + pub is_test: bool, } impl ChunkRow { /// Construct from a SQLite row whose SELECT list begins with the pinned - /// 16 columns from [`CHUNK_ROW_SELECT_COLUMNS`] (or its prefixed sibling). + /// 17 columns from [`CHUNK_ROW_SELECT_COLUMNS`] (or its prefixed sibling). /// Reads via ordinal access — no column-name strcmp scan per row. /// /// SELECTs may append additional columns (rowid, embedding, - /// target_type_name) AFTER ordinal 15; those are read by their named + /// target_type_name) AFTER ordinal 16; those are read by their named /// callers, not here. pub(crate) fn from_row(row: &sqlx::sqlite::SqliteRow) -> Self { use sqlx::Row; @@ -126,6 +130,11 @@ impl ChunkRow { let v: i64 = row.get(15); v != 0 }, + // `try_get` so a SELECT that predates the pinned-column update (or + // deliberately omits is_test) still hydrates, defaulting to + // not-test. All standard SELECTs use CHUNK_ROW_SELECT_COLUMNS, which + // carries it at ordinal 16. + is_test: row.try_get::(16).map(|v| v != 0).unwrap_or(false), } } @@ -156,6 +165,10 @@ impl ChunkRow { .try_get::("vendored") .map(|v| v != 0) .unwrap_or(false), + is_test: row + .try_get::("is_test") + .map(|v| v != 0) + .unwrap_or(false), } } @@ -185,6 +198,7 @@ impl ChunkRow { parent_type_name: None, parser_version: 0, vendored: false, + is_test: false, } } } diff --git a/src/store/helpers/types.rs b/src/store/helpers/types.rs index d16810d6..8e949598 100644 --- a/src/store/helpers/types.rs +++ b/src/store/helpers/types.rs @@ -82,6 +82,13 @@ pub struct ChunkSummary { /// SELECT omits the column. #[serde(default, skip_serializing_if = "crate::serde_helpers::is_false")] pub vendored: bool, + /// Parse-time test-code flag (`chunks.is_test`, v33). Read at rank time by + /// the `CQS_TESTCODE_DEMOTE` knob (`search::scoring::testcode`) so the + /// demotion sees `#[cfg(test)]`-in-`src/` chunks whose attribute text is + /// outside the chunk body. Defaults to false when the loading SELECT omits + /// the column. + #[serde(default, skip_serializing_if = "crate::serde_helpers::is_false")] + pub is_test: bool, } #[inline] @@ -117,6 +124,8 @@ impl From<&ChunkSummary> for Chunk { // when the source row was loaded by a SELECT that omitted // `parser_version`, in which case the next reindex will rewrite it. parser_version: cs.parser_version, + // Preserve the parse-time test flag on round-trip. + is_test: cs.is_test, } } } @@ -156,6 +165,7 @@ impl From for ChunkSummary { parent_type_name: row.parent_type_name, parser_version: row.parser_version, vendored: row.vendored, + is_test: row.is_test, } } } @@ -842,6 +852,7 @@ mod tests { window_idx: None, parser_version: 0, vendored: false, + is_test: false, } } @@ -905,6 +916,7 @@ mod tests { window_idx: None, parser_version: 0, vendored: false, + is_test: false, }, 0.9375, ) diff --git a/src/store/migrations.rs b/src/store/migrations.rs index 1f8543df..5f9b55e6 100644 --- a/src/store/migrations.rs +++ b/src/store/migrations.rs @@ -402,6 +402,7 @@ const MIGRATIONS: &[(i32, i32, MigrationFn)] = &[ (29, 30, |c| Box::pin(migrate_v29_to_v30(c))), (30, 31, |c| Box::pin(migrate_v30_to_v31(c))), (31, 32, |c| Box::pin(migrate_v31_to_v32(c))), + (32, 33, |c| Box::pin(migrate_v32_to_v33(c))), ]; /// Run a single migration step @@ -1299,6 +1300,37 @@ async fn migrate_v31_to_v32(conn: &mut sqlx::SqliteConnection) -> Result<(), Sto Ok(()) } +/// v32 → v33: add `chunks.is_test` (INTEGER NOT NULL DEFAULT 0). +/// +/// The parse-time test-code flag. Set when a Rust item carries a `#[test]`- +/// family attribute, when the item or any ancestor is gated by `#[cfg(test)]` / +/// `#[cfg(all(test, …))]`, or when the canonical `is_test_chunk` path/name +/// registry patterns match. Read at rank time by `CQS_TESTCODE_DEMOTE` +/// (`search::scoring::testcode`) so the demotion sees `#[cfg(test)]`-in-`src/` +/// chunks whose attribute text lives OUTSIDE the chunk body and is therefore +/// invisible to any content sniff. +/// +/// Additive `ALTER TABLE ADD COLUMN`; pre-v33 rows read the `0` default (not +/// test) until the PARSER_VERSION bump (17→18) makes every chunk stale and a +/// plain reindex re-parses it, populating the real flag (no `--force` needed). +/// A stale-flag `0` on a genuine test chunk only under-demotes until reindex — +/// it never mis-hides a production chunk. +async fn migrate_v32_to_v33(conn: &mut sqlx::SqliteConnection) -> Result<(), StoreError> { + let _span = tracing::info_span!("migrate_v32_to_v33").entered(); + + sqlx::query("ALTER TABLE chunks ADD COLUMN is_test INTEGER NOT NULL DEFAULT 0") + .execute(&mut *conn) + .await?; + + tracing::info!( + "Migrated to v33: chunks.is_test column (default 0). Pre-v33 rows read 0 \ + until the PARSER_VERSION bump (17→18) makes them stale and a plain \ + reindex re-parses them to populate the parse-time test-code flag \ + (no --force needed)." + ); + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -1331,7 +1363,7 @@ mod tests { #[test] fn test_current_schema_version_documented() { // Ensure the current version matches what we document - assert_eq!(CURRENT_SCHEMA_VERSION, 32); + assert_eq!(CURRENT_SCHEMA_VERSION, 33); } #[test] @@ -4994,6 +5026,146 @@ mod tests { }); } + /// Build a minimal v32 schema: metadata + a v32-shaped `chunks` table + /// WITHOUT the `is_test` column, stamped at version 32. Only the columns the + /// v32→v33 step touches matter (it just `ALTER TABLE chunks ADD COLUMN`), so + /// the table carries a bare `id`/`name` shape a pre-migration row can use. + async fn setup_v32_schema(db_path: &std::path::Path) -> SqlitePool { + let pool = SqlitePoolOptions::new() + .max_connections(1) + .connect_with( + sqlx::sqlite::SqliteConnectOptions::new() + .filename(db_path) + .create_if_missing(true) + .foreign_keys(true), + ) + .await + .unwrap(); + sqlx::query("CREATE TABLE metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL)") + .execute(&pool) + .await + .unwrap(); + // Pre-v33 chunks: no is_test column. + sqlx::query("CREATE TABLE chunks (id TEXT PRIMARY KEY, name TEXT NOT NULL)") + .execute(&pool) + .await + .unwrap(); + sqlx::query("INSERT INTO metadata (key, value) VALUES ('schema_version', '32')") + .execute(&pool) + .await + .unwrap(); + pool + } + + /// v32 → v33 adds `chunks.is_test` with a `0` default. A pre-existing row + /// (inserted before the migration, when the column did not exist) must read + /// back `is_test = 0` — the legacy-state guarantee that an old DB's chunks + /// are treated as not-test until the PARSER_VERSION bump re-parses them — and + /// a fresh insert can carry `is_test = 1`. + #[test] + fn test_migrate_v32_to_v33_adds_is_test_column() { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let dir = tempfile::tempdir().unwrap(); + let db_path = dir.path().join("test.db"); + + rt.block_on(async { + let pool = setup_v32_schema(&db_path).await; + + // Insert a pre-migration row (no is_test column yet). + sqlx::query("INSERT INTO chunks (id, name) VALUES ('src/a.rs:1:x', 'foo')") + .execute(&pool) + .await + .unwrap(); + + let pool = migrate(pool, &db_path, 32, 33).await.unwrap(); + + // Pre-migration row defaults to 0 (not test). + let (is_test,): (i64,) = + sqlx::query_as("SELECT is_test FROM chunks WHERE id = 'src/a.rs:1:x'") + .fetch_one(&pool) + .await + .unwrap(); + assert_eq!( + is_test, 0, + "pre-v33 chunk row must default to is_test = 0 until reindex" + ); + + // A fresh insert can carry is_test = 1. + sqlx::query("INSERT INTO chunks (id, name, is_test) VALUES ('src/b.rs:1:y', 'bar', 1)") + .execute(&pool) + .await + .unwrap(); + let (is_test2,): (i64,) = + sqlx::query_as("SELECT is_test FROM chunks WHERE id = 'src/b.rs:1:y'") + .fetch_one(&pool) + .await + .unwrap(); + assert_eq!(is_test2, 1); + }); + } + + /// Fresh-create (schema.sql) and migrated (v32 → v33) must both carry the + /// `chunks.is_test` column. The headline `test_fresh_init_equals_full_migration_from_v10` + /// covers full-shape parity structurally; this names the exact new surface. + #[test] + fn test_v33_fresh_create_matches_migrated_shape() { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + rt.block_on(async { + // Fresh: apply schema.sql and read the chunks DDL. + let fresh = SqlitePoolOptions::new() + .max_connections(1) + .connect_with( + sqlx::sqlite::SqliteConnectOptions::new() + .filename(":memory:") + .foreign_keys(true), + ) + .await + .unwrap(); + for stmt in crate::store::split_sql_statements(include_str!("../schema.sql")) { + if stmt.is_empty() { + continue; + } + sqlx::query(sqlx::AssertSqlSafe(stmt.as_str())) + .execute(&fresh) + .await + .unwrap(); + } + let (fresh_sql,): (String,) = sqlx::query_as( + "SELECT sql FROM sqlite_master WHERE type='table' AND name='chunks'", + ) + .fetch_one(&fresh) + .await + .unwrap(); + assert!( + fresh_sql.contains("is_test"), + "fresh chunks must carry is_test, got: {fresh_sql}" + ); + + // Migrated: build v32, migrate to v33, read the same. + let dir = tempfile::tempdir().unwrap(); + let db_path = dir.path().join("test.db"); + let pool = setup_v32_schema(&db_path).await; + let pool = migrate(pool, &db_path, 32, 33).await.unwrap(); + let (mig_sql,): (String,) = sqlx::query_as( + "SELECT sql FROM sqlite_master WHERE type='table' AND name='chunks'", + ) + .fetch_one(&pool) + .await + .unwrap(); + assert!( + mig_sql.contains("is_test"), + "migrated chunks must carry is_test, got: {mig_sql}" + ); + }); + } + /// FROZEN-ARTIFACT full-chain guard (legacy-state / version-null shape). /// /// Every other migration test in this module hand-builds a *minimal* diff --git a/src/store/mod.rs b/src/store/mod.rs index b46fea1d..dd4acba3 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -2256,6 +2256,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; let mut v = vec![0.0f32; writer.dim()]; if !v.is_empty() { @@ -2526,6 +2527,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; // A unit-vector embedding of the store's configured dim. let mut v = vec![0.0f32; store.dim()]; diff --git a/src/store/summary_queue.rs b/src/store/summary_queue.rs index f7aa5dc0..02908b04 100644 --- a/src/store/summary_queue.rs +++ b/src/store/summary_queue.rs @@ -605,6 +605,7 @@ mod tests { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, }; let mut emb_vec = vec![0.0_f32; EMBEDDING_DIM]; emb_vec[0] = 1.0; diff --git a/src/store/types.rs b/src/store/types.rs index a62ddcd4..2c7c7a31 100644 --- a/src/store/types.rs +++ b/src/store/types.rs @@ -395,8 +395,8 @@ impl Store { for batch in type_names.chunks(BATCH_SIZE) { let placeholders = super::helpers::make_placeholders(batch.len()); // target_type_name appended AFTER the pinned ChunkRow columns - // so the ordinal contract for `from_row` (0..15) holds; read - // by ordinal 16. + // so the ordinal contract for `from_row` (0..16) holds; read + // by ordinal 17. let sql = format!( "SELECT {cols}, te.target_type_name \ FROM type_edges te \ @@ -412,7 +412,7 @@ impl Store { let rows: Vec<_> = q.fetch_all(&self.pool).await?; for row in rows { use sqlx::Row; - let type_name: String = row.get(16); + let type_name: String = row.get(17); let chunk = ChunkSummary::from(ChunkRow::from_row(&row)); result.entry(type_name).or_default().push(chunk); } diff --git a/src/suggest.rs b/src/suggest.rs index 2ee6af79..e85eff3e 100644 --- a/src/suggest.rs +++ b/src/suggest.rs @@ -438,6 +438,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store .upsert_chunk( @@ -516,6 +517,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store .upsert_chunk( @@ -583,6 +585,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store .upsert_chunk( @@ -617,6 +620,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store .upsert_chunk( @@ -693,6 +697,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store .upsert_chunk( @@ -726,6 +731,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store .upsert_chunk( @@ -777,6 +783,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store .upsert_chunk( diff --git a/src/task.rs b/src/task.rs index cf6935fd..47a50a3d 100644 --- a/src/task.rs +++ b/src/task.rs @@ -774,6 +774,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/src/where_to_add.rs b/src/where_to_add.rs index 60d2b6fc..2d5b380b 100644 --- a/src/where_to_add.rs +++ b/src/where_to_add.rs @@ -914,11 +914,14 @@ mod tests { use crate::parser::ChunkType; /// Build a `ChunkSummary` for a function chunk, with default test - /// values for file path, lines, and the optional fields. + /// values for file path, lines, and the optional fields. The path is a + /// neutral production file — NOT `test.rs`, which the registry classifies + /// as test code (so `detect_naming_convention` would skip every chunk and + /// these naming-detection assertions would read the empty-count default). fn make_chunk(name: &str, sig: &str, content: &str, lang: Language) -> ChunkSummary { ChunkSummary { id: format!("id-{name}"), - file: PathBuf::from("src/test.rs"), + file: PathBuf::from("src/module.rs"), language: lang, chunk_type: ChunkType::Function, name: name.to_string(), @@ -932,6 +935,7 @@ mod tests { content_hash: String::new(), window_idx: None, parser_version: 0, + is_test: false, vendored: false, } } diff --git a/src/worktree_overlay.rs b/src/worktree_overlay.rs index a89ca6ec..85f6f4be 100644 --- a/src/worktree_overlay.rs +++ b/src/worktree_overlay.rs @@ -1785,6 +1785,7 @@ mod tests { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, vendored: false, }; SearchResult::new(summary, score) @@ -1818,6 +1819,7 @@ mod tests { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store .upsert_chunks_batch(&[(chunk, one_hot(*slot))], Some(0)) diff --git a/tests/ci_test.rs b/tests/ci_test.rs index 9e457cba..11746454 100644 --- a/tests/ci_test.rs +++ b/tests/ci_test.rs @@ -32,6 +32,7 @@ fn chunk_at(name: &str, file: &str, line_start: u32, line_end: u32) -> Chunk { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } @@ -57,6 +58,7 @@ fn test_chunk_at(name: &str, file: &str, line_start: u32, line_end: u32) -> Chun window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/tests/cli_dead_test.rs b/tests/cli_dead_test.rs index d0c8efaa..7751a370 100644 --- a/tests/cli_dead_test.rs +++ b/tests/cli_dead_test.rs @@ -65,6 +65,7 @@ fn make_chunk(id: &str, name: &str, content: &str) -> Chunk { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/tests/cli_doc_injection_relay_test.rs b/tests/cli_doc_injection_relay_test.rs index d48f8cab..3bbc6247 100644 --- a/tests/cli_doc_injection_relay_test.rs +++ b/tests/cli_doc_injection_relay_test.rs @@ -92,6 +92,7 @@ fn seed_doc_store(doc_payload: &str) -> TempDir { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; let emb = dummy_embedding(); store.upsert_chunk(&chunk, &emb, Some(1)).expect("upsert"); diff --git a/tests/cli_eval_freshness_gate_test.rs b/tests/cli_eval_freshness_gate_test.rs index 2fc0d908..058e1741 100644 --- a/tests/cli_eval_freshness_gate_test.rs +++ b/tests/cli_eval_freshness_gate_test.rs @@ -203,6 +203,7 @@ fn setup_project() -> (TempDir, PathBuf) { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; let pairs = vec![(chunk, mock_embedding(1.0_f32))]; store diff --git a/tests/cli_explain_test.rs b/tests/cli_explain_test.rs index 8a6106b1..42de6a9c 100644 --- a/tests/cli_explain_test.rs +++ b/tests/cli_explain_test.rs @@ -65,6 +65,7 @@ fn make_chunk(id: &str, name: &str, content: &str) -> Chunk { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/tests/cli_surface_test.rs b/tests/cli_surface_test.rs index be7111a1..46059437 100644 --- a/tests/cli_surface_test.rs +++ b/tests/cli_surface_test.rs @@ -340,6 +340,7 @@ fn seed_review_store(dir: &std::path::Path) { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } }; @@ -603,6 +604,7 @@ fn seed_session_project(dir: &std::path::Path) { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } }; diff --git a/tests/cli_trace_cross_project_test.rs b/tests/cli_trace_cross_project_test.rs index 76ebc0cb..1ae8d43f 100644 --- a/tests/cli_trace_cross_project_test.rs +++ b/tests/cli_trace_cross_project_test.rs @@ -62,6 +62,7 @@ fn make_chunk(id: &str, name: &str, content: &str) -> Chunk { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 149789dd..63eea1ab 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -103,13 +103,19 @@ impl std::ops::Deref for TestStore { } } -/// Create a test chunk with sensible defaults +/// Create a test chunk with sensible defaults. +/// +/// The origin is a NEUTRAL production path (`src/lib.rs`), not `test.rs`: the +/// registry now classifies a `test.rs` / `tests.rs` filename as test code, so a +/// `test.rs` origin would make every fixture chunk read as a test and drop out +/// of dead-code / importance / scout analysis. Tests that specifically want a +/// test-classified chunk set `is_test` or the path explicitly. #[allow(dead_code)] pub fn test_chunk(name: &str, content: &str) -> Chunk { let hash = blake3::hash(content.as_bytes()).to_hex().to_string(); Chunk { - id: format!("test.rs:1:{}", &hash[..8]), - file: PathBuf::from("test.rs"), + id: format!("src/lib.rs:1:{}", &hash[..8]), + file: PathBuf::from("src/lib.rs"), language: Language::Rust, chunk_type: ChunkType::Function, name: name.to_string(), @@ -125,6 +131,7 @@ pub fn test_chunk(name: &str, content: &str) -> Chunk { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/tests/cross_project_test.rs b/tests/cross_project_test.rs index bfc0b11d..042d3df9 100644 --- a/tests/cross_project_test.rs +++ b/tests/cross_project_test.rs @@ -57,6 +57,7 @@ fn insert_chunk_and_call( parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, }; let embedding = cqs::Embedding::new(vec![0.0f32; store.dim()]); store.upsert_chunks_batch(&[(chunk, embedding)], None)?; diff --git a/tests/eval_subcommand_test.rs b/tests/eval_subcommand_test.rs index b4d789c2..5c325f98 100644 --- a/tests/eval_subcommand_test.rs +++ b/tests/eval_subcommand_test.rs @@ -53,6 +53,7 @@ fn build_chunk(name: &str, file: &str, line_start: u32) -> Chunk { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/tests/eval_test.rs b/tests/eval_test.rs index 3e3aa729..404cb3f2 100644 --- a/tests/eval_test.rs +++ b/tests/eval_test.rs @@ -223,6 +223,7 @@ fn seed_chunk(name: &str, file: &str, hash: &str) -> Chunk { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/tests/impact_diff_test.rs b/tests/impact_diff_test.rs index fc6ac8bc..97f07b76 100644 --- a/tests/impact_diff_test.rs +++ b/tests/impact_diff_test.rs @@ -30,6 +30,7 @@ fn chunk_at(name: &str, file: &str, line_start: u32, line_end: u32) -> Chunk { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } @@ -296,6 +297,7 @@ fn test_chunk_at(name: &str, file: &str, line_start: u32, line_end: u32) -> Chun window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/tests/impact_test.rs b/tests/impact_test.rs index 90eca42b..bd436f7f 100644 --- a/tests/impact_test.rs +++ b/tests/impact_test.rs @@ -29,6 +29,7 @@ fn chunk_at(name: &str, file: &str, line_start: u32, line_end: u32) -> Chunk { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } @@ -54,6 +55,7 @@ fn test_chunk_at(name: &str, file: &str, line_start: u32, line_end: u32) -> Chun window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/tests/legacy_parser_v8_chunk_id_migration_test.rs b/tests/legacy_parser_v8_chunk_id_migration_test.rs index 351b39b3..87fbf5c0 100644 --- a/tests/legacy_parser_v8_chunk_id_migration_test.rs +++ b/tests/legacy_parser_v8_chunk_id_migration_test.rs @@ -95,6 +95,7 @@ fn legacy_md_row(origin: &str, id: &str, name: &str, line_start: u32, byte_start parent_type_name: None, // The defining fact: this row was written by parser version 8. parser_version: 8, + is_test: false, } } diff --git a/tests/legacy_v10_full_chain_migration_test.rs b/tests/legacy_v10_full_chain_migration_test.rs index 3571179d..aefb1736 100644 --- a/tests/legacy_v10_full_chain_migration_test.rs +++ b/tests/legacy_v10_full_chain_migration_test.rs @@ -1,11 +1,11 @@ //! Frozen-artifact guard: the FULL schema migration chain from the OLDEST -//! migratable version (v10) to current (v32), run against a hand-built v10 DB. +//! migratable version (v10) to current (v33), run against a hand-built v10 DB. //! //! # The version boundary //! //! Every fresh-state fixture is born at the current schema. The existing //! migration tests each exercise ONE step (or at most a 3-step v12→v14 chain) -//! against a minimal per-step setup helper. NONE runs the complete v10→v32 +//! against a minimal per-step setup helper. NONE runs the complete v10→v33 //! chain against a faithfully-seeded oldest DB. That chain crosses every //! destructive in-place TABLE REBUILD — v15→v16 (llm_summaries composite PK), //! v18→v19 (sparse_vectors FK), v28→v29 (notes CHECK + clamp) — plus the recent @@ -30,11 +30,11 @@ //! - `type_edges`, `sparse_vectors`, `llm_summaries`, `file_registry`, //! `candidate_edges` all ABSENT (created by later migrations). //! -//! The current code can never emit this DB: it always writes schema 32. +//! The current code can never emit this DB: it always writes the current schema. //! //! # What it asserts (current code reads/migrates the old shape correctly) //! -//! (1) `Store::open` runs the whole v10→v32 chain without error and stamps 32. +//! (1) `Store::open` runs the whole v10→v33 chain without error and stamps 33. //! (2) Every v10 chunk row SURVIVES the chain (no silent loss across the //! three destructive rebuilds). `chunk_count` and the rows match. //! (3) The off-grid note (-0.8) is CLAMPED to the nearest legal discrete @@ -261,7 +261,7 @@ fn build_v10_artifact(db_path: &Path) { }); } -/// GUARD: the full v10→v32 chain reads/migrates the frozen v10 artifact +/// GUARD: the full v10→v33 chain reads/migrates the frozen v10 artifact /// correctly through the real `Store::open` path (which auto-migrates). /// /// Calibration (RED proof): mutate the v28→v29 clamp in @@ -279,17 +279,19 @@ fn v10_full_chain_migrates_and_reads_legacy_shapes() { build_v10_artifact(&db_path); // (1) Open through the real path — this runs check_and_migrate_schema, i.e. - // the entire v10→v32 chain. A mis-migration surfaces as an Err here. - let store = Store::open(&db_path).expect("Store::open must migrate v10 → v32 without error"); + // the entire v10→current chain. A mis-migration surfaces as an Err here. + let store = + Store::open(&db_path).expect("Store::open must migrate v10 → current without error"); - // schema_version is stamped 32. (Probe the file directly; Store is open - // read-write but we re-read via a separate read-only connection.) + // schema_version is stamped at the current version. (Probe the file + // directly; Store is open read-write but we re-read via a separate + // read-only connection.) let sv = query_scalar_string( &db_path, "SELECT value FROM metadata WHERE key = 'schema_version'".to_string(), ) .expect("schema_version row must exist after migrate"); - assert_eq!(sv, "32", "full chain must stamp schema_version = 32"); + assert_eq!(sv, "33", "full chain must stamp schema_version = 33"); // (2) Every v10 chunk row survived all three destructive rebuilds. let count = store.chunk_count().expect("chunk_count after migrate"); @@ -305,6 +307,13 @@ fn v10_full_chain_migrates_and_reads_legacy_shapes() { names.contains("foo") && names.contains("bar"), "both legacy chunk rows must be queryable after migrate; got {names:?}" ); + // The v32→v33 `is_test` ADD COLUMN backfills the `0` default on every + // pre-existing row — a v10 chunk migrated all the way to v33 reads as + // not-test until a reindex re-parses it (the legacy-state guarantee). + assert!( + chunks.iter().all(|c| !c.is_test), + "migrated legacy chunks must default to is_test = false pre-reparse" + ); // (3) The off-grid note (-0.8) is clamped to the nearest legal value (-1.0) // by the v28→v29 legacy-read clamp; the on-grid note (0.5) is unchanged. @@ -350,7 +359,7 @@ fn v10_full_chain_creates_all_later_tables() { let db_path = tmp.path().join("index.db"); build_v10_artifact(&db_path); - let _store = Store::open(&db_path).expect("Store::open must migrate v10 → v32"); + let _store = Store::open(&db_path).expect("Store::open must migrate v10 → v33"); for table in [ "type_edges", // v10→v11 @@ -366,7 +375,7 @@ fn v10_full_chain_creates_all_later_tables() { assert_eq!( found.as_deref(), Some(table), - "table {table} must exist after the full v10→v32 chain" + "table {table} must exist after the full v10→v33 chain" ); } } diff --git a/tests/local_provider_integration.rs b/tests/local_provider_integration.rs index 6799c1cc..44395782 100644 --- a/tests/local_provider_integration.rs +++ b/tests/local_provider_integration.rs @@ -72,6 +72,7 @@ fn insert_callable_chunk(store: &Store, content_hash: &str, name: &str) { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, }; store.upsert_chunk(&chunk, &embedding, None).unwrap(); diff --git a/tests/model_eval.rs b/tests/model_eval.rs index 6b806074..a34e7583 100644 --- a/tests/model_eval.rs +++ b/tests/model_eval.rs @@ -1502,6 +1502,7 @@ fn test_hard_reranker_comparison() { parent_id: None, parent_type_name: None, parser_version: 0, + is_test: false, vendored: false, }, *score, diff --git a/tests/reference_test.rs b/tests/reference_test.rs index 167318f0..463b55fc 100644 --- a/tests/reference_test.rs +++ b/tests/reference_test.rs @@ -31,6 +31,7 @@ fn make_code_result(name: &str, score: f32) -> SearchResult { content_hash: String::new(), window_idx: None, parser_version: 0, + is_test: false, vendored: false, }, score, diff --git a/tests/related_test.rs b/tests/related_test.rs index 3bbc06fd..79d746e7 100644 --- a/tests/related_test.rs +++ b/tests/related_test.rs @@ -31,6 +31,7 @@ fn chunk_at(name: &str, file: &str, line_start: u32, line_end: u32, sig: &str) - window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/tests/review_test.rs b/tests/review_test.rs index 4512c5b0..350af1bc 100644 --- a/tests/review_test.rs +++ b/tests/review_test.rs @@ -33,6 +33,7 @@ fn chunk_at(name: &str, file: &str, line_start: u32, line_end: u32) -> Chunk { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } @@ -58,6 +59,7 @@ fn test_chunk_at(name: &str, file: &str, line_start: u32, line_end: u32) -> Chun window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/tests/search_test.rs b/tests/search_test.rs index 1f53ba2e..f75012b0 100644 --- a/tests/search_test.rs +++ b/tests/search_test.rs @@ -68,6 +68,7 @@ fn chunk_with_path(name: &str, file: &str, lang: Language) -> cqs::Chunk { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } @@ -873,6 +874,7 @@ fn chunk_with_real_id(name: &str, file: &str, byte_start: u32, lang: Language) - window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } } diff --git a/tests/store_calls_test.rs b/tests/store_calls_test.rs index 05aac500..094d5787 100644 --- a/tests/store_calls_test.rs +++ b/tests/store_calls_test.rs @@ -1045,6 +1045,7 @@ fn method_chunk( window_idx: None, parent_type_name: parent_type.map(String::from), parser_version: 0, + is_test: false, } } diff --git a/tests/store_test.rs b/tests/store_test.rs index 1d5d839a..83342f49 100644 --- a/tests/store_test.rs +++ b/tests/store_test.rs @@ -17,7 +17,7 @@ fn test_store_init() { let stats = store.stats().unwrap(); assert_eq!(stats.total_chunks, 0); assert_eq!(stats.total_files, 0); - assert_eq!(stats.schema_version, 32); // v32: candidate_edges side-table (call-graph candidates that must not surface as callers) + assert_eq!(stats.schema_version, 33); // v33: chunks.is_test parse-time flag assert_eq!(stats.model_name, cqs::embedder::DEFAULT_MODEL_REPO); } @@ -138,7 +138,9 @@ fn test_delete_by_origin() { .unwrap(); // Delete chunks from test.rs - let deleted = store.delete_by_origin(&PathBuf::from("test.rs")).unwrap(); + let deleted = store + .delete_by_origin(&PathBuf::from("src/lib.rs")) + .unwrap(); assert_eq!(deleted, 1); // Only chunk2 should remain @@ -149,7 +151,9 @@ fn test_delete_by_origin() { assert_eq!(results[0].chunk.name, "fn2"); // Deleting again should return 0 - let deleted_again = store.delete_by_origin(&PathBuf::from("test.rs")).unwrap(); + let deleted_again = store + .delete_by_origin(&PathBuf::from("src/lib.rs")) + .unwrap(); assert_eq!(deleted_again, 0); } @@ -173,7 +177,7 @@ fn test_prune_missing() { // Prune with only test.rs existing. Use a nonexistent temp path as // `root` — neither test.rs nor other.rs live on disk, so the filesystem // fallback in `origin_exists` resolves to false for anything not in the set. - let existing: HashSet = vec![PathBuf::from("test.rs")].into_iter().collect(); + let existing: HashSet = vec![PathBuf::from("src/lib.rs")].into_iter().collect(); let fake_root = std::env::temp_dir().join("cqs-prune-missing-test-root-nonexistent"); let pruned = store.prune_missing(&existing, &fake_root).unwrap(); @@ -319,7 +323,7 @@ fn test_fts_search() { ); assert!(results .iter() - .any(|id| id.contains("parseConfigFile") || id.starts_with("test.rs"))); + .any(|id| id.contains("parseConfigFile") || id.starts_with("src/lib.rs"))); // FTS search for "parse file" should also find parseConfigFile (normalized) let results = store.search_fts("parse file", 5).unwrap(); @@ -796,7 +800,7 @@ fn test_get_call_graph() { }, ]; store - .upsert_function_calls(&PathBuf::from("test.rs"), &function_calls) + .upsert_function_calls(&PathBuf::from("src/lib.rs"), &function_calls) .unwrap(); // Get call graph @@ -859,7 +863,7 @@ fn test_all_chunk_identities() { // Find chunk1 identity let id1 = identities.iter().find(|i| i.name == "fn1").unwrap(); - assert_eq!(id1.file.to_string_lossy(), "test.rs"); + assert_eq!(id1.file.to_string_lossy(), "src/lib.rs"); assert_eq!(id1.language, Language::Rust); assert_eq!(id1.line_start, 1); @@ -1097,6 +1101,7 @@ fn test_check_origins_stale_across_batch_boundary() { window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, }; // First half stores a divergent old mtime (stale); second half @@ -1178,7 +1183,7 @@ fn test_open_readonly_on_initialized_store() { let ro = cqs::store::Store::open_readonly(&db_path).unwrap(); let stats = ro.stats().unwrap(); assert_eq!(stats.total_chunks, 0); - assert_eq!(stats.schema_version, 32); + assert_eq!(stats.schema_version, 33); assert_eq!(stats.model_name, cqs::embedder::DEFAULT_MODEL_REPO); } diff --git a/tests/task_test.rs b/tests/task_test.rs index e19c93aa..ee8b5d01 100644 --- a/tests/task_test.rs +++ b/tests/task_test.rs @@ -257,6 +257,7 @@ fn test_compute_risk_and_tests_integration() { content_hash: String::new(), window_idx: None, parser_version: 0, + is_test: false, vendored: false, }]; diff --git a/tests/where_test.rs b/tests/where_test.rs index f984f749..04deba87 100644 --- a/tests/where_test.rs +++ b/tests/where_test.rs @@ -50,6 +50,7 @@ fn placement_chunk_with_lang( window_idx: None, parent_type_name: None, parser_version: 0, + is_test: false, } }