stemedb/applications/aphoria/tests/gap_fixes_integration.rs
jml 65065f3d8f feat(aphoria): implement community corpus with wiki import and pattern aggregation
Implements Phase 4 (A4) - Community corpus as first-class citizens:

- **Community Corpus Builder** - Queries StemeDB pattern aggregates
- **Wiki Import** - Bootstrap corpus from markdown docs (aphoria corpus import wiki)
- **Pattern Aggregation** - Automatic learning from local scans (--sync flag)
- **Storage Layer** - StemeDBPatternStore with content-addressed deduplication
- **Promotion Logic** - Multi-tier thresholds (95%/80%/50% adoption rates)
- **Corpus Build** - Unified registry for RFC/OWASP/Vendor/Community sources
- **Trust Packs** - Export corpus as signed, distributable artifacts
- **Documentation** - bootstrap-corpus.md guide + CLI reference updates

Technical details:
- Pattern aggregates stored as assertions with predicate "pattern_aggregate"
- Content-addressed subjects via BLAKE3(subject:predicate:value)
- PatternAggregator handles write path (observations → patterns)
- StemeDBPatternStore handles read path (pattern queries)
- Integration tests + fixtures in tests/wiki_import_test.rs

Deleted hardcoded.rs (368 lines) - corpus now fully emergent from StemeDB.
Deleted enriched-corpus-patterns.md (677 lines) - feature shipped.

Closes VG-026 (community corpus), part of A4 milestone.

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

219 lines
8.2 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::claims_file::ClaimsFile;
use aphoria::{AuthoredClaim, AuthoredValue, ClaimStatus, ComparisonMode};
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);
}