stemedb/applications/aphoria/tests/gap_fixes_integration.rs
jml e95c978481 feat(aphoria): add inline claim markers and claim enrichment infrastructure
This commit implements Phase 17 of the Aphoria roadmap, adding:

**Inline Claim Markers (@aphoria:claim):**
- New extractor for detecting inline markers in comments
- Pending markers tracked in .aphoria/pending_markers.toml
- CLI commands: list-markers, formalize-marker, reject-marker
- Support for all major comment styles (Rust, Python, SQL, etc.)
- Auto-sync during scan (configurable)

**Claim Enrichment:**
- ClaimEnrichment type with source attribution (inline, extractor, manual)
- EnrichedClaimInfo with full enrichment metadata
- Extended AuthoredClaim with optional enrichment field
- API endpoints for enriched claim queries
- Dashboard UI components (enrichment badge, verdict badge)

**Enhanced Extractor Trait:**
- verifiable_predicates() method for declaring (tail_path, predicate) pairs
- 10 security extractors now implement verifiable_predicates
- Enables claim suggester skill to find unclaimed patterns

**Documentation:**
- Phase 17 summary with complete implementation details
- Gap fixes summary documenting 8 closed vision gaps
- Updated CLI reference with new commands
- New aphoria-docs skill for documentation maintenance
- Updated roadmap with Phase 17 completion

**Integration:**
- ClaimsFile support for claim enrichment persistence
- Pattern aggregate store support for enrichment queries
- Dashboard filters and display for enrichment metadata
- API handlers for list-markers and enrichment queries

**Tests:**
- New gap_fixes_integration test suite
- Corpus enricher module with best practices ingestion

Closes: VG-005, VG-017, VG-018, VG-019, VG-020, VG-021, VG-022, VG-023

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-08 20:18:20 +00:00

228 lines
8.3 KiB
Rust

//! Integration tests for Gap 1 and Gap 5 fixes.
//!
//! Gap 1: Observations should use confidence-based tiers (4 or 5), not Tier 3
//! Gap 5: Superseding claims should auto-deprecate old claims, warn on duplicates
use aphoria::{AuthoredClaim, AuthoredValue, ClaimStatus, ComparisonMode};
use aphoria::claims_file::ClaimsFile;
use stemedb_core::types::SourceClass;
use tempfile::TempDir;
/// Test Gap 1: Observations use confidence-based tiers (not Tier 3 Expert)
#[test]
fn test_gap1_observation_tiers() {
// High confidence observation should be Tier 4 (Community)
let high_confidence_tier = aphoria::bridge::observation_to_tier(0.95);
assert_eq!(high_confidence_tier, SourceClass::Community);
assert_eq!(high_confidence_tier.tier(), 4);
assert!((high_confidence_tier.authority_weight() - 0.3).abs() < f32::EPSILON);
// Low confidence observation should be Tier 5 (Anecdotal)
let low_confidence_tier = aphoria::bridge::observation_to_tier(0.7);
assert_eq!(low_confidence_tier, SourceClass::Anecdotal);
assert_eq!(low_confidence_tier.tier(), 5);
assert!((low_confidence_tier.authority_weight() - 0.1).abs() < f32::EPSILON);
// Boundary case: exactly 0.9 should be Tier 4
let boundary_tier = aphoria::bridge::observation_to_tier(0.9);
assert_eq!(boundary_tier, SourceClass::Community);
assert_eq!(boundary_tier.tier(), 4);
}
/// Test Gap 5: Supersede auto-deprecates old claims
#[test]
fn test_gap5_supersede_auto_deprecates() {
let temp_dir = TempDir::new().expect("create temp dir");
let claims_path = temp_dir.path().join("claims.toml");
let mut claims_file = ClaimsFile::new();
// Create initial claim
let claim_v1 = AuthoredClaim {
id: "test-001".to_string(),
concept_path: "test/feature/enabled".to_string(),
predicate: "value".to_string(),
value: AuthoredValue::Bool(true),
comparison: ComparisonMode::Equals,
provenance: "Initial implementation".to_string(),
invariant: "Feature should be enabled".to_string(),
consequence: "Feature disabled".to_string(),
authority_tier: "expert".to_string(),
evidence: vec![],
category: "feature".to_string(),
status: ClaimStatus::Active,
supersedes: None,
created_by: "dev".to_string(),
created_at: "2026-02-08T10:00:00Z".to_string(),
updated_at: None,
};
claims_file.add(claim_v1);
assert_eq!(claims_file.len(), 1);
assert_eq!(
claims_file.find_by_id("test-001").map(|c| &c.status),
Some(&ClaimStatus::Active)
);
// Supersede with v2
let claim_v2 = AuthoredClaim {
id: "test-002".to_string(),
concept_path: "test/feature/enabled".to_string(),
predicate: "value".to_string(),
value: AuthoredValue::Bool(false),
comparison: ComparisonMode::Equals,
provenance: "Updated after review".to_string(),
invariant: "Feature should be disabled".to_string(),
consequence: "Feature enabled".to_string(),
authority_tier: "expert".to_string(),
evidence: vec!["Review notes".to_string()],
category: "feature".to_string(),
status: ClaimStatus::Active,
supersedes: Some("test-001".to_string()),
created_by: "lead".to_string(),
created_at: "2026-02-08T11:00:00Z".to_string(),
updated_at: None,
};
claims_file.supersede("test-001", claim_v2).expect("supersede");
// Verify old claim is superseded
assert_eq!(
claims_file.find_by_id("test-001").map(|c| &c.status),
Some(&ClaimStatus::Superseded)
);
// Verify new claim is active
assert_eq!(
claims_file.find_by_id("test-002").map(|c| &c.status),
Some(&ClaimStatus::Active)
);
// Verify lineage link
assert_eq!(
claims_file.find_by_id("test-002").and_then(|c| c.supersedes.as_deref()),
Some("test-001")
);
// Verify persistence
claims_file.save(&claims_path).expect("save");
let loaded = ClaimsFile::load(&claims_path).expect("load");
assert_eq!(loaded.len(), 2);
assert_eq!(
loaded.find_by_id("test-001").map(|c| &c.status),
Some(&ClaimStatus::Superseded)
);
}
/// Test Gap 5: Duplicate validation warns when creating duplicate active claims
#[test]
fn test_gap5_duplicate_validation_warning() {
let mut claims_file = ClaimsFile::new();
// Create first claim
let claim1 = AuthoredClaim {
id: "dup-001".to_string(),
concept_path: "test/config/timeout".to_string(),
predicate: "value".to_string(),
value: AuthoredValue::Number(30.0),
comparison: ComparisonMode::Equals,
provenance: "Initial config".to_string(),
invariant: "Timeout must be 30s".to_string(),
consequence: "Requests timeout too fast".to_string(),
authority_tier: "team_policy".to_string(),
evidence: vec![],
category: "config".to_string(),
status: ClaimStatus::Active,
supersedes: None,
created_by: "dev1".to_string(),
created_at: "2026-02-08T10:00:00Z".to_string(),
updated_at: None,
};
claims_file.add(claim1);
// Create duplicate (same concept_path + predicate, different ID)
let claim2 = AuthoredClaim {
id: "dup-002".to_string(),
concept_path: "test/config/timeout".to_string(), // Same
predicate: "value".to_string(), // Same
value: AuthoredValue::Number(60.0), // Different value
comparison: ComparisonMode::Equals,
provenance: "Updated config".to_string(),
invariant: "Timeout must be 60s".to_string(),
consequence: "Requests timeout too slow".to_string(),
authority_tier: "team_policy".to_string(),
evidence: vec![],
category: "config".to_string(),
status: ClaimStatus::Active,
supersedes: None,
created_by: "dev2".to_string(),
created_at: "2026-02-08T11:00:00Z".to_string(),
updated_at: None,
};
// This should print a warning (captured in test output)
// but still add the claim
claims_file.add(claim2);
assert_eq!(claims_file.len(), 2);
assert_eq!(claims_file.find_by_status(&ClaimStatus::Active).len(), 2);
}
/// Test Gap 5: No warning when duplicate is deprecated
#[test]
fn test_gap5_no_warning_for_deprecated_duplicate() {
let mut claims_file = ClaimsFile::new();
// Create and deprecate first claim
let claim1 = AuthoredClaim {
id: "old-001".to_string(),
concept_path: "test/feature/mode".to_string(),
predicate: "value".to_string(),
value: AuthoredValue::Text("legacy".to_string()),
comparison: ComparisonMode::Equals,
provenance: "Old implementation".to_string(),
invariant: "Mode should be legacy".to_string(),
consequence: "Mode incorrect".to_string(),
authority_tier: "expert".to_string(),
evidence: vec![],
category: "feature".to_string(),
status: ClaimStatus::Active,
supersedes: None,
created_by: "dev".to_string(),
created_at: "2026-02-08T10:00:00Z".to_string(),
updated_at: None,
};
claims_file.add(claim1);
claims_file.deprecate("old-001", "2026-02-08T11:00:00Z").expect("deprecate");
// Now add new claim with same concept_path/predicate
// Should NOT warn because the first is deprecated
let claim2 = AuthoredClaim {
id: "new-001".to_string(),
concept_path: "test/feature/mode".to_string(), // Same
predicate: "value".to_string(), // Same
value: AuthoredValue::Text("modern".to_string()),
comparison: ComparisonMode::Equals,
provenance: "New implementation".to_string(),
invariant: "Mode should be modern".to_string(),
consequence: "Mode incorrect".to_string(),
authority_tier: "expert".to_string(),
evidence: vec![],
category: "feature".to_string(),
status: ClaimStatus::Active,
supersedes: None,
created_by: "dev".to_string(),
created_at: "2026-02-08T12:00:00Z".to_string(),
updated_at: None,
};
// Should NOT print warning
claims_file.add(claim2);
assert_eq!(claims_file.len(), 2);
assert_eq!(claims_file.find_by_status(&ClaimStatus::Active).len(), 1);
assert_eq!(claims_file.find_by_status(&ClaimStatus::Deprecated).len(), 1);
}