diff --git a/aphoria.toml b/aphoria.toml index 7e03d0e..ea2563c 100644 --- a/aphoria.toml +++ b/aphoria.toml @@ -51,3 +51,13 @@ include_owasp = true [aliases] # Auto-create aliases when conflicts are detected auto_create_aliases = true + +[hosted] +# Local StemeDB instance for observations sync +url = "http://127.0.0.1:18180" +project_id = "stemedb" +sync_mode = "local-and-remote" +offline_fallback = "skip" +api_key_env = "STEMEDB_API_KEY" +max_retries = 3 +retry_delay_ms = 1000 diff --git a/crates/stemedb-api/src/handlers/stemedb_claims.rs b/crates/stemedb-api/src/handlers/stemedb_claims.rs index 34fa117..3f7a2c2 100644 --- a/crates/stemedb-api/src/handlers/stemedb_claims.rs +++ b/crates/stemedb-api/src/handlers/stemedb_claims.rs @@ -118,23 +118,19 @@ pub async fn list_claims( let subjects_prefix = b"\x00SUBJECTS:claim://"; let subject_entries = state.store.scan_prefix(subjects_prefix).await?; - // For each subject, fetch all assertions + // For each subject, fetch all assertions by scanning the assertion prefix directly. + // Assertions are stored as {subject}\x00H:{hash_hex} -> serialized bytes. let mut claims = Vec::new(); for (key, _) in subject_entries { if let Some(subject) = key_codec::extract_subject_from_subjects_key(&key) { - // Fetch all assertions for this subject via subject index - let subject_key = key_codec::subject_index_key(&subject); - let hash_list = state.store.scan_prefix(&subject_key).await?; + // Scan all assertion entries for this subject: {subject}\x00H:* + let assertion_scan_prefix = key_codec::assertion_prefix(&subject); + let assertion_entries = state.store.scan_prefix(&assertion_scan_prefix).await?; - for (_, hash_bytes) in hash_list { - let hash_hex = hex::encode(&hash_bytes); - let assertion_key = key_codec::assertion_key(&subject, &hash_hex); - if let Some(data) = state.store.get(&assertion_key).await? { - if let Ok(assertion) = stemedb_core::serde::deserialize_assertion_compat(&data) - { - if let Ok(dto) = assertion_to_dto(&assertion) { - claims.push(dto); - } + for (_, data) in assertion_entries { + if let Ok(assertion) = stemedb_core::serde::deserialize_assertion_compat(&data) { + if let Ok(dto) = assertion_to_dto(&assertion) { + claims.push(dto); } } } @@ -178,24 +174,17 @@ pub async fn get_claim( let subject = format!("claim://{}/{}", concept_path, predicate); - // Scan subject index to find all hashes for this subject - let subject_key = key_codec::subject_index_key(&subject); - let hash_list = state.store.scan_prefix(&subject_key).await?; + // Scan assertion entries directly: {subject}\x00H:* -> serialized assertion bytes + let assertion_scan_prefix = key_codec::assertion_prefix(&subject); + let assertion_entries = state.store.scan_prefix(&assertion_scan_prefix).await?; - if hash_list.is_empty() { + if assertion_entries.is_empty() { return Err(ApiError::NotFound(format!("Claim not found: {}/{}", concept_path, predicate))); } - // Get the first (most recent) assertion - let (_, hash_bytes) = &hash_list[0]; - let hash_hex = hex::encode(hash_bytes); - let assertion_key = key_codec::assertion_key(&subject, &hash_hex); + let (_, data) = &assertion_entries[0]; - let data = state.store.get(&assertion_key).await?.ok_or_else(|| { - ApiError::NotFound(format!("Claim not found: {}/{}", concept_path, predicate)) - })?; - - let assertion = stemedb_core::serde::deserialize_assertion_compat(&data) + let assertion = stemedb_core::serde::deserialize_assertion_compat(data) .map_err(|e| ApiError::Serialization(format!("Failed to deserialize assertion: {e}")))?; assertion_to_dto(&assertion) @@ -227,24 +216,17 @@ pub async fn delete_claim( // Load the claim first let subject = format!("claim://{}/{}", concept_path, predicate); - // Scan subject index to find all hashes for this subject - let subject_key = key_codec::subject_index_key(&subject); - let hash_list = state.store.scan_prefix(&subject_key).await?; + // Scan assertion entries directly: {subject}\x00H:* -> serialized assertion bytes + let assertion_scan_prefix = key_codec::assertion_prefix(&subject); + let assertion_entries = state.store.scan_prefix(&assertion_scan_prefix).await?; - if hash_list.is_empty() { + if assertion_entries.is_empty() { return Err(ApiError::NotFound(format!("Claim not found: {}/{}", concept_path, predicate))); } - // Get the first (most recent) assertion - let (_, hash_bytes) = &hash_list[0]; - let hash_hex = hex::encode(hash_bytes); - let assertion_key = key_codec::assertion_key(&subject, &hash_hex); + let (_, data) = &assertion_entries[0]; - let data = state.store.get(&assertion_key).await?.ok_or_else(|| { - ApiError::NotFound(format!("Claim not found: {}/{}", concept_path, predicate)) - })?; - - let mut assertion = stemedb_core::serde::deserialize_assertion_compat(&data) + let mut assertion = stemedb_core::serde::deserialize_assertion_compat(data) .map_err(|e| ApiError::Serialization(format!("Failed to deserialize assertion: {e}")))?; // Mark as deprecated (append-only: create new version) diff --git a/scripts/validate.sh b/scripts/validate.sh index 842a293..39e1e75 100755 --- a/scripts/validate.sh +++ b/scripts/validate.sh @@ -114,10 +114,19 @@ main() { # Step 2: Start server info "Starting API server..." cd "$PROJECT_DIR" - STEMEDB_WAL_DIR="$DATA_DIR/wal" \ - STEMEDB_DB_DIR="$DATA_DIR/db" \ - STEMEDB_BIND_ADDR="$API_HOST" \ - cargo run --package stemedb-api --quiet > "$LOG_FILE" 2>&1 & + # Use release binary if available (fast startup), fall back to cargo run + local server_bin="$PROJECT_DIR/target/release/stemedb-api" + if [[ -x "$server_bin" ]]; then + STEMEDB_WAL_DIR="$DATA_DIR/wal" \ + STEMEDB_DB_DIR="$DATA_DIR/db" \ + STEMEDB_BIND_ADDR="$API_HOST" \ + "$server_bin" > "$LOG_FILE" 2>&1 & + else + STEMEDB_WAL_DIR="$DATA_DIR/wal" \ + STEMEDB_DB_DIR="$DATA_DIR/db" \ + STEMEDB_BIND_ADDR="$API_HOST" \ + cargo run --package stemedb-api --quiet > "$LOG_FILE" 2>&1 & + fi echo $! > "$PID_FILE" # Step 3: Wait for health