stemedb/applications/aphoria/src/resolution/tier_verdict.rs
jml fae9b47fae feat(aphoria): implement hosted mode with remote StemeDB integration
Add remote mode infrastructure for querying claims from StemeDB API:
- Remote client with caching layer for claim queries
- Authority resolution logic with tier-based verdict system
- StemeDB API handlers for claims CRUD operations
- Enhanced conflict detection with remote claim support
- Validation reports documenting A5.3 phase completion

Changes:
- applications/aphoria/src/remote/: New client + cache modules
- applications/aphoria/src/resolution/: Authority tier resolution
- crates/stemedb-api/src/handlers/stemedb_claims.rs: API handlers
- applications/aphoria/validation/a5.3/: Phase validation reports
- Updated roadmap with hosted mode milestones

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-14 09:29:56 +00:00

307 lines
10 KiB
Rust

//! Tier-aware verdict types for authority-scoped conflict resolution.
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::types::{TierBreakdown, Verdict};
/// Tier-aware verdict that shows what each tier says about a conflict.
///
/// This enables tier-specific conflict resolution where higher-tier authority
/// (lower tier number) wins. For example, Tier 1 (RFC) overrides Tier 3 (Expert).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TierAwareVerdict {
/// Single tier conflict (all conflicts from same tier).
///
/// This is the common case when all conflicting sources share the same authority tier.
SingleTier {
/// Tier number (0-5, with 2.5 for TeamPolicy).
tier: u8,
/// Human-readable tier name (e.g., "Tier 1 (Clinical/RFC)").
tier_name: String,
/// The verdict at this tier.
verdict: Verdict,
/// Number of sources at this tier.
sources: usize,
/// Maximum confidence among sources at this tier.
max_confidence: f32,
},
/// Multi-tier conflict (conflicts from multiple tiers).
///
/// When conflicts span multiple authority tiers, the primary tier (highest authority,
/// lowest tier number) determines the effective verdict.
MultiTier {
/// Primary tier (lowest tier number = highest authority).
primary_tier: u8,
/// The verdict from the primary tier (this is the effective verdict).
primary_verdict: Verdict,
/// Per-tier verdicts: (tier, verdict, source_count, max_confidence).
tier_verdicts: Vec<(u8, Verdict, usize, f32)>,
/// Overall conflict score.
conflict_score: f32,
},
/// Higher tier agrees with code (no conflict at high tier, conflict at low tier).
///
/// This occurs when a higher-authority tier agrees with the code observation,
/// but a lower-authority tier conflicts. The recommendation is to trust the
/// higher tier and ignore the lower tier conflict.
HigherTierAgreement {
/// The tier that agrees with the code.
agreeing_tier: u8,
/// The tier that conflicts with the code.
conflicting_tier: u8,
/// Human-readable recommendation.
recommendation: String,
},
}
impl TierAwareVerdict {
/// Get the effective verdict (what should be shown to user).
///
/// For `SingleTier` and `MultiTier`, this is the verdict.
/// For `HigherTierAgreement`, this is `Verdict::Pass` (no action needed).
pub fn effective_verdict(&self) -> Verdict {
match self {
TierAwareVerdict::SingleTier { verdict, .. } => *verdict,
TierAwareVerdict::MultiTier { primary_verdict, .. } => *primary_verdict,
TierAwareVerdict::HigherTierAgreement { .. } => Verdict::Pass,
}
}
/// Get the primary tier (highest authority tier involved).
///
/// Returns the tier number (0-5) of the most authoritative source involved.
pub fn primary_tier(&self) -> u8 {
match self {
TierAwareVerdict::SingleTier { tier, .. } => *tier,
TierAwareVerdict::MultiTier { primary_tier, .. } => *primary_tier,
TierAwareVerdict::HigherTierAgreement { agreeing_tier, .. } => *agreeing_tier,
}
}
/// Format for display (used in CLI output).
///
/// Returns a human-readable string describing the tier-aware verdict.
pub fn display(&self) -> String {
match self {
TierAwareVerdict::SingleTier { tier_name, verdict, sources, max_confidence, .. } => {
format!(
"{} {} - {} source{}, max confidence {:.2}",
verdict.symbol(),
tier_name,
sources,
if *sources == 1 { "" } else { "s" },
max_confidence
)
}
TierAwareVerdict::MultiTier {
primary_tier,
primary_verdict,
tier_verdicts,
conflict_score,
} => {
let tier_name = format_tier_name(*primary_tier);
let mut display = format!(
"{} {} (primary tier, score {:.2})",
primary_verdict.symbol(),
tier_name,
conflict_score
);
// Add tier breakdown summary
if tier_verdicts.len() > 1 {
display.push_str(&format!(" - {} tiers involved", tier_verdicts.len()));
}
display
}
TierAwareVerdict::HigherTierAgreement { recommendation, .. } => {
format!("✓ PASS - {}", recommendation)
}
}
}
/// Create a SingleTier verdict from tier breakdown.
pub fn from_single_tier(breakdown: &TierBreakdown, verdict: Verdict) -> Self {
Self::SingleTier {
tier: breakdown.tier,
tier_name: format_tier_name(breakdown.tier),
verdict,
sources: breakdown.assertion_count,
max_confidence: breakdown.max_confidence,
}
}
/// Create a MultiTier verdict from tier breakdown map.
pub fn from_multi_tier(
tier_breakdown: &BTreeMap<u8, TierBreakdown>,
primary_tier: u8,
primary_verdict: Verdict,
conflict_score: f32,
) -> Self {
let tier_verdicts: Vec<_> = tier_breakdown
.iter()
.map(|(tier, bd)| {
// Compute what this tier alone would say
// For now, we'll use the primary verdict for all tiers
// In the future, this could be tier-specific based on thresholds
let tier_verdict = if *tier == primary_tier {
primary_verdict
} else {
// Lower-authority tiers might have different verdicts
// but for now, we'll keep it simple
primary_verdict
};
(*tier, tier_verdict, bd.assertion_count, bd.max_confidence)
})
.collect();
Self::MultiTier {
primary_tier,
primary_verdict,
tier_verdicts,
conflict_score,
}
}
}
impl Verdict {
/// Get the symbol for this verdict.
pub fn symbol(&self) -> &'static str {
match self {
Verdict::Block => "❌ BLOCK",
Verdict::Flag => "⚠️ FLAG",
Verdict::Pass => "✓ PASS",
Verdict::Ack => "✓ ACK",
Verdict::Drift => "🔄 DRIFT",
}
}
}
/// Format a tier number as a human-readable name.
///
/// Examples:
/// - Tier 0 → "Tier 0 (Regulatory)"
/// - Tier 1 → "Tier 1 (Clinical/RFC)"
/// - Tier 2 → "Tier 2 (Observational)"
/// - Tier 2.5 → "Tier 2.5 (TeamPolicy)"
/// - Tier 3 → "Tier 3 (Expert)"
/// - Tier 4 → "Tier 4 (Community)"
/// - Tier 5 → "Tier 5 (Anecdotal)"
pub fn format_tier_name(tier: u8) -> String {
match tier {
0 => "Tier 0 (Regulatory)".to_string(),
1 => "Tier 1 (Clinical/RFC)".to_string(),
2 => "Tier 2 (Observational/TeamPolicy)".to_string(),
3 => "Tier 3 (Expert)".to_string(),
4 => "Tier 4 (Community)".to_string(),
5 => "Tier 5 (Anecdotal)".to_string(),
_ => format!("Tier {tier}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use stemedb_core::types::SourceClass;
#[test]
fn test_single_tier_verdict() {
let breakdown = TierBreakdown {
tier: 1,
source_class: SourceClass::Clinical,
assertion_count: 3,
max_confidence: 0.95,
};
let verdict = TierAwareVerdict::from_single_tier(&breakdown, Verdict::Block);
assert_eq!(verdict.effective_verdict(), Verdict::Block);
assert_eq!(verdict.primary_tier(), 1);
let display = verdict.display();
assert!(display.contains("BLOCK"));
assert!(display.contains("Tier 1"));
assert!(display.contains("3 sources"));
}
#[test]
fn test_multi_tier_verdict() {
let mut tier_breakdown = BTreeMap::new();
tier_breakdown.insert(
1,
TierBreakdown {
tier: 1,
source_class: SourceClass::Clinical,
assertion_count: 2,
max_confidence: 0.95,
},
);
tier_breakdown.insert(
3,
TierBreakdown {
tier: 3,
source_class: SourceClass::Expert,
assertion_count: 1,
max_confidence: 0.70,
},
);
let verdict =
TierAwareVerdict::from_multi_tier(&tier_breakdown, 1, Verdict::Block, 0.92);
assert_eq!(verdict.effective_verdict(), Verdict::Block);
assert_eq!(verdict.primary_tier(), 1);
let display = verdict.display();
assert!(display.contains("BLOCK"));
assert!(display.contains("Tier 1"));
assert!(display.contains("2 tiers"));
}
#[test]
fn test_higher_tier_agreement() {
let verdict = TierAwareVerdict::HigherTierAgreement {
agreeing_tier: 1,
conflicting_tier: 4,
recommendation: "Tier 1 RFC agrees with your code, ignore Tier 4".to_string(),
};
assert_eq!(verdict.effective_verdict(), Verdict::Pass);
assert_eq!(verdict.primary_tier(), 1);
let display = verdict.display();
assert!(display.contains("PASS"));
assert!(display.contains("Tier 1"));
}
#[test]
fn test_primary_tier_is_lowest_number() {
let tiers = vec![1u8, 3, 5];
let primary = *tiers.iter().min().unwrap();
assert_eq!(primary, 1);
}
#[test]
fn test_format_tier_name() {
assert_eq!(format_tier_name(0), "Tier 0 (Regulatory)");
assert_eq!(format_tier_name(1), "Tier 1 (Clinical/RFC)");
assert_eq!(format_tier_name(2), "Tier 2 (Observational/TeamPolicy)");
assert_eq!(format_tier_name(3), "Tier 3 (Expert)");
assert_eq!(format_tier_name(4), "Tier 4 (Community)");
assert_eq!(format_tier_name(5), "Tier 5 (Anecdotal)");
}
#[test]
fn test_verdict_symbols() {
assert_eq!(Verdict::Block.symbol(), "❌ BLOCK");
assert_eq!(Verdict::Flag.symbol(), "⚠️ FLAG");
assert_eq!(Verdict::Pass.symbol(), "✓ PASS");
assert_eq!(Verdict::Ack.symbol(), "✓ ACK");
assert_eq!(Verdict::Drift.symbol(), "🔄 DRIFT");
}
}