feat: schema projections, command transactions, quantum-secure encryption
This commit is contained in:
@@ -12,6 +12,10 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
engram-core = { path = "../engram-core" }
|
||||
engram-sync = { path = "../engram-sync" }
|
||||
engram-projection = { path = "../engram-projection" }
|
||||
engram-tx = { path = "../engram-tx" }
|
||||
engram-crypto = { path = "../engram-crypto" }
|
||||
sled = "0.34"
|
||||
axum = { version = "0.7", features = ["json"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tower = "0.4"
|
||||
|
||||
@@ -36,7 +36,9 @@ use axum::{
|
||||
Router,
|
||||
};
|
||||
use engram_core::EngramDb;
|
||||
use engram_projection::registry::ProjectionRegistry;
|
||||
use engram_sync::{SyncConfig, SyncEngine};
|
||||
use engram_tx::TransactionEngine;
|
||||
use mime_guess::from_path;
|
||||
use rust_embed::RustEmbed;
|
||||
use tokio::time::interval;
|
||||
@@ -109,6 +111,18 @@ async fn main() -> anyhow::Result<()> {
|
||||
let db = EngramDb::open(&PathBuf::from(&db_path))?;
|
||||
let db = Arc::new(Mutex::new(db));
|
||||
|
||||
// Transaction engine (separate sled db alongside the main db)
|
||||
let tx_log_path = format!("{}-tx-log", db_path);
|
||||
let tx_log_db = sled::open(&tx_log_path)?;
|
||||
let tx_engine = Arc::new(Mutex::new(TransactionEngine::new(
|
||||
db.clone(),
|
||||
tx_log_db,
|
||||
Some(uuid::Uuid::new_v4()),
|
||||
)));
|
||||
|
||||
// Projection registry
|
||||
let projection_registry = Arc::new(Mutex::new(ProjectionRegistry::new()));
|
||||
|
||||
info!("Database opened at {}", db_path);
|
||||
|
||||
// Sync engine — wrapped in tokio::sync::Mutex so it can be held across .await
|
||||
@@ -164,6 +178,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
db: db.clone(),
|
||||
sync_engine: sync_engine.clone(),
|
||||
api_key: api_key.clone(),
|
||||
projection_registry,
|
||||
tx_engine,
|
||||
});
|
||||
|
||||
// Protected sync/swarm routes (auth middleware applied)
|
||||
@@ -192,6 +208,26 @@ async fn main() -> anyhow::Result<()> {
|
||||
.route("/decay", post(routes::core::decay))
|
||||
.route("/consolidate", post(routes::core::consolidate));
|
||||
|
||||
// Projection routes (no auth)
|
||||
let projection_routes = Router::new()
|
||||
.route("/projections", post(routes::projection::register_projection))
|
||||
.route("/projections", get(routes::projection::list_projections))
|
||||
.route(
|
||||
"/projections/{name}/schema",
|
||||
get(routes::projection::get_projection_schema),
|
||||
)
|
||||
.route(
|
||||
"/projections/{name}/query",
|
||||
post(routes::projection::query_projection),
|
||||
);
|
||||
|
||||
// Transaction routes (no auth — add auth layer if needed)
|
||||
let tx_routes = Router::new()
|
||||
.route("/tx/apply", post(routes::tx::tx_apply))
|
||||
.route("/tx/rollback/{command_id}", post(routes::tx::tx_rollback))
|
||||
.route("/tx/history", get(routes::tx::tx_history))
|
||||
.route("/tx/chain/{command_id}", get(routes::tx::tx_causal_chain));
|
||||
|
||||
let studio_routes = Router::new()
|
||||
.route("/", get(serve_studio_index))
|
||||
.route("/studio", get(serve_studio_index))
|
||||
@@ -201,6 +237,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
.merge(studio_routes)
|
||||
.merge(core_routes)
|
||||
.merge(sync_routes)
|
||||
.merge(projection_routes)
|
||||
.merge(tx_routes)
|
||||
.layer(CorsLayer::permissive())
|
||||
.with_state(state);
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod core;
|
||||
pub mod projection;
|
||||
pub mod sync;
|
||||
pub mod swarm;
|
||||
pub mod tx;
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/// Projection API routes.
|
||||
///
|
||||
/// POST /projections — register a projection schema
|
||||
/// GET /projections — list all registered schemas
|
||||
/// POST /projections/{name}/query — run activation then project results
|
||||
/// GET /projections/{name}/schema — get a schema definition
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use engram_projection::{
|
||||
engine::ProjectionEngine,
|
||||
schema::{ProjectionResult, ProjectionSchema},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
// ── POST /projections ─────────────────────────────────────────────────────────
|
||||
|
||||
pub async fn register_projection(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(schema): Json<ProjectionSchema>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
let mut reg = state
|
||||
.projection_registry
|
||||
.lock()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
reg.upsert(schema)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
Ok(StatusCode::CREATED)
|
||||
}
|
||||
|
||||
// ── GET /projections ──────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ListProjectionsResponse {
|
||||
pub schemas: Vec<ProjectionSchema>,
|
||||
}
|
||||
|
||||
pub async fn list_projections(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<ListProjectionsResponse>, StatusCode> {
|
||||
let reg = state
|
||||
.projection_registry
|
||||
.lock()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let schemas = reg.list().into_iter().cloned().collect();
|
||||
Ok(Json(ListProjectionsResponse { schemas }))
|
||||
}
|
||||
|
||||
// ── GET /projections/{name}/schema ────────────────────────────────────────────
|
||||
|
||||
pub async fn get_projection_schema(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(name): Path<String>,
|
||||
) -> Result<Json<ProjectionSchema>, StatusCode> {
|
||||
let reg = state
|
||||
.projection_registry
|
||||
.lock()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
match reg.get(&name) {
|
||||
Ok(schema) => Ok(Json(schema.clone())),
|
||||
Err(_) => Err(StatusCode::NOT_FOUND),
|
||||
}
|
||||
}
|
||||
|
||||
// ── POST /projections/{name}/query ────────────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ProjectionQueryRequest {
|
||||
/// Seed node IDs for spreading activation.
|
||||
pub seeds: Vec<Uuid>,
|
||||
/// Query embedding for spreading activation.
|
||||
pub query_embedding: Vec<f32>,
|
||||
#[serde(default = "default_depth")]
|
||||
pub max_depth: u8,
|
||||
#[serde(default = "default_limit")]
|
||||
pub limit: usize,
|
||||
}
|
||||
|
||||
fn default_depth() -> u8 {
|
||||
3
|
||||
}
|
||||
fn default_limit() -> usize {
|
||||
20
|
||||
}
|
||||
|
||||
pub async fn query_projection(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(name): Path<String>,
|
||||
Json(req): Json<ProjectionQueryRequest>,
|
||||
) -> Result<Json<ProjectionResult>, StatusCode> {
|
||||
// Load schema
|
||||
let schema = {
|
||||
let reg = state
|
||||
.projection_registry
|
||||
.lock()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
match reg.get(&name) {
|
||||
Ok(s) => s.clone(),
|
||||
Err(_) => return Err(StatusCode::NOT_FOUND),
|
||||
}
|
||||
};
|
||||
|
||||
// Run spreading activation
|
||||
let activated = {
|
||||
let db = state.db.lock().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
db.activate(&req.seeds, &req.query_embedding, req.max_depth, req.limit)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
};
|
||||
|
||||
// Apply projection
|
||||
let result = ProjectionEngine::project(&schema, &activated)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/// Transaction API routes.
|
||||
///
|
||||
/// POST /tx/apply — apply a command
|
||||
/// POST /tx/rollback/{id} — roll back a command
|
||||
/// GET /tx/history?since={ms} — command history
|
||||
/// GET /tx/chain/{id} — causal chain for a command
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use engram_tx::{Command, CommandResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
// ── POST /tx/apply ────────────────────────────────────────────────────────────
|
||||
|
||||
pub async fn tx_apply(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(cmd): Json<Command>,
|
||||
) -> Result<Json<CommandResult>, StatusCode> {
|
||||
let mut engine = state
|
||||
.tx_engine
|
||||
.lock()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let result = engine
|
||||
.apply(cmd)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
// ── POST /tx/rollback/{command_id} ───────────────────────────────────────────
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct RollbackResponse {
|
||||
pub rollback_command_id: Uuid,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
pub async fn tx_rollback(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(command_id): Path<Uuid>,
|
||||
) -> Result<Json<RollbackResponse>, StatusCode> {
|
||||
let mut engine = state
|
||||
.tx_engine
|
||||
.lock()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let rb = engine
|
||||
.rollback(command_id)
|
||||
.map_err(|e| {
|
||||
tracing::warn!("rollback failed: {}", e);
|
||||
StatusCode::BAD_REQUEST
|
||||
})?;
|
||||
Ok(Json(RollbackResponse {
|
||||
rollback_command_id: rb.id,
|
||||
status: format!("{:?}", rb.status),
|
||||
}))
|
||||
}
|
||||
|
||||
// ── GET /tx/history ───────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct HistoryParams {
|
||||
pub since: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct HistoryResponse {
|
||||
pub commands: Vec<Command>,
|
||||
}
|
||||
|
||||
pub async fn tx_history(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(params): Query<HistoryParams>,
|
||||
) -> Result<Json<HistoryResponse>, StatusCode> {
|
||||
let engine = state
|
||||
.tx_engine
|
||||
.lock()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let since = params.since.unwrap_or(0);
|
||||
let commands = engine
|
||||
.history(since)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
Ok(Json(HistoryResponse { commands }))
|
||||
}
|
||||
|
||||
// ── GET /tx/chain/{command_id} ────────────────────────────────────────────────
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CausalChainResponse {
|
||||
pub chain: Vec<Command>,
|
||||
}
|
||||
|
||||
pub async fn tx_causal_chain(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(command_id): Path<Uuid>,
|
||||
) -> Result<Json<CausalChainResponse>, StatusCode> {
|
||||
let engine = state
|
||||
.tx_engine
|
||||
.lock()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let chain = engine
|
||||
.causal_chain(command_id)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
Ok(Json(CausalChainResponse { chain }))
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
/// Shared application state for all request handlers.
|
||||
use engram_core::EngramDb;
|
||||
use engram_projection::registry::ProjectionRegistry;
|
||||
use engram_sync::SyncEngine;
|
||||
use engram_tx::TransactionEngine;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub struct AppState {
|
||||
@@ -10,4 +12,8 @@ pub struct AppState {
|
||||
pub sync_engine: Arc<tokio::sync::Mutex<SyncEngine>>,
|
||||
/// API key used to authenticate incoming sync requests
|
||||
pub api_key: String,
|
||||
/// Projection registry — named schema views over the activation surface
|
||||
pub projection_registry: Arc<Mutex<ProjectionRegistry>>,
|
||||
/// Transaction engine — append-only command log with rollback support
|
||||
pub tx_engine: Arc<Mutex<TransactionEngine>>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user