diff --git a/applications/aphoria/src/baseline.rs b/applications/aphoria/src/baseline.rs index 498e550..7badcf4 100644 --- a/applications/aphoria/src/baseline.rs +++ b/applications/aphoria/src/baseline.rs @@ -49,6 +49,7 @@ pub async fn show_diff(config: &AphoriaConfig) -> Result { sync: false, // Diff does not write observations file_source: crate::types::FileSource::All, benchmark: false, + show_claims: false, }; let result = run_scan(args, config).await?; diff --git a/applications/aphoria/src/cli/mod.rs b/applications/aphoria/src/cli/mod.rs index d7c0f73..5a3174c 100644 --- a/applications/aphoria/src/cli/mod.rs +++ b/applications/aphoria/src/cli/mod.rs @@ -88,6 +88,10 @@ pub enum Commands { /// Run performance benchmark with timing breakdown. #[arg(long)] benchmark: bool, + + /// Show all extracted claims in the output + #[arg(long)] + show_claims: bool, }, /// Manage acknowledgments (mark conflicts as intentional) diff --git a/applications/aphoria/src/handlers/mod.rs b/applications/aphoria/src/handlers/mod.rs index 8d35be8..849ec97 100644 --- a/applications/aphoria/src/handlers/mod.rs +++ b/applications/aphoria/src/handlers/mod.rs @@ -64,12 +64,22 @@ pub async fn handle_command(command: Commands, config: &AphoriaConfig) -> ExitCo staged, community_preview, benchmark, + show_claims, } => { if community_preview { scan::handle_community_preview(path, config).await } else { scan::handle_scan( - path, format, exit_code, strict, persist, debug, sync, staged, benchmark, + path, + format, + exit_code, + strict, + persist, + debug, + sync, + staged, + benchmark, + show_claims, config, ) .await diff --git a/applications/aphoria/src/handlers/scan.rs b/applications/aphoria/src/handlers/scan.rs index 3f8dcd3..92f719d 100644 --- a/applications/aphoria/src/handlers/scan.rs +++ b/applications/aphoria/src/handlers/scan.rs @@ -15,6 +15,7 @@ pub async fn handle_scan( sync: bool, staged: bool, benchmark: bool, + show_claims: bool, config: &AphoriaConfig, ) -> ExitCode { // Validate: --sync requires --persist @@ -36,6 +37,7 @@ pub async fn handle_scan( sync, file_source, benchmark, + show_claims, }; // Apply stricter thresholds if requested @@ -99,6 +101,7 @@ pub async fn handle_community_preview( sync: false, file_source: FileSource::All, benchmark: false, + show_claims: false, }; let claims = match extract_claims(&args, config).await { diff --git a/applications/aphoria/src/learning/types.rs b/applications/aphoria/src/learning/types.rs index 281fd94..d1e8b89 100644 --- a/applications/aphoria/src/learning/types.rs +++ b/applications/aphoria/src/learning/types.rs @@ -18,10 +18,11 @@ use crate::types::Language; /// /// Used to classify the type of value extracted from code patterns, /// enabling proper placeholder generation during normalization. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "lowercase")] pub enum ValueType { /// Text/string value (e.g., "TLSv1.2", "admin") + #[default] Text, /// Numeric value (e.g., 4096, 30) Number, @@ -29,12 +30,6 @@ pub enum ValueType { Boolean, } -impl Default for ValueType { - fn default() -> Self { - Self::Text - } -} - impl std::fmt::Display for ValueType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/applications/aphoria/src/lifecycle/types.rs b/applications/aphoria/src/lifecycle/types.rs index 6ac7e64..1bbf83e 100644 --- a/applications/aphoria/src/lifecycle/types.rs +++ b/applications/aphoria/src/lifecycle/types.rs @@ -11,12 +11,13 @@ use uuid::Uuid; /// /// Patterns move through this state machine as they age, become obsolete, /// or are replaced by better alternatives. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(tag = "status", rename_all = "snake_case")] pub enum KnowledgeStatus { /// Pattern is active and should trigger matches. /// /// This is the default state for all patterns. + #[default] Active, /// Pattern is deprecated but still active. @@ -65,12 +66,6 @@ pub enum KnowledgeStatus { }, } -impl Default for KnowledgeStatus { - fn default() -> Self { - Self::Active - } -} - impl KnowledgeStatus { /// Check if this status is active (pattern should match during scans). pub fn is_active(&self) -> bool { diff --git a/applications/aphoria/src/report/json.rs b/applications/aphoria/src/report/json.rs index 10deb6c..f744aad 100644 --- a/applications/aphoria/src/report/json.rs +++ b/applications/aphoria/src/report/json.rs @@ -153,6 +153,26 @@ impl ReportFormatter for JsonReport { "deprecated_usages": deprecated_json, }); + // Add claims if present + if let Some(claims) = &result.claims { + let claims_json: Vec = claims + .iter() + .map(|claim| { + serde_json::json!({ + "concept_path": claim.concept_path, + "predicate": claim.predicate, + "value": object_value_to_json(&claim.value), + "file": claim.file, + "line": claim.line, + "matched_text": claim.matched_text, + "confidence": claim.confidence, + "description": claim.description, + }) + }) + .collect(); + report["claims"] = serde_json::json!(claims_json); + } + // Add timing if benchmark mode was enabled if let Some(timing) = &result.timing { let mut timing_json = serde_json::json!({ @@ -215,6 +235,7 @@ mod tests { debug: false, observations_recorded: 0, timing: None, + claims: None, deprecated_usages: vec![], }; diff --git a/applications/aphoria/src/report/markdown.rs b/applications/aphoria/src/report/markdown.rs index ff81452..54b006b 100644 --- a/applications/aphoria/src/report/markdown.rs +++ b/applications/aphoria/src/report/markdown.rs @@ -32,6 +32,31 @@ impl ReportFormatter for MarkdownReport { if result.conflicts.is_empty() && result.drifts.is_empty() { out.push_str("No conflicts or drifts found.\n"); + + // Still show claims if requested, even with no conflicts + if let Some(claims) = &result.claims { + out.push_str("\n## Extracted Claims\n\n"); + + if claims.is_empty() { + out.push_str("No claims extracted.\n\n"); + } else { + out.push_str("| Concept | Value | File | Line | Confidence |\n"); + out.push_str("|---------|-------|------|------|------------|\n"); + + for claim in claims { + out.push_str(&format!( + "| `{}` | `{}` | `{}` | {} | {:.0}% |\n", + extract_leaf_concept(&claim.concept_path), + object_value_display(&claim.value), + claim.file, + claim.line, + claim.confidence * 100.0, + )); + } + out.push('\n'); + } + } + return out; } @@ -233,6 +258,30 @@ impl ReportFormatter for MarkdownReport { } } + // Extracted Claims section + if let Some(claims) = &result.claims { + out.push_str("## Extracted Claims\n\n"); + + if claims.is_empty() { + out.push_str("No claims extracted.\n\n"); + } else { + out.push_str("| Concept | Value | File | Line | Confidence |\n"); + out.push_str("|---------|-------|------|------|------------|\n"); + + for claim in claims { + out.push_str(&format!( + "| `{}` | `{}` | `{}` | {} | {:.0}% |\n", + extract_leaf_concept(&claim.concept_path), + object_value_display(&claim.value), + claim.file, + claim.line, + claim.confidence * 100.0, + )); + } + out.push('\n'); + } + } + out } } @@ -280,6 +329,7 @@ mod tests { debug: false, observations_recorded: 0, timing: None, + claims: None, deprecated_usages: vec![], }; diff --git a/applications/aphoria/src/report/sarif.rs b/applications/aphoria/src/report/sarif.rs index 00b8a57..8aa4c53 100644 --- a/applications/aphoria/src/report/sarif.rs +++ b/applications/aphoria/src/report/sarif.rs @@ -297,10 +297,69 @@ impl ReportFormatter for SarifReport { }) .collect(); + // Add claims if present (as informational-level results) + let claims_results: Vec = if let Some(claims) = &result.claims { + // Add a single rule for all claims + if !claims.is_empty() && !rule_indices.contains_key("aphoria/claim") { + let idx = rules.len(); + rule_indices.insert("aphoria/claim".to_string(), idx); + rules.push(serde_json::json!({ + "id": "aphoria/claim", + "shortDescription": { + "text": "Extracted claim (no conflict detected)", + }, + "defaultConfiguration": { + "level": "note", + }, + "helpUri": "https://github.com/orchard9/aphoria/docs/claims", + })); + } + + claims + .iter() + .map(|claim| { + let rule_index = rule_indices.get("aphoria/claim").copied().unwrap_or(0); + let message = format!( + "{}\n{} = {}", + claim.description, + claim.predicate, + object_value_display(&claim.value) + ); + + serde_json::json!({ + "ruleId": "aphoria/claim", + "ruleIndex": rule_index, + "level": "note", + "message": { + "text": message, + }, + "locations": [{ + "physicalLocation": { + "artifactLocation": { + "uri": claim.file, + "uriBaseId": "%SRCROOT%", + }, + "region": { + "startLine": claim.line, + } + } + }], + "properties": { + "concept_path": claim.concept_path, + "confidence": claim.confidence, + } + }) + }) + .collect() + } else { + Vec::new() + }; + // Combine all results let mut all_results = results; all_results.extend(drift_results); all_results.extend(deprecated_results); + all_results.extend(claims_results); let sarif = serde_json::json!({ "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", @@ -393,6 +452,7 @@ mod tests { debug: false, observations_recorded: 0, timing: None, + claims: None, deprecated_usages: vec![], }; diff --git a/applications/aphoria/src/report/table.rs b/applications/aphoria/src/report/table.rs index 37eeb4d..eeb742f 100644 --- a/applications/aphoria/src/report/table.rs +++ b/applications/aphoria/src/report/table.rs @@ -32,6 +32,40 @@ impl ReportFormatter for TableReport { if result.conflicts.is_empty() && result.drifts.is_empty() { output.push_str("No conflicts or drifts found.\n"); + + // Still show claims if requested, even with no conflicts + if let Some(claims) = &result.claims { + if claims.is_empty() { + output.push_str("\nExtracted Claims:\n\n"); + output.push_str(" No claims extracted.\n"); + } else { + output.push_str("\nExtracted Claims:\n\n"); + let mut claims_table = Table::new(); + claims_table.set_content_arrangement(ContentArrangement::Dynamic); + claims_table.set_header(vec![ + Cell::new("Concept"), + Cell::new("Value"), + Cell::new("File"), + Cell::new("Line").set_alignment(CellAlignment::Right), + Cell::new("Confidence").set_alignment(CellAlignment::Right), + ]); + + for claim in claims { + claims_table.add_row(vec![ + Cell::new(extract_leaf_concept(&claim.concept_path)), + Cell::new(object_value_display(&claim.value)), + Cell::new(&claim.file), + Cell::new(claim.line.to_string()).set_alignment(CellAlignment::Right), + Cell::new(format!("{:.0}%", claim.confidence * 100.0)) + .set_alignment(CellAlignment::Right), + ]); + } + + output.push_str(&claims_table.to_string()); + output.push('\n'); + } + } + return output; } @@ -220,6 +254,39 @@ impl ReportFormatter for TableReport { } } + // Extracted Claims section + if let Some(claims) = &result.claims { + if claims.is_empty() { + output.push_str("\nExtracted Claims:\n\n"); + output.push_str(" No claims extracted.\n\n"); + } else { + output.push_str("\nExtracted Claims:\n\n"); + let mut claims_table = Table::new(); + claims_table.set_content_arrangement(ContentArrangement::Dynamic); + claims_table.set_header(vec![ + Cell::new("Concept"), + Cell::new("Value"), + Cell::new("File"), + Cell::new("Line").set_alignment(CellAlignment::Right), + Cell::new("Confidence").set_alignment(CellAlignment::Right), + ]); + + for claim in claims { + claims_table.add_row(vec![ + Cell::new(extract_leaf_concept(&claim.concept_path)), + Cell::new(object_value_display(&claim.value)), + Cell::new(&claim.file), + Cell::new(claim.line.to_string()).set_alignment(CellAlignment::Right), + Cell::new(format!("{:.0}%", claim.confidence * 100.0)) + .set_alignment(CellAlignment::Right), + ]); + } + + output.push_str(&claims_table.to_string()); + output.push('\n'); + } + } + // Footer summary let block_count = result.count_by_verdict(Verdict::Block); let flag_count = result.count_by_verdict(Verdict::Flag); @@ -335,6 +402,7 @@ mod tests { debug: false, observations_recorded: 0, timing: None, + claims: None, deprecated_usages: vec![], } } diff --git a/applications/aphoria/src/scan/scanner.rs b/applications/aphoria/src/scan/scanner.rs index 3c957ff..dbc9594 100644 --- a/applications/aphoria/src/scan/scanner.rs +++ b/applications/aphoria/src/scan/scanner.rs @@ -87,7 +87,16 @@ pub async fn run_scan(args: ScanArgs, config: &AphoriaConfig) -> Result Result, + /// Extracted claims (only populated when --show-claims is enabled). + /// + /// When present, contains all claims extracted during the scan, sorted by + /// file path and line number for easy verification and debugging. + pub claims: Option>, + /// Deprecated pattern usages detected. /// /// Populated when deprecated patterns are matched during scan. @@ -87,6 +93,7 @@ impl ScanResult { debug: false, observations_recorded: 0, timing: None, + claims: None, deprecated_usages: vec![], } } @@ -428,6 +435,7 @@ mod tests { debug: false, observations_recorded: 0, timing: None, + claims: None, deprecated_usages: vec![], }; diff --git a/crates/stemedb-storage/src/circuit_breaker_store/model.rs b/crates/stemedb-storage/src/circuit_breaker_store/model.rs index 9caa110..449a92d 100644 --- a/crates/stemedb-storage/src/circuit_breaker_store/model.rs +++ b/crates/stemedb-storage/src/circuit_breaker_store/model.rs @@ -10,10 +10,11 @@ use rkyv::{Archive, Deserialize, Serialize}; /// - **Closed**: Normal operation, requests are allowed. /// - **Open**: Circuit has tripped, requests are blocked. /// - **HalfOpen**: Testing after timeout, one request allowed. -#[derive(Archive, Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Archive, Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Default)] #[archive(check_bytes)] pub enum CircuitState { /// Normal operation - requests allowed. + #[default] Closed, /// Circuit tripped - requests blocked until timeout. @@ -34,12 +35,6 @@ impl CircuitState { } } -impl Default for CircuitState { - fn default() -> Self { - Self::Closed - } -} - /// Types of failures that trip the circuit breaker. /// /// Each failure type counts toward the threshold. The type is recorded