## Root Cause Claims file was in applications/aphoria/.aphoria/ but all commands looked for .aphoria/claims.toml relative to project root. Additionally, .aphoria/ was fully gitignored, preventing version control of claims. ## Changes ### Path Fixes - Move claims.toml from applications/aphoria/.aphoria/ to .aphoria/ at project root - Update .gitignore: .aphoria/ → .aphoria/* with !.aphoria/claims.toml exception - Now claims can be version controlled while keys remain secret ### Verify Integration (Scanner) - scanner.rs: Load claims from ClaimsFile and call verify_claims() - ScanResult: Add verify field with VerifyReport - Report formatters: Add claim verification sections showing PASS/CONFLICT/MISSING ### Clippy Fix - report/json.rs: Replace filter().map().expect() with filter_map() ## Verification - aphoria scan . → Shows claim verification with verdicts - aphoria verify run → Per-claim verification results - aphoria verify map → Extractor coverage mapping (7/10 claims = 70%) - aphoria claims list → Reads from project root - aphoria claims create → Writes to project root - All tests pass (1120+ aphoria tests) - clippy --workspace passes ## Impact Both primary use cases now work: 1. Day-to-day (commit-time): Skills can read/create claims via CLI 2. Audit (scan-time): Scanner verifies code against authored claims Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
82 lines
3.0 KiB
Rust
82 lines
3.0 KiB
Rust
//! JSON-formatted output for verification reports.
|
|
|
|
use crate::report::object_value_to_json;
|
|
use crate::verify::{AuditVerdict, VerifyReport};
|
|
|
|
/// Format a verification report as JSON.
|
|
pub fn format_verify_json(report: &VerifyReport, show_unclaimed: bool) -> String {
|
|
let results: Vec<serde_json::Value> = report
|
|
.results
|
|
.iter()
|
|
.filter(|r| r.verdict != AuditVerdict::Unclaimed || show_unclaimed)
|
|
.map(|r| {
|
|
let observations: Vec<serde_json::Value> = r
|
|
.matching_observations
|
|
.iter()
|
|
.map(|o| {
|
|
serde_json::json!({
|
|
"concept_path": o.concept_path,
|
|
"predicate": o.predicate,
|
|
"value": object_value_to_json(&o.value),
|
|
"file": o.file,
|
|
"line": o.line,
|
|
"confidence": o.confidence,
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
if let Some(ref claim) = r.claim {
|
|
serde_json::json!({
|
|
"claim_id": claim.id,
|
|
"concept_path": claim.concept_path,
|
|
"predicate": claim.predicate,
|
|
"verdict": r.verdict,
|
|
"comparison": claim.comparison.to_string(),
|
|
"explanation": r.explanation,
|
|
"matching_observations": observations,
|
|
})
|
|
} else {
|
|
// Unclaimed observation — no backing claim
|
|
let obs = r.matching_observations.first();
|
|
serde_json::json!({
|
|
"claim_id": serde_json::Value::Null,
|
|
"concept_path": obs.map(|o| o.concept_path.as_str()).unwrap_or(""),
|
|
"predicate": obs.map(|o| o.predicate.as_str()).unwrap_or(""),
|
|
"verdict": r.verdict,
|
|
"comparison": serde_json::Value::Null,
|
|
"explanation": r.explanation,
|
|
"matching_observations": observations,
|
|
})
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
let output = serde_json::json!({
|
|
"summary": {
|
|
"total_claims": report.summary.total_claims,
|
|
"pass": report.summary.pass,
|
|
"conflict": report.summary.conflict,
|
|
"missing": report.summary.missing,
|
|
"unclaimed": report.summary.unclaimed,
|
|
},
|
|
"results": results,
|
|
});
|
|
|
|
serde_json::to_string_pretty(&output).unwrap_or_else(|_| "{}".to_string())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::verify::{VerifyReport, VerifySummary};
|
|
|
|
#[test]
|
|
fn test_empty_report_json() {
|
|
let report = VerifyReport { results: vec![], summary: VerifySummary::default() };
|
|
let output = format_verify_json(&report, false);
|
|
let parsed: serde_json::Value = serde_json::from_str(&output).expect("valid json");
|
|
assert_eq!(parsed["summary"]["total_claims"], 0);
|
|
assert_eq!(parsed["results"].as_array().map(|a| a.len()), Some(0));
|
|
}
|
|
}
|