stemedb/applications/aphoria/src/report/verify_json.rs
jml 6430ff0fd6 fix(aphoria): move claims.toml to project root and fix verify integration
## 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>
2026-02-08 11:09:57 +00:00

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));
}
}