This commit adds the read path (Cortex) to complement the write path (Spine): ## Crates - stemedb-api: HTTP API with axum + utoipa OpenAPI - /v1/assert, /v1/query, /v1/epoch, /v1/skeptic, /v1/trace, /v1/audit - Metered endpoints with quota enforcement - Ed25519 signature verification - stemedb-lens: Truth resolution lenses - RecencyLens, ConsensusLens, ConfidenceLens - VoteAwareConsensusLens (Ballot Box pattern) - TrustAwareAuthorityLens (The Hive pattern) - SkepticLens (conflict analysis) - EpochAwareLens (paradigm-safe queries) - stemedb-query: Query engine with materialized views ## Storage Extensions - VoteStore: Vote aggregation with cached counts - TrustRankStore: Agent reputation with decay - AuditStore: Query audit trail - IndexStore: SP/P/S index structures - SupersessionStore: Epoch supersession chains ## SDKs - sdk/go/steme: Go HTTP client with Ed25519 signing - sdk/go/adk: ADK-Go tools for AI agents ## Documentation - Updated CLAUDE.md, architecture.md, roadmap.md - New ai-lookup entries for all services - Use case docs for consumer health intelligence - Arena roadmap for simulation advancement Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
174 lines
5.7 KiB
Rust
174 lines
5.7 KiB
Rust
//! Handler for assertion supersession endpoint.
|
|
//!
|
|
//! The supersede endpoint enables error correction without violating append-only
|
|
//! semantics. Instead of mutating an assertion, we create a supersession record
|
|
//! that points from the old (target) to the new (replacement) assertion.
|
|
//!
|
|
//! # Use Case: 3am Incident Investigation
|
|
//!
|
|
//! 1. SRE finds agent deployed bad config
|
|
//! 2. Trace shows agent queried assertion X with hash `abc123`
|
|
//! 3. Supervisor creates supersession: `abc123` → `new_hash` (Invalidate)
|
|
//! 4. Future queries skip `abc123` automatically
|
|
//! 5. Audit trail preserved: "Who fixed it? When? Why?"
|
|
|
|
use axum::{extract::State, http::StatusCode, Json};
|
|
use tracing::instrument;
|
|
|
|
use crate::{
|
|
dto::{SupersedeRequest, SupersedeResponse},
|
|
error::{ApiError, Result},
|
|
hex,
|
|
state::AppState,
|
|
};
|
|
|
|
use stemedb_core::types::{Supersession, SupersessionType};
|
|
use stemedb_storage::{GenericSupersessionStore, SupersessionStore};
|
|
|
|
/// Create a supersession record for error correction.
|
|
///
|
|
/// This endpoint accepts a supersession request, validates the fields,
|
|
/// and stores the supersession record. Future queries will automatically
|
|
/// filter out superseded assertions based on type.
|
|
///
|
|
/// # Request Body
|
|
///
|
|
/// - `target_hash`: Hash of the assertion being superseded (hex-encoded)
|
|
/// - `supersession_type`: How the target is being superseded (Invalidate, Temporal, etc.)
|
|
/// - `reason`: Human-readable explanation for audit trail
|
|
/// - `new_hash`: Hash of replacement assertion (optional)
|
|
/// - `agent_id`: Public key of agent creating the supersession
|
|
/// - `signature`: Signature over the supersession content
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```text
|
|
/// POST /v1/supersede
|
|
/// {
|
|
/// "target_hash": "abc123...",
|
|
/// "supersession_type": "Invalidate",
|
|
/// "reason": "Proposal treated as approved. See incident INC-2024-001",
|
|
/// "new_hash": "def456...",
|
|
/// "agent_id": "deadbeef...",
|
|
/// "signature": "..."
|
|
/// }
|
|
/// ```
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/v1/supersede",
|
|
request_body = SupersedeRequest,
|
|
responses(
|
|
(status = 201, description = "Supersession recorded successfully", body = SupersedeResponse),
|
|
(status = 400, description = "Invalid request parameters", body = crate::dto::ErrorResponse),
|
|
(status = 404, description = "Target assertion not found", body = crate::dto::ErrorResponse),
|
|
(status = 409, description = "Assertion already superseded", body = crate::dto::ErrorResponse),
|
|
(status = 500, description = "Internal server error", body = crate::dto::ErrorResponse)
|
|
),
|
|
tag = "supersession"
|
|
)]
|
|
#[instrument(skip(state), fields(
|
|
target_hash = %req.target_hash,
|
|
supersession_type = ?req.supersession_type,
|
|
agent_id = %req.agent_id
|
|
))]
|
|
pub async fn supersede(
|
|
State(state): State<AppState>,
|
|
Json(req): Json<SupersedeRequest>,
|
|
) -> Result<(StatusCode, Json<SupersedeResponse>)> {
|
|
// Decode and validate hex fields
|
|
let target_hash = hex::decode_hash_32(&req.target_hash)?;
|
|
let agent_id = hex::decode_agent_id(&req.agent_id)?;
|
|
let signature = hex::decode_signature(&req.signature)?;
|
|
|
|
let new_hash = req.new_hash.map(|h| hex::decode_hash_32(&h)).transpose()?;
|
|
|
|
// Validate reason is not empty
|
|
if req.reason.trim().is_empty() {
|
|
return Err(ApiError::InvalidRequest(
|
|
"Reason is required for supersession audit trail".to_string(),
|
|
));
|
|
}
|
|
|
|
// Get current timestamp
|
|
let timestamp = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map_err(|e| ApiError::InvalidRequest(format!("System clock error: {}", e)))?
|
|
.as_secs();
|
|
|
|
// Create supersession store
|
|
let supersession_store = GenericSupersessionStore::new(state.store.clone());
|
|
|
|
// Check if already superseded
|
|
if supersession_store
|
|
.is_superseded(&target_hash)
|
|
.await
|
|
.map_err(|e| ApiError::Internal(format!("Failed to check supersession status: {}", e)))?
|
|
{
|
|
return Err(ApiError::Conflict(format!(
|
|
"Assertion {} is already superseded",
|
|
req.target_hash
|
|
)));
|
|
}
|
|
|
|
// Convert DTO type to core type
|
|
let supersession_type: SupersessionType = req.supersession_type.into();
|
|
|
|
// Create supersession record
|
|
let supersession = Supersession {
|
|
target_hash,
|
|
supersession_type,
|
|
reason: req.reason.clone(),
|
|
new_hash,
|
|
timestamp,
|
|
agent_id,
|
|
signature,
|
|
};
|
|
|
|
// Store the supersession
|
|
supersession_store
|
|
.put_supersession(&supersession)
|
|
.await
|
|
.map_err(|e| ApiError::Internal(format!("Failed to store supersession: {}", e)))?;
|
|
|
|
// Build response
|
|
let response = SupersedeResponse {
|
|
status: "superseded".to_string(),
|
|
target_hash: req.target_hash,
|
|
supersession_type: req.supersession_type,
|
|
timestamp,
|
|
};
|
|
|
|
Ok((StatusCode::CREATED, Json(response)))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::dto::SupersessionTypeDto;
|
|
|
|
#[test]
|
|
fn test_supersession_type_conversion() {
|
|
// Test all DTO types convert correctly
|
|
assert_eq!(
|
|
SupersessionType::from(SupersessionTypeDto::Invalidate),
|
|
SupersessionType::Invalidate
|
|
);
|
|
assert_eq!(
|
|
SupersessionType::from(SupersessionTypeDto::Temporal),
|
|
SupersessionType::Temporal
|
|
);
|
|
assert_eq!(
|
|
SupersessionType::from(SupersessionTypeDto::Refinement),
|
|
SupersessionType::Refinement
|
|
);
|
|
assert_eq!(
|
|
SupersessionType::from(SupersessionTypeDto::RequiresReview),
|
|
SupersessionType::RequiresReview
|
|
);
|
|
assert_eq!(
|
|
SupersessionType::from(SupersessionTypeDto::Additive),
|
|
SupersessionType::Additive
|
|
);
|
|
}
|
|
}
|