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>
219 lines
8.2 KiB
Rust
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);
|
|
}
|