Skip to content

Commit 7d601b7

Browse files
authored
fix(grep): report truthful insufficient-result stats (#9)
* fix(grep): report truthful insufficient-result stats * test(grep): prove insufficient KG preservation
1 parent 6ce5099 commit 7d601b7

4 files changed

Lines changed: 111 additions & 5 deletions

File tree

crates/terraphim_grep/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ Query
113113
├── Sufficient ──→ Return chunks (SearchOnly)
114114
├── NeedsSynthesis ──→ RLM fallback (if LLM configured)
115115
├── NeedsExpansion ──→ RLM fallback with additional chunks
116-
└── Insufficient ──→ Return empty (RlmInsufficient)
116+
└── Insufficient ──→ Preserve retrieved chunks + KG metadata (RlmInsufficient)
117117
```
118118

119119
## Key Types

crates/terraphim_grep/src/lib.rs

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,17 +158,20 @@ impl TerraphimGrep {
158158
.await
159159
}
160160
sufficiency_judge::Sufficiency::Insufficient(chunks) => {
161+
// Preserve the retrieved chunks and KG concepts and derive the
162+
// counters from the actual vectors so the JSON stats stay
163+
// truthful even when the judge deems the result insufficient.
161164
let stats = GrepStats {
162165
search_latency_ms,
163166
rlm_latency_ms: None,
164-
chunks_returned: 0,
165-
kg_hits: 0,
167+
chunks_returned: chunks.len(),
168+
kg_hits: hybrid_results.kg_concepts.len(),
166169
};
167170

168171
Ok(GrepResult {
169172
chunks,
170173
answer: None,
171-
concepts: vec![],
174+
concepts: hybrid_results.kg_concepts,
172175
sufficiency: SufficiencyState::RlmInsufficient,
173176
stats,
174177
})
@@ -333,7 +336,7 @@ impl TerraphimGrep {
333336
mod tests {
334337
use super::*;
335338
#[cfg(feature = "code-search")]
336-
use terraphim_types::Thesaurus;
339+
use terraphim_types::{NormalizedTerm, NormalizedTermValue, Thesaurus};
337340

338341
#[test]
339342
fn test_grep_options_default() {
@@ -438,6 +441,71 @@ mod tests {
438441
assert_eq!(result.stats.kg_hits, 0);
439442
}
440443

444+
/// When the sufficiency judge returns `Insufficient` with non-empty chunks
445+
/// (fewer than `min_results` matches), the returned chunks and KG concepts
446+
/// must be preserved and the stats must be truthful:
447+
/// `stats.chunks_returned == chunks.len()` and `stats.kg_hits == concepts.len()`.
448+
/// This guards the JSON result invariant that blocks release wrapper #3208.
449+
#[cfg(feature = "code-search")]
450+
#[tokio::test]
451+
async fn rlm_insufficient_preserves_chunks_and_reports_truthful_stats() {
452+
let tmp = tempfile::TempDir::new().expect("tempdir");
453+
// Single match => chunks.len() (1) < min_results (3) => Insufficient branch.
454+
let path = tmp.path().join("only_match.rs");
455+
std::fs::write(&path, "fn unique_target() { /* unique_target */ }\n").unwrap();
456+
457+
let mut thesaurus = Thesaurus::new("t".to_string());
458+
let concept_key = NormalizedTermValue::from("unique_target");
459+
let concept = NormalizedTerm::new(1, concept_key.clone())
460+
.with_display_value("unique_target".to_string());
461+
thesaurus.insert(concept_key, concept);
462+
463+
let hybrid = HybridSearcher::new("test-role".to_string(), thesaurus)
464+
.expect("build hybrid searcher")
465+
.with_search_path(tmp.path().to_path_buf());
466+
let grep = TerraphimGrep::new(Arc::new(hybrid), Arc::new(SufficiencyJudge::default()));
467+
468+
let result = grep
469+
.search(
470+
"unique_target",
471+
GrepOptions {
472+
haystack: Haystack::Code,
473+
max_results: 50,
474+
..GrepOptions::default()
475+
},
476+
)
477+
.await
478+
.expect("search should succeed");
479+
480+
assert!(
481+
!result.chunks.is_empty(),
482+
"expected at least one chunk from the known-match corpus"
483+
);
484+
assert!(
485+
matches!(result.sufficiency, SufficiencyState::RlmInsufficient),
486+
"single match must hit the Insufficient branch, got {:?}",
487+
result.sufficiency
488+
);
489+
assert_eq!(
490+
result.stats.chunks_returned,
491+
result.chunks.len(),
492+
"stats.chunks_returned must equal chunks.len() in the RlmInsufficient branch"
493+
);
494+
assert_eq!(
495+
result.stats.kg_hits,
496+
result.concepts.len(),
497+
"stats.kg_hits must equal concepts.len() when concepts are retained"
498+
);
499+
assert!(result.stats.kg_hits > 0, "fixture must produce a KG hit");
500+
assert!(
501+
result
502+
.concepts
503+
.iter()
504+
.any(|concept| concept.name == "unique_target"),
505+
"the known KG concept must survive the RlmInsufficient branch"
506+
);
507+
}
508+
441509
/// The RLM prompt for `include_answer` must embed the `AnswerSignature`
442510
/// JSON instructions so the model knows it must return structured output.
443511
#[test]

crates/terraphim_grep/tests/no_thesaurus_cli.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,22 @@ fn cli_runs_without_thesaurus() {
5757
Some(0),
5858
"kg_hits should be zero"
5959
);
60+
61+
// Truthful-stats invariant (blocks release wrapper #3208): the reported
62+
// counter must always match the number of chunks actually returned, even
63+
// when the sufficiency heuristic classifies the result as RlmInsufficient
64+
// (fewer than min_results matches, as in this single-file corpus).
65+
let chunks_returned = result["stats"]["chunks_returned"]
66+
.as_u64()
67+
.expect("chunks_returned is a number") as usize;
68+
assert_eq!(
69+
chunks_returned,
70+
chunks.len(),
71+
"stats.chunks_returned must equal chunks.len() (got {chunks_returned}, chunks = {})",
72+
chunks.len()
73+
);
74+
assert!(
75+
chunks_returned >= 1,
76+
"known-match corpus must report at least one returned chunk"
77+
);
6078
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Design: truthful `terraphim_grep` insufficient-result statistics
2+
3+
## Problem
4+
Published `terraphim_grep` 1.21.1 can return non-empty `chunks` with `stats.chunks_returned = 0` when the sufficiency heuristic classifies fewer than three matches as `RlmInsufficient`. This violates the JSON result invariant and blocks release wrapper #3208.
5+
6+
## Decision
7+
In `crates/terraphim_grep/src/lib.rs`, preserve returned chunks and KG concepts in the `Sufficiency::Insufficient` branch and derive counters from the actual vectors. Do not change sufficiency thresholds or reinterpret `RlmInsufficient`.
8+
9+
## Tests first
10+
Extend the real CLI known-match regression so it asserts `stats.chunks_returned == chunks.len()`. Add/extend library coverage for `RlmInsufficient` with non-empty chunks and the same invariant. Run the targeted test before implementation and retain the expected RED.
11+
12+
## Acceptance
13+
- Known-match JSON has at least one chunk.
14+
- `stats.chunks_returned == chunks.len()` in every result branch.
15+
- `stats.kg_hits == concepts.len()` when concepts are retained.
16+
- Focused crate tests, fmt and clippy pass.
17+
- Exact Release Guardian known-match smoke passes from the branch binary.
18+
19+
## Non-goals
20+
No release version bump, publish, sufficiency threshold change, unrelated crate refactor, or dependency update in this PR.

0 commit comments

Comments
 (0)