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>
228 lines
8.3 KiB
Rust
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);
|
|
}
|