From 4096967c203bfcdcaf114b72b0de9e47a348cb2a Mon Sep 17 00:00:00 2001 From: jordan Date: Mon, 23 Feb 2026 15:12:44 -0700 Subject: [PATCH] fix: fix claims API scan prefix bug and add hosted Aphoria config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - stemedb_claims.rs: fix list/get/delete handlers using wrong scan key - Was scanning subject_index_key ({subject}\x00S:) which stores a single Vec — scan_prefix finds nothing on a single key - Fix: use assertion_prefix ({subject}\x00H:*) to scan all assertions - GET /v1/claims was returning [] even after creating claims - aphoria.toml: add [hosted] section pointing to local StemeDB (18180) - Enables aphoria scan --persist to push observations to StemeDB - scripts/validate.sh: use release binary if available for fast startup - --no-build flag now actually skips all compilation (sub-3s startup) Co-Authored-By: Claude Sonnet 4.6 --- aphoria.toml | 10 ++++ .../src/handlers/stemedb_claims.rs | 60 +++++++------------ scripts/validate.sh | 17 ++++-- 3 files changed, 44 insertions(+), 43 deletions(-) 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