## 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>
62 lines
2.1 KiB
Rust
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),
|
|
}
|
|
}
|