stemedb/crates/stemedb-api/src/handlers/health.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

62 lines
2.1 KiB
Rust

//! Handler for health checks.
use axum::{extract::State, Json};
use tracing::instrument;
use crate::{dto::HealthResponse, error::Result, state::AppState};
use stemedb_storage::{key_codec, CircuitBreakerStore, KVStore, QuarantineStore};
/// Health check endpoint.
///
/// Returns service status ("healthy"), API version, and the total number of assertions
/// currently stored in the database. Useful for monitoring and load balancer health checks.
///
/// Also updates Prometheus gauges for assertions_total, quarantine_pending,
/// and circuit_breakers_open on each call.
#[utoipa::path(
get,
path = "/v1/health",
responses(
(status = 200, description = "Service is healthy", body = HealthResponse),
),
tag = "health"
)]
#[instrument(skip(state))]
pub async fn health_check(State(state): State<AppState>) -> Result<Json<HealthResponse>> {
// Count assertions in the database
let assertions_count = count_assertions(&state).await?;
// Update Prometheus gauges (best-effort — don't fail health check)
metrics::gauge!("stemedb_assertions_total").set(assertions_count as f64);
let pending_count =
state.quarantine_store.list_pending(usize::MAX).await.map(|v| v.len()).unwrap_or_default();
metrics::gauge!("stemedb_quarantine_pending").set(pending_count as f64);
let tripped_count = state
.circuit_breaker_store
.list_tripped(usize::MAX)
.await
.map(|v| v.len())
.unwrap_or_default();
metrics::gauge!("stemedb_circuit_breakers_open").set(tripped_count as f64);
Ok(Json(HealthResponse {
status: "healthy".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
assertions_count,
}))
}
/// Count the number of assertions in the database.
async fn count_assertions(state: &AppState) -> Result<u64> {
// Read the atomic assertion count maintained by the ingestion pipeline
let count_key = key_codec::assertion_count_key();
match state.store.get(&count_key).await? {
Some(bytes) if bytes.len() == 8 => {
Ok(u64::from_le_bytes(bytes.try_into().unwrap_or([0u8; 8])))
}
_ => Ok(0),
}
}