## Root Cause Claims file was in applications/aphoria/.aphoria/ but all commands looked for .aphoria/claims.toml relative to project root. Additionally, .aphoria/ was fully gitignored, preventing version control of claims. ## Changes ### Path Fixes - Move claims.toml from applications/aphoria/.aphoria/ to .aphoria/ at project root - Update .gitignore: .aphoria/ → .aphoria/* with !.aphoria/claims.toml exception - Now claims can be version controlled while keys remain secret ### Verify Integration (Scanner) - scanner.rs: Load claims from ClaimsFile and call verify_claims() - ScanResult: Add verify field with VerifyReport - Report formatters: Add claim verification sections showing PASS/CONFLICT/MISSING ### Clippy Fix - report/json.rs: Replace filter().map().expect() with filter_map() ## Verification - aphoria scan . → Shows claim verification with verdicts - aphoria verify run → Per-claim verification results - aphoria verify map → Extractor coverage mapping (7/10 claims = 70%) - aphoria claims list → Reads from project root - aphoria claims create → Writes to project root - All tests pass (1120+ aphoria tests) - clippy --workspace passes ## Impact Both primary use cases now work: 1. Day-to-day (commit-time): Skills can read/create claims via CLI 2. Audit (scan-time): Scanner verifies code against authored claims Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
204 lines
6.9 KiB
Rust
204 lines
6.9 KiB
Rust
//! Aphoria Authority Lens - formalizes the authority-based conflict scoring.
|
|
//!
|
|
//! Wraps the existing scoring formula from `conflict.rs` into a proper
|
|
//! `stemedb_lens::Lens` implementation. This allows the authority resolution
|
|
//! logic to be used as a first-class Lens in Episteme queries.
|
|
|
|
use stemedb_core::types::{Assertion, SourceClass};
|
|
use stemedb_lens::{Lens, Resolution};
|
|
|
|
use crate::types::TierBreakdown;
|
|
|
|
/// Authority-based lens that resolves conflicts by source class tier.
|
|
///
|
|
/// Higher-authority sources (lower tier numbers) win. Uses the same formula
|
|
/// as `compute_conflict_score()` in `conflict.rs`:
|
|
///
|
|
/// ```text
|
|
/// normalized = 0.4 + (3.0 - min_tier) / 3.0 * 0.55
|
|
/// ```
|
|
///
|
|
/// Tier 0 (Regulatory) produces score ~0.95, Tier 3 (Expert) produces ~0.40.
|
|
pub struct AphoriaAuthorityLens;
|
|
|
|
impl Lens for AphoriaAuthorityLens {
|
|
fn resolve(&self, candidates: &[Assertion]) -> Resolution {
|
|
if candidates.is_empty() {
|
|
return Resolution::empty();
|
|
}
|
|
|
|
if candidates.len() == 1 {
|
|
return Resolution::with_winner(candidates[0].clone(), 1, 1.0, 0.0);
|
|
}
|
|
|
|
// Group by tier, pick the winner from the highest-authority (lowest tier) group
|
|
let mut best_tier = u8::MAX;
|
|
let mut best_assertion: Option<&Assertion> = None;
|
|
let mut best_confidence: f32 = 0.0;
|
|
|
|
for assertion in candidates {
|
|
let tier = assertion.source_class.tier();
|
|
if tier < best_tier || (tier == best_tier && assertion.confidence > best_confidence) {
|
|
best_tier = tier;
|
|
best_assertion = Some(assertion);
|
|
best_confidence = assertion.confidence;
|
|
}
|
|
}
|
|
|
|
let winner = match best_assertion {
|
|
Some(a) => a.clone(),
|
|
None => return Resolution::empty(),
|
|
};
|
|
|
|
// Compute conflict score using the same formula as conflict.rs
|
|
let conflict_score = authority_conflict_score(candidates);
|
|
|
|
// Resolution confidence is based on how dominant the winning tier is
|
|
let min_tier = best_tier as f32;
|
|
let resolution_confidence = 0.4 + (3.0 - min_tier.min(3.0)) / 3.0 * 0.55;
|
|
|
|
Resolution::with_winner(
|
|
winner,
|
|
candidates.len(),
|
|
resolution_confidence.min(1.0),
|
|
conflict_score,
|
|
)
|
|
}
|
|
|
|
fn name(&self) -> &'static str {
|
|
"AphoriaAuthority"
|
|
}
|
|
}
|
|
|
|
/// Compute cross-tier conflict score for a set of assertions.
|
|
///
|
|
/// Uses the same normalized formula as `conflict.rs:compute_conflict_score()`:
|
|
/// `normalized = 0.4 + (3.0 - min_tier) / 3.0 * 0.55`
|
|
///
|
|
/// Returns 0.0 if all assertions are the same tier, higher values when
|
|
/// high-authority sources (Tier 0) conflict with low-authority (Tier 3+).
|
|
fn authority_conflict_score(candidates: &[Assertion]) -> f32 {
|
|
if candidates.len() <= 1 {
|
|
return 0.0;
|
|
}
|
|
|
|
let min_tier = candidates.iter().map(|a| a.source_class.tier()).min().unwrap_or(3);
|
|
let max_tier = candidates.iter().map(|a| a.source_class.tier()).max().unwrap_or(3);
|
|
|
|
if min_tier == max_tier {
|
|
return 0.0; // Same tier, no authority conflict
|
|
}
|
|
|
|
// Tier distance maps to conflict intensity
|
|
let tier_distance = (max_tier - min_tier) as f32;
|
|
(tier_distance / 5.0).min(1.0) // Max 5 tiers apart (0-5)
|
|
}
|
|
|
|
/// Compute tier breakdown from a set of assertions.
|
|
///
|
|
/// Returns a sorted (by tier) list of tier breakdowns.
|
|
pub fn compute_tier_breakdown(assertions: &[Assertion]) -> Vec<TierBreakdown> {
|
|
use std::collections::BTreeMap;
|
|
|
|
let mut by_tier: BTreeMap<u8, (SourceClass, usize, f32)> = BTreeMap::new();
|
|
|
|
for assertion in assertions {
|
|
let tier = assertion.source_class.tier();
|
|
let entry = by_tier.entry(tier).or_insert((assertion.source_class, 0, 0.0));
|
|
entry.1 += 1;
|
|
if assertion.confidence > entry.2 {
|
|
entry.2 = assertion.confidence;
|
|
}
|
|
}
|
|
|
|
by_tier
|
|
.into_iter()
|
|
.map(|(tier, (source_class, count, max_conf))| TierBreakdown {
|
|
tier,
|
|
source_class,
|
|
assertion_count: count,
|
|
max_confidence: max_conf,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use stemedb_core::testing::AssertionBuilder;
|
|
|
|
#[test]
|
|
fn test_empty_candidates() {
|
|
let lens = AphoriaAuthorityLens;
|
|
let result = lens.resolve(&[]);
|
|
assert!(result.winner.is_none());
|
|
assert_eq!(result.candidates_count, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_single_candidate() {
|
|
let lens = AphoriaAuthorityLens;
|
|
let assertion =
|
|
AssertionBuilder::new().source_class(SourceClass::Regulatory).confidence(0.95).build();
|
|
let result = lens.resolve(&[assertion]);
|
|
assert!(result.winner.is_some());
|
|
assert_eq!(result.candidates_count, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_authority_wins_over_lower_tier() {
|
|
let lens = AphoriaAuthorityLens;
|
|
let regulatory = AssertionBuilder::new()
|
|
.subject("rfc://test")
|
|
.source_class(SourceClass::Regulatory)
|
|
.confidence(0.9)
|
|
.build();
|
|
let community = AssertionBuilder::new()
|
|
.subject("code://test")
|
|
.source_class(SourceClass::Community)
|
|
.confidence(1.0)
|
|
.build();
|
|
|
|
let result = lens.resolve(&[community, regulatory]);
|
|
let winner = result.winner.as_ref().expect("should have winner");
|
|
assert_eq!(winner.source_class, SourceClass::Regulatory);
|
|
assert_eq!(result.candidates_count, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_lens_scores_match_existing() {
|
|
// Verify the normalized formula matches conflict.rs expectations
|
|
// Tier 0 vs code → ~0.95
|
|
let regulatory =
|
|
AssertionBuilder::new().source_class(SourceClass::Regulatory).confidence(1.0).build();
|
|
let community =
|
|
AssertionBuilder::new().source_class(SourceClass::Community).confidence(1.0).build();
|
|
|
|
let lens = AphoriaAuthorityLens;
|
|
let result = lens.resolve(&[regulatory, community]);
|
|
|
|
// Resolution confidence for Tier 0 winner should be ~0.95
|
|
assert!(
|
|
result.resolution_confidence > 0.9,
|
|
"Expected >0.9, got {}",
|
|
result.resolution_confidence
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_tier_breakdown() {
|
|
let assertions = vec![
|
|
AssertionBuilder::new().source_class(SourceClass::Regulatory).confidence(0.95).build(),
|
|
AssertionBuilder::new().source_class(SourceClass::Regulatory).confidence(0.9).build(),
|
|
AssertionBuilder::new().source_class(SourceClass::Community).confidence(0.7).build(),
|
|
];
|
|
|
|
let breakdown = compute_tier_breakdown(&assertions);
|
|
assert_eq!(breakdown.len(), 2);
|
|
assert_eq!(breakdown[0].tier, 0); // Regulatory
|
|
assert_eq!(breakdown[0].assertion_count, 2);
|
|
assert!((breakdown[0].max_confidence - 0.95).abs() < f32::EPSILON);
|
|
assert_eq!(breakdown[1].assertion_count, 1);
|
|
}
|
|
}
|