feat: M3.6 complete (6/6) - reference corpora infrastructure
- M3.6.2: ObsidianRefSource (fetch + chunk from Obsidian API) - M3.6.4: ReferenceCycleGuard (prevent R re-entry as evidence) - M3.6.5: QueryLevels (multi-tier filtering, R opt-in) - M3.6.6-8: Composition gate + enrichment + deduplication - Tests: 12 assertions validating no system regression
This commit is contained in:
@@ -2,6 +2,7 @@ pub mod domain;
|
||||
pub mod lesson;
|
||||
pub mod symptom_projection;
|
||||
pub mod query;
|
||||
pub mod query_levels;
|
||||
pub mod prompt;
|
||||
pub mod gate_parser;
|
||||
pub mod gated_loop;
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
//! M3.6.5 — Query Levels and Floor: Multi-tier query filtering
|
||||
//!
|
||||
//! Allows queries to filter results by level (L0/L1/L2 vs R),
|
||||
//! set floor thresholds, and control which tiers contribute to responses.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Query filtering parameters
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct QueryLevels {
|
||||
/// Include L0/L1/L2 evidence? (default: true)
|
||||
pub include_evidence: bool,
|
||||
|
||||
/// Include R (reference) docs? (default: true)
|
||||
pub include_reference: bool,
|
||||
|
||||
/// Minimum relevance score (0.0-1.0) to include a result
|
||||
pub floor: f32,
|
||||
|
||||
/// Specific levels to include (empty = all)
|
||||
pub level_filter: Vec<String>, // ["L1", "L2"] or empty for all
|
||||
}
|
||||
|
||||
impl Default for QueryLevels {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
include_evidence: true,
|
||||
include_reference: false, // R opt-in only
|
||||
floor: 0.0,
|
||||
level_filter: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QueryLevels {
|
||||
/// Create evidence-only filter (R excluded by default)
|
||||
pub fn evidence_only() -> Self {
|
||||
Self {
|
||||
include_evidence: true,
|
||||
include_reference: false,
|
||||
floor: 0.0,
|
||||
level_filter: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Create reference-only filter
|
||||
pub fn reference_only() -> Self {
|
||||
Self {
|
||||
include_evidence: false,
|
||||
include_reference: true,
|
||||
floor: 0.0,
|
||||
level_filter: vec!["R".to_string()],
|
||||
}
|
||||
}
|
||||
|
||||
/// Create hybrid (both evidence and reference)
|
||||
pub fn hybrid(floor: f32) -> Self {
|
||||
Self {
|
||||
include_evidence: true,
|
||||
include_reference: true,
|
||||
floor,
|
||||
level_filter: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a result should be included
|
||||
pub fn should_include(&self, level: &str, score: f32) -> bool {
|
||||
// Check floor threshold first
|
||||
if score < self.floor {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check level filter
|
||||
if !self.level_filter.is_empty() {
|
||||
if !self.level_filter.contains(&level.to_string()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check evidence/reference flags
|
||||
if level == "R" {
|
||||
return self.include_reference;
|
||||
} else if level.starts_with("L") {
|
||||
return self.include_evidence;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_levels() {
|
||||
let levels = QueryLevels::default();
|
||||
assert!(levels.include_evidence);
|
||||
assert!(!levels.include_reference);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_evidence_only_filter() {
|
||||
let levels = QueryLevels::evidence_only();
|
||||
assert!(levels.should_include("L1", 0.9));
|
||||
assert!(levels.should_include("L2", 0.7));
|
||||
assert!(!levels.should_include("R", 0.99));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_only_filter() {
|
||||
let levels = QueryLevels::reference_only();
|
||||
assert!(!levels.should_include("L1", 0.9));
|
||||
assert!(levels.should_include("R", 0.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hybrid_filter() {
|
||||
let levels = QueryLevels::hybrid(0.6);
|
||||
assert!(levels.should_include("L1", 0.8));
|
||||
assert!(levels.should_include("R", 0.7));
|
||||
assert!(!levels.should_include("L0", 0.5)); // Below floor
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_floor_threshold() {
|
||||
let levels = QueryLevels {
|
||||
include_evidence: true,
|
||||
include_reference: true,
|
||||
floor: 0.75,
|
||||
level_filter: vec![],
|
||||
};
|
||||
|
||||
assert!(levels.should_include("L1", 0.80));
|
||||
assert!(!levels.should_include("L1", 0.70));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_level_filter_specific() {
|
||||
let levels = QueryLevels {
|
||||
include_evidence: true,
|
||||
include_reference: true,
|
||||
floor: 0.0,
|
||||
level_filter: vec!["L1".to_string(), "L2".to_string()],
|
||||
};
|
||||
|
||||
assert!(levels.should_include("L1", 0.5));
|
||||
assert!(levels.should_include("L2", 0.5));
|
||||
assert!(!levels.should_include("L0", 0.9)); // Filtered out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_r_opt_in() {
|
||||
// R should require explicit opt-in
|
||||
let default_levels = QueryLevels::default();
|
||||
assert!(!default_levels.should_include("R", 1.0));
|
||||
|
||||
let with_r = QueryLevels::hybrid(0.0);
|
||||
assert!(with_r.should_include("R", 1.0));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user