feat(aphoria): add --show-claims flag to display all extracted claims
Implements the --show-claims feature requested by users who need to verify extractors are working correctly and debug false negatives. Changes: - Add `claims: Option<Vec<ExtractedClaim>>` field to ScanResult - Add `--show-claims` CLI flag to scan command - Add `show_claims: bool` parameter to ScanArgs - Populate claims in scanner when flag is set (sorted by file, then line) - Display claims in all output formats: * Table: New "Extracted Claims" section with concept/value/file/line/confidence * JSON: Top-level `claims` array with full claim details * Markdown: "## Extracted Claims" section with table * SARIF: Informational-level results (level: "note") for IDE integration User outcome: - `aphoria scan . --show-claims` displays all claims (not just conflicts) - Users can verify extractors detected their code patterns - Users can debug false negatives by seeing what WAS extracted - Builds trust through transparency Quality: - Zero breaking changes (opt-in flag, backward compatible) - All tests passing (943 passed) - Clippy clean (no warnings) - Manual testing verified all 4 output formats Addresses user feedback from /home/jml/Workspace/maxwell/.aphoria/.notes-for-aphoria-team Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c65066fd1c
commit
e73bf3c4b7
@ -49,6 +49,7 @@ pub async fn show_diff(config: &AphoriaConfig) -> Result<String, AphoriaError> {
|
||||
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?;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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<serde_json::Value> = 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![],
|
||||
};
|
||||
|
||||
|
||||
@ -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![],
|
||||
};
|
||||
|
||||
|
||||
@ -297,10 +297,69 @@ impl ReportFormatter for SarifReport {
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Add claims if present (as informational-level results)
|
||||
let claims_results: Vec<serde_json::Value> = 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![],
|
||||
};
|
||||
|
||||
|
||||
@ -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![],
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,7 +87,16 @@ pub async fn run_scan(args: ScanArgs, config: &AphoriaConfig) -> Result<ScanResu
|
||||
None
|
||||
};
|
||||
|
||||
// 6. Build result
|
||||
// 6. Populate claims if requested (clone and sort by file, then line)
|
||||
let claims = if args.show_claims {
|
||||
let mut sorted = all_claims.to_vec();
|
||||
sorted.sort_by(|a, b| a.file.cmp(&b.file).then(a.line.cmp(&b.line)));
|
||||
Some(sorted)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// 7. Build result
|
||||
let project_name =
|
||||
project_root.file_name().and_then(|s| s.to_str()).unwrap_or("unknown").to_string();
|
||||
|
||||
@ -102,6 +111,7 @@ pub async fn run_scan(args: ScanArgs, config: &AphoriaConfig) -> Result<ScanResu
|
||||
debug: args.debug,
|
||||
observations_recorded: result.observations_recorded,
|
||||
timing,
|
||||
claims,
|
||||
deprecated_usages: vec![], // TODO: Populate from lifecycle store during scan
|
||||
})
|
||||
}
|
||||
|
||||
@ -45,6 +45,7 @@ async fn test_conflict_detection_tls_disabled() {
|
||||
sync: false,
|
||||
file_source: FileSource::All,
|
||||
benchmark: false,
|
||||
show_claims: false,
|
||||
};
|
||||
|
||||
let mut config = AphoriaConfig::default();
|
||||
@ -112,6 +113,7 @@ async fn test_conflict_detection_jwt_audience_disabled() {
|
||||
sync: false,
|
||||
file_source: FileSource::All,
|
||||
benchmark: false,
|
||||
show_claims: false,
|
||||
};
|
||||
|
||||
let mut config = AphoriaConfig::default();
|
||||
@ -181,6 +183,7 @@ async fn test_no_conflicts_when_compliant() {
|
||||
sync: false,
|
||||
file_source: FileSource::All,
|
||||
benchmark: false,
|
||||
show_claims: false,
|
||||
};
|
||||
|
||||
let mut config = AphoriaConfig::default();
|
||||
|
||||
@ -68,6 +68,7 @@ fn test_scan_result_has_drifts() {
|
||||
observations_recorded: 0,
|
||||
timing: None,
|
||||
deprecated_usages: vec![],
|
||||
claims: None,
|
||||
};
|
||||
|
||||
assert!(result.has_drifts());
|
||||
@ -101,6 +102,7 @@ fn test_drift_json_output_format() {
|
||||
observations_recorded: 0,
|
||||
timing: None,
|
||||
deprecated_usages: vec![],
|
||||
claims: None,
|
||||
};
|
||||
|
||||
let formatter = JsonReport;
|
||||
@ -136,6 +138,7 @@ fn test_drift_sarif_output_format() {
|
||||
observations_recorded: 0,
|
||||
timing: None,
|
||||
deprecated_usages: vec![],
|
||||
claims: None,
|
||||
};
|
||||
|
||||
let formatter = SarifReport;
|
||||
@ -173,6 +176,7 @@ fn test_drift_table_output_format() {
|
||||
observations_recorded: 0,
|
||||
timing: None,
|
||||
deprecated_usages: vec![],
|
||||
claims: None,
|
||||
};
|
||||
|
||||
let formatter = TableReport;
|
||||
|
||||
@ -128,6 +128,7 @@ version = "0.1.0"
|
||||
sync: false,
|
||||
file_source: FileSource::All,
|
||||
benchmark: false,
|
||||
show_claims: false,
|
||||
};
|
||||
|
||||
let result = run_scan(args, &config_b).await.expect("scan should succeed");
|
||||
|
||||
@ -39,6 +39,7 @@ async fn test_scan_returns_result() {
|
||||
sync: false,
|
||||
file_source: FileSource::All,
|
||||
benchmark: false,
|
||||
show_claims: false,
|
||||
};
|
||||
|
||||
let mut config = AphoriaConfig::default();
|
||||
|
||||
@ -32,6 +32,7 @@ version = "0.1.0"
|
||||
sync: false,
|
||||
file_source: FileSource::All,
|
||||
benchmark: false,
|
||||
show_claims: false,
|
||||
};
|
||||
|
||||
let mut config = AphoriaConfig::default();
|
||||
@ -87,6 +88,7 @@ version = "0.1.0"
|
||||
sync: false,
|
||||
file_source: FileSource::All,
|
||||
benchmark: false,
|
||||
show_claims: false,
|
||||
};
|
||||
|
||||
let mut config = AphoriaConfig::default();
|
||||
@ -151,6 +153,7 @@ version = "0.1.0"
|
||||
sync: false,
|
||||
file_source: FileSource::All,
|
||||
benchmark: false,
|
||||
show_claims: false,
|
||||
};
|
||||
let ephemeral_result = run_scan(ephemeral_args, &config).await.expect("ephemeral scan");
|
||||
|
||||
@ -165,6 +168,7 @@ version = "0.1.0"
|
||||
sync: false,
|
||||
file_source: FileSource::All,
|
||||
benchmark: false,
|
||||
show_claims: false,
|
||||
};
|
||||
let persistent_result = run_scan(persistent_args, &config).await.expect("persistent scan");
|
||||
|
||||
@ -241,6 +245,7 @@ version = "0.1.0"
|
||||
sync: true, // Enable observation write-back
|
||||
file_source: FileSource::All,
|
||||
benchmark: false,
|
||||
show_claims: false,
|
||||
};
|
||||
|
||||
let result = run_scan(args, &config).await.expect("scan should succeed");
|
||||
@ -291,6 +296,7 @@ version = "0.1.0"
|
||||
sync: false, // Disabled
|
||||
file_source: FileSource::All,
|
||||
benchmark: false,
|
||||
show_claims: false,
|
||||
};
|
||||
|
||||
let result = run_scan(args, &config).await.expect("scan should succeed");
|
||||
@ -335,6 +341,7 @@ version = "0.1.0"
|
||||
sync: false, // Would be ignored anyway in ephemeral mode
|
||||
file_source: FileSource::All,
|
||||
benchmark: false,
|
||||
show_claims: false,
|
||||
};
|
||||
|
||||
let result = run_scan(args, &config).await.expect("scan should succeed");
|
||||
@ -388,6 +395,7 @@ version = "0.1.0"
|
||||
sync: true, // Record observations
|
||||
file_source: FileSource::All,
|
||||
benchmark: false,
|
||||
show_claims: false,
|
||||
};
|
||||
|
||||
let result1 = run_scan(args1, &config).await.expect("first scan should succeed");
|
||||
@ -415,6 +423,7 @@ version = "0.1.0"
|
||||
sync: false, // Don't need to sync on drift detection
|
||||
file_source: FileSource::All,
|
||||
benchmark: false,
|
||||
show_claims: false,
|
||||
};
|
||||
|
||||
let result2 = run_scan(args2, &config).await.expect("second scan should succeed");
|
||||
@ -470,6 +479,7 @@ version = "0.1.0"
|
||||
sync: false,
|
||||
file_source: FileSource::All,
|
||||
benchmark: false,
|
||||
show_claims: false,
|
||||
};
|
||||
|
||||
let result = run_scan(args, &config).await.expect("scan should succeed");
|
||||
|
||||
@ -238,6 +238,7 @@ async fn test_staged_with_persist_and_sync() {
|
||||
sync: false,
|
||||
file_source: FileSource::Staged,
|
||||
benchmark: false,
|
||||
show_claims: false,
|
||||
};
|
||||
|
||||
let result = run_scan(args, &config).await.expect("scan should succeed");
|
||||
|
||||
@ -60,6 +60,11 @@ pub struct ScanArgs {
|
||||
/// When enabled, timing measurements are captured for each scan phase
|
||||
/// and included in the output.
|
||||
pub benchmark: bool,
|
||||
|
||||
/// Show all extracted claims in the output.
|
||||
/// When enabled, all claims (not just conflicts) are included in the
|
||||
/// scan result, sorted by file path and line number.
|
||||
pub show_claims: bool,
|
||||
}
|
||||
|
||||
/// Arguments for the acknowledge command.
|
||||
|
||||
@ -44,6 +44,12 @@ pub struct ScanResult {
|
||||
/// Benchmark timing breakdown (only populated when --benchmark is set).
|
||||
pub timing: Option<ScanTiming>,
|
||||
|
||||
/// 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<Vec<ExtractedClaim>>,
|
||||
|
||||
/// 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![],
|
||||
};
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user