fix: fix claims API scan prefix bug and add hosted Aphoria config

- stemedb_claims.rs: fix list/get/delete handlers using wrong scan key
  - Was scanning subject_index_key ({subject}\x00S:) which stores a
    single Vec<Hash> — 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 <noreply@anthropic.com>
This commit is contained in:
jordan 2026-02-23 15:12:44 -07:00
parent cde30b9213
commit 4096967c20
3 changed files with 44 additions and 43 deletions

View File

@ -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

View File

@ -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)

View File

@ -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