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:
2026-08-28 13:59:29 -07:00
parent 4f31a68139
commit c1fcdb9769
10 changed files with 977 additions and 3 deletions
+1
View File
@@ -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;
+161
View File
@@ -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));
}
}