//! Handler for constraints (agent pre-flight check) queries. use axum::{ extract::{Query as AxumQuery, State}, Json, }; use std::time::{SystemTime, UNIX_EPOCH}; use tracing::instrument; use crate::{ dto::{ConstraintEntryDto, ConstraintsQueryParams, ConstraintsResponse, ErrorResponse}, error::Result, state::AppState, }; use stemedb_core::types::Assertion; use stemedb_lens::ConstraintsLens; use stemedb_query::Query; /// Query for constraint analysis using ConstraintsLens. /// /// This endpoint provides pre-flight constraint checking for AI agents, /// categorizing assertions by predicate pattern into must_use, forbidden, and prefer. /// /// # Predicate Patterns /// /// - `must_use:*` → Required, non-negotiable constraints /// - `forbidden:*` → Explicitly banned items /// - `prefer:*` → Recommended but optional /// /// # Response /// /// Returns a `ConstraintsResponse` with: /// - `must_use`: Required constraints sorted by confidence /// - `forbidden`: Banned items sorted by confidence /// - `prefer`: Recommendations sorted by confidence /// /// # Example /// /// ```ignore /// GET /v1/constraints?subject=project_alpha /// /// { /// "subject": "project_alpha", /// "must_use": [ /// { "category": "http_client", "value": {"type": "Text", "value": "axios"}, "confidence": 0.95 } /// ], /// "forbidden": [ /// { "category": "http_client", "value": {"type": "Text", "value": "requests"}, "confidence": 0.9 } /// ], /// "prefer": [ /// { "category": "language", "value": {"type": "Text", "value": "typescript"}, "confidence": 0.8 } /// ] /// } /// ``` #[utoipa::path( get, path = "/v1/constraints", params(ConstraintsQueryParams), responses( (status = 200, description = "Constraint analysis successful", body = ConstraintsResponse), (status = 400, description = "Invalid request", body = ErrorResponse), (status = 500, description = "Internal server error", body = ErrorResponse) ), tag = "query" )] #[instrument(skip(state), fields(subject = %params.subject))] pub async fn constraints_query( State(state): State, AxumQuery(params): AxumQuery, ) -> Result> { let query_start = std::time::Instant::now(); metrics::counter!("stemedb_queries_total", "endpoint" => "constraints").increment(1); // Build query for all assertions with this subject // We need ALL predicates, not just one specific one let query = Query::builder().subject(¶ms.subject).build(); // Execute the query let query_engine = state.query_engine(); let result = query_engine.execute(&query).await?; // Apply ConstraintsLens to categorize let lens = ConstraintsLens; let constraint_set = lens.resolve_constraints(&result.assertions); // Get current timestamp let computed_at = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0); // Convert to DTOs let must_use = constraint_set .must_use .into_iter() .map(|a| assertion_to_constraint_entry(a, "must_use:")) .collect(); let forbidden = constraint_set .forbidden .into_iter() .map(|a| assertion_to_constraint_entry(a, "forbidden:")) .collect(); let prefer = constraint_set .prefer .into_iter() .map(|a| assertion_to_constraint_entry(a, "prefer:")) .collect(); metrics::histogram!("stemedb_query_latency_seconds", "endpoint" => "constraints") .record(query_start.elapsed().as_secs_f64()); Ok(Json(ConstraintsResponse { subject: params.subject, must_use, forbidden, prefer, candidates_count: constraint_set.candidates_count, conflict_score: constraint_set.conflict_score, computed_at, })) } /// Convert an assertion to a ConstraintEntryDto. /// /// Extracts the category from the predicate by removing the prefix. fn assertion_to_constraint_entry(assertion: Assertion, prefix: &str) -> ConstraintEntryDto { // Extract category from predicate (e.g., "must_use:http_client" -> "http_client") let category = assertion.predicate.strip_prefix(prefix).unwrap_or(&assertion.predicate).to_string(); // Compute assertion hash let hash = stemedb_core::serde::serialize(&assertion) .map(|bytes| hex::encode(blake3::hash(&bytes).as_bytes())) .unwrap_or_default(); ConstraintEntryDto { category, value: assertion.object.into(), confidence: assertion.confidence, assertion_hash: hash, timestamp: assertion.timestamp, source_class: assertion.source_class.into(), } } #[cfg(test)] mod tests { // Integration tests would go here // For now, the unit tests in stemedb-lens/src/constraints.rs cover the core functionality }