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
|
sync: false, // Diff does not write observations
|
||||||
file_source: crate::types::FileSource::All,
|
file_source: crate::types::FileSource::All,
|
||||||
benchmark: false,
|
benchmark: false,
|
||||||
|
show_claims: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = run_scan(args, config).await?;
|
let result = run_scan(args, config).await?;
|
||||||
|
|||||||
@ -88,6 +88,10 @@ pub enum Commands {
|
|||||||
/// Run performance benchmark with timing breakdown.
|
/// Run performance benchmark with timing breakdown.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
benchmark: bool,
|
benchmark: bool,
|
||||||
|
|
||||||
|
/// Show all extracted claims in the output
|
||||||
|
#[arg(long)]
|
||||||
|
show_claims: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Manage acknowledgments (mark conflicts as intentional)
|
/// Manage acknowledgments (mark conflicts as intentional)
|
||||||
|
|||||||
@ -64,12 +64,22 @@ pub async fn handle_command(command: Commands, config: &AphoriaConfig) -> ExitCo
|
|||||||
staged,
|
staged,
|
||||||
community_preview,
|
community_preview,
|
||||||
benchmark,
|
benchmark,
|
||||||
|
show_claims,
|
||||||
} => {
|
} => {
|
||||||
if community_preview {
|
if community_preview {
|
||||||
scan::handle_community_preview(path, config).await
|
scan::handle_community_preview(path, config).await
|
||||||
} else {
|
} else {
|
||||||
scan::handle_scan(
|
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,
|
config,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@ -15,6 +15,7 @@ pub async fn handle_scan(
|
|||||||
sync: bool,
|
sync: bool,
|
||||||
staged: bool,
|
staged: bool,
|
||||||
benchmark: bool,
|
benchmark: bool,
|
||||||
|
show_claims: bool,
|
||||||
config: &AphoriaConfig,
|
config: &AphoriaConfig,
|
||||||
) -> ExitCode {
|
) -> ExitCode {
|
||||||
// Validate: --sync requires --persist
|
// Validate: --sync requires --persist
|
||||||
@ -36,6 +37,7 @@ pub async fn handle_scan(
|
|||||||
sync,
|
sync,
|
||||||
file_source,
|
file_source,
|
||||||
benchmark,
|
benchmark,
|
||||||
|
show_claims,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply stricter thresholds if requested
|
// Apply stricter thresholds if requested
|
||||||
@ -99,6 +101,7 @@ pub async fn handle_community_preview(
|
|||||||
sync: false,
|
sync: false,
|
||||||
file_source: FileSource::All,
|
file_source: FileSource::All,
|
||||||
benchmark: false,
|
benchmark: false,
|
||||||
|
show_claims: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let claims = match extract_claims(&args, config).await {
|
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,
|
/// Used to classify the type of value extracted from code patterns,
|
||||||
/// enabling proper placeholder generation during normalization.
|
/// 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")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum ValueType {
|
pub enum ValueType {
|
||||||
/// Text/string value (e.g., "TLSv1.2", "admin")
|
/// Text/string value (e.g., "TLSv1.2", "admin")
|
||||||
|
#[default]
|
||||||
Text,
|
Text,
|
||||||
/// Numeric value (e.g., 4096, 30)
|
/// Numeric value (e.g., 4096, 30)
|
||||||
Number,
|
Number,
|
||||||
@ -29,12 +30,6 @@ pub enum ValueType {
|
|||||||
Boolean,
|
Boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ValueType {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for ValueType {
|
impl std::fmt::Display for ValueType {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
|||||||
@ -11,12 +11,13 @@ use uuid::Uuid;
|
|||||||
///
|
///
|
||||||
/// Patterns move through this state machine as they age, become obsolete,
|
/// Patterns move through this state machine as they age, become obsolete,
|
||||||
/// or are replaced by better alternatives.
|
/// 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")]
|
#[serde(tag = "status", rename_all = "snake_case")]
|
||||||
pub enum KnowledgeStatus {
|
pub enum KnowledgeStatus {
|
||||||
/// Pattern is active and should trigger matches.
|
/// Pattern is active and should trigger matches.
|
||||||
///
|
///
|
||||||
/// This is the default state for all patterns.
|
/// This is the default state for all patterns.
|
||||||
|
#[default]
|
||||||
Active,
|
Active,
|
||||||
|
|
||||||
/// Pattern is deprecated but still 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 {
|
impl KnowledgeStatus {
|
||||||
/// Check if this status is active (pattern should match during scans).
|
/// Check if this status is active (pattern should match during scans).
|
||||||
pub fn is_active(&self) -> bool {
|
pub fn is_active(&self) -> bool {
|
||||||
|
|||||||
@ -153,6 +153,26 @@ impl ReportFormatter for JsonReport {
|
|||||||
"deprecated_usages": deprecated_json,
|
"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
|
// Add timing if benchmark mode was enabled
|
||||||
if let Some(timing) = &result.timing {
|
if let Some(timing) = &result.timing {
|
||||||
let mut timing_json = serde_json::json!({
|
let mut timing_json = serde_json::json!({
|
||||||
@ -215,6 +235,7 @@ mod tests {
|
|||||||
debug: false,
|
debug: false,
|
||||||
observations_recorded: 0,
|
observations_recorded: 0,
|
||||||
timing: None,
|
timing: None,
|
||||||
|
claims: None,
|
||||||
deprecated_usages: vec![],
|
deprecated_usages: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -32,6 +32,31 @@ impl ReportFormatter for MarkdownReport {
|
|||||||
|
|
||||||
if result.conflicts.is_empty() && result.drifts.is_empty() {
|
if result.conflicts.is_empty() && result.drifts.is_empty() {
|
||||||
out.push_str("No conflicts or drifts found.\n");
|
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;
|
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
|
out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -280,6 +329,7 @@ mod tests {
|
|||||||
debug: false,
|
debug: false,
|
||||||
observations_recorded: 0,
|
observations_recorded: 0,
|
||||||
timing: None,
|
timing: None,
|
||||||
|
claims: None,
|
||||||
deprecated_usages: vec![],
|
deprecated_usages: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -297,10 +297,69 @@ impl ReportFormatter for SarifReport {
|
|||||||
})
|
})
|
||||||
.collect();
|
.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
|
// Combine all results
|
||||||
let mut all_results = results;
|
let mut all_results = results;
|
||||||
all_results.extend(drift_results);
|
all_results.extend(drift_results);
|
||||||
all_results.extend(deprecated_results);
|
all_results.extend(deprecated_results);
|
||||||
|
all_results.extend(claims_results);
|
||||||
|
|
||||||
let sarif = serde_json::json!({
|
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",
|
"$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,
|
debug: false,
|
||||||
observations_recorded: 0,
|
observations_recorded: 0,
|
||||||
timing: None,
|
timing: None,
|
||||||
|
claims: None,
|
||||||
deprecated_usages: vec![],
|
deprecated_usages: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -32,6 +32,40 @@ impl ReportFormatter for TableReport {
|
|||||||
|
|
||||||
if result.conflicts.is_empty() && result.drifts.is_empty() {
|
if result.conflicts.is_empty() && result.drifts.is_empty() {
|
||||||
output.push_str("No conflicts or drifts found.\n");
|
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;
|
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
|
// Footer summary
|
||||||
let block_count = result.count_by_verdict(Verdict::Block);
|
let block_count = result.count_by_verdict(Verdict::Block);
|
||||||
let flag_count = result.count_by_verdict(Verdict::Flag);
|
let flag_count = result.count_by_verdict(Verdict::Flag);
|
||||||
@ -335,6 +402,7 @@ mod tests {
|
|||||||
debug: false,
|
debug: false,
|
||||||
observations_recorded: 0,
|
observations_recorded: 0,
|
||||||
timing: None,
|
timing: None,
|
||||||
|
claims: None,
|
||||||
deprecated_usages: vec![],
|
deprecated_usages: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,7 +87,16 @@ pub async fn run_scan(args: ScanArgs, config: &AphoriaConfig) -> Result<ScanResu
|
|||||||
None
|
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 =
|
let project_name =
|
||||||
project_root.file_name().and_then(|s| s.to_str()).unwrap_or("unknown").to_string();
|
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,
|
debug: args.debug,
|
||||||
observations_recorded: result.observations_recorded,
|
observations_recorded: result.observations_recorded,
|
||||||
timing,
|
timing,
|
||||||
|
claims,
|
||||||
deprecated_usages: vec![], // TODO: Populate from lifecycle store during scan
|
deprecated_usages: vec![], // TODO: Populate from lifecycle store during scan
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,6 +45,7 @@ async fn test_conflict_detection_tls_disabled() {
|
|||||||
sync: false,
|
sync: false,
|
||||||
file_source: FileSource::All,
|
file_source: FileSource::All,
|
||||||
benchmark: false,
|
benchmark: false,
|
||||||
|
show_claims: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut config = AphoriaConfig::default();
|
let mut config = AphoriaConfig::default();
|
||||||
@ -112,6 +113,7 @@ async fn test_conflict_detection_jwt_audience_disabled() {
|
|||||||
sync: false,
|
sync: false,
|
||||||
file_source: FileSource::All,
|
file_source: FileSource::All,
|
||||||
benchmark: false,
|
benchmark: false,
|
||||||
|
show_claims: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut config = AphoriaConfig::default();
|
let mut config = AphoriaConfig::default();
|
||||||
@ -181,6 +183,7 @@ async fn test_no_conflicts_when_compliant() {
|
|||||||
sync: false,
|
sync: false,
|
||||||
file_source: FileSource::All,
|
file_source: FileSource::All,
|
||||||
benchmark: false,
|
benchmark: false,
|
||||||
|
show_claims: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut config = AphoriaConfig::default();
|
let mut config = AphoriaConfig::default();
|
||||||
|
|||||||
@ -68,6 +68,7 @@ fn test_scan_result_has_drifts() {
|
|||||||
observations_recorded: 0,
|
observations_recorded: 0,
|
||||||
timing: None,
|
timing: None,
|
||||||
deprecated_usages: vec![],
|
deprecated_usages: vec![],
|
||||||
|
claims: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(result.has_drifts());
|
assert!(result.has_drifts());
|
||||||
@ -101,6 +102,7 @@ fn test_drift_json_output_format() {
|
|||||||
observations_recorded: 0,
|
observations_recorded: 0,
|
||||||
timing: None,
|
timing: None,
|
||||||
deprecated_usages: vec![],
|
deprecated_usages: vec![],
|
||||||
|
claims: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let formatter = JsonReport;
|
let formatter = JsonReport;
|
||||||
@ -136,6 +138,7 @@ fn test_drift_sarif_output_format() {
|
|||||||
observations_recorded: 0,
|
observations_recorded: 0,
|
||||||
timing: None,
|
timing: None,
|
||||||
deprecated_usages: vec![],
|
deprecated_usages: vec![],
|
||||||
|
claims: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let formatter = SarifReport;
|
let formatter = SarifReport;
|
||||||
@ -173,6 +176,7 @@ fn test_drift_table_output_format() {
|
|||||||
observations_recorded: 0,
|
observations_recorded: 0,
|
||||||
timing: None,
|
timing: None,
|
||||||
deprecated_usages: vec![],
|
deprecated_usages: vec![],
|
||||||
|
claims: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let formatter = TableReport;
|
let formatter = TableReport;
|
||||||
|
|||||||
@ -128,6 +128,7 @@ version = "0.1.0"
|
|||||||
sync: false,
|
sync: false,
|
||||||
file_source: FileSource::All,
|
file_source: FileSource::All,
|
||||||
benchmark: false,
|
benchmark: false,
|
||||||
|
show_claims: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = run_scan(args, &config_b).await.expect("scan should succeed");
|
let result = run_scan(args, &config_b).await.expect("scan should succeed");
|
||||||
|
|||||||
@ -39,6 +39,7 @@ async fn test_scan_returns_result() {
|
|||||||
sync: false,
|
sync: false,
|
||||||
file_source: FileSource::All,
|
file_source: FileSource::All,
|
||||||
benchmark: false,
|
benchmark: false,
|
||||||
|
show_claims: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut config = AphoriaConfig::default();
|
let mut config = AphoriaConfig::default();
|
||||||
|
|||||||
@ -32,6 +32,7 @@ version = "0.1.0"
|
|||||||
sync: false,
|
sync: false,
|
||||||
file_source: FileSource::All,
|
file_source: FileSource::All,
|
||||||
benchmark: false,
|
benchmark: false,
|
||||||
|
show_claims: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut config = AphoriaConfig::default();
|
let mut config = AphoriaConfig::default();
|
||||||
@ -87,6 +88,7 @@ version = "0.1.0"
|
|||||||
sync: false,
|
sync: false,
|
||||||
file_source: FileSource::All,
|
file_source: FileSource::All,
|
||||||
benchmark: false,
|
benchmark: false,
|
||||||
|
show_claims: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut config = AphoriaConfig::default();
|
let mut config = AphoriaConfig::default();
|
||||||
@ -151,6 +153,7 @@ version = "0.1.0"
|
|||||||
sync: false,
|
sync: false,
|
||||||
file_source: FileSource::All,
|
file_source: FileSource::All,
|
||||||
benchmark: false,
|
benchmark: false,
|
||||||
|
show_claims: false,
|
||||||
};
|
};
|
||||||
let ephemeral_result = run_scan(ephemeral_args, &config).await.expect("ephemeral scan");
|
let ephemeral_result = run_scan(ephemeral_args, &config).await.expect("ephemeral scan");
|
||||||
|
|
||||||
@ -165,6 +168,7 @@ version = "0.1.0"
|
|||||||
sync: false,
|
sync: false,
|
||||||
file_source: FileSource::All,
|
file_source: FileSource::All,
|
||||||
benchmark: false,
|
benchmark: false,
|
||||||
|
show_claims: false,
|
||||||
};
|
};
|
||||||
let persistent_result = run_scan(persistent_args, &config).await.expect("persistent scan");
|
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
|
sync: true, // Enable observation write-back
|
||||||
file_source: FileSource::All,
|
file_source: FileSource::All,
|
||||||
benchmark: false,
|
benchmark: false,
|
||||||
|
show_claims: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = run_scan(args, &config).await.expect("scan should succeed");
|
let result = run_scan(args, &config).await.expect("scan should succeed");
|
||||||
@ -291,6 +296,7 @@ version = "0.1.0"
|
|||||||
sync: false, // Disabled
|
sync: false, // Disabled
|
||||||
file_source: FileSource::All,
|
file_source: FileSource::All,
|
||||||
benchmark: false,
|
benchmark: false,
|
||||||
|
show_claims: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = run_scan(args, &config).await.expect("scan should succeed");
|
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
|
sync: false, // Would be ignored anyway in ephemeral mode
|
||||||
file_source: FileSource::All,
|
file_source: FileSource::All,
|
||||||
benchmark: false,
|
benchmark: false,
|
||||||
|
show_claims: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = run_scan(args, &config).await.expect("scan should succeed");
|
let result = run_scan(args, &config).await.expect("scan should succeed");
|
||||||
@ -388,6 +395,7 @@ version = "0.1.0"
|
|||||||
sync: true, // Record observations
|
sync: true, // Record observations
|
||||||
file_source: FileSource::All,
|
file_source: FileSource::All,
|
||||||
benchmark: false,
|
benchmark: false,
|
||||||
|
show_claims: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result1 = run_scan(args1, &config).await.expect("first scan should succeed");
|
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
|
sync: false, // Don't need to sync on drift detection
|
||||||
file_source: FileSource::All,
|
file_source: FileSource::All,
|
||||||
benchmark: false,
|
benchmark: false,
|
||||||
|
show_claims: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result2 = run_scan(args2, &config).await.expect("second scan should succeed");
|
let result2 = run_scan(args2, &config).await.expect("second scan should succeed");
|
||||||
@ -470,6 +479,7 @@ version = "0.1.0"
|
|||||||
sync: false,
|
sync: false,
|
||||||
file_source: FileSource::All,
|
file_source: FileSource::All,
|
||||||
benchmark: false,
|
benchmark: false,
|
||||||
|
show_claims: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = run_scan(args, &config).await.expect("scan should succeed");
|
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,
|
sync: false,
|
||||||
file_source: FileSource::Staged,
|
file_source: FileSource::Staged,
|
||||||
benchmark: false,
|
benchmark: false,
|
||||||
|
show_claims: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = run_scan(args, &config).await.expect("scan should succeed");
|
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
|
/// When enabled, timing measurements are captured for each scan phase
|
||||||
/// and included in the output.
|
/// and included in the output.
|
||||||
pub benchmark: bool,
|
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.
|
/// Arguments for the acknowledge command.
|
||||||
|
|||||||
@ -44,6 +44,12 @@ pub struct ScanResult {
|
|||||||
/// Benchmark timing breakdown (only populated when --benchmark is set).
|
/// Benchmark timing breakdown (only populated when --benchmark is set).
|
||||||
pub timing: Option<ScanTiming>,
|
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.
|
/// Deprecated pattern usages detected.
|
||||||
///
|
///
|
||||||
/// Populated when deprecated patterns are matched during scan.
|
/// Populated when deprecated patterns are matched during scan.
|
||||||
@ -87,6 +93,7 @@ impl ScanResult {
|
|||||||
debug: false,
|
debug: false,
|
||||||
observations_recorded: 0,
|
observations_recorded: 0,
|
||||||
timing: None,
|
timing: None,
|
||||||
|
claims: None,
|
||||||
deprecated_usages: vec![],
|
deprecated_usages: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -428,6 +435,7 @@ mod tests {
|
|||||||
debug: false,
|
debug: false,
|
||||||
observations_recorded: 0,
|
observations_recorded: 0,
|
||||||
timing: None,
|
timing: None,
|
||||||
|
claims: None,
|
||||||
deprecated_usages: vec![],
|
deprecated_usages: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -10,10 +10,11 @@ use rkyv::{Archive, Deserialize, Serialize};
|
|||||||
/// - **Closed**: Normal operation, requests are allowed.
|
/// - **Closed**: Normal operation, requests are allowed.
|
||||||
/// - **Open**: Circuit has tripped, requests are blocked.
|
/// - **Open**: Circuit has tripped, requests are blocked.
|
||||||
/// - **HalfOpen**: Testing after timeout, one request allowed.
|
/// - **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)]
|
#[archive(check_bytes)]
|
||||||
pub enum CircuitState {
|
pub enum CircuitState {
|
||||||
/// Normal operation - requests allowed.
|
/// Normal operation - requests allowed.
|
||||||
|
#[default]
|
||||||
Closed,
|
Closed,
|
||||||
|
|
||||||
/// Circuit tripped - requests blocked until timeout.
|
/// 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.
|
/// Types of failures that trip the circuit breaker.
|
||||||
///
|
///
|
||||||
/// Each failure type counts toward the threshold. The type is recorded
|
/// Each failure type counts toward the threshold. The type is recorded
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user