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>
307 lines
10 KiB
Rust
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");
|
|
}
|
|
}
|