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:
parent
cde30b9213
commit
4096967c20
10
aphoria.toml
10
aphoria.toml
@ -51,3 +51,13 @@ include_owasp = true
|
|||||||
[aliases]
|
[aliases]
|
||||||
# Auto-create aliases when conflicts are detected
|
# Auto-create aliases when conflicts are detected
|
||||||
auto_create_aliases = true
|
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
|
||||||
|
|||||||
@ -118,23 +118,19 @@ pub async fn list_claims(
|
|||||||
let subjects_prefix = b"\x00SUBJECTS:claim://";
|
let subjects_prefix = b"\x00SUBJECTS:claim://";
|
||||||
let subject_entries = state.store.scan_prefix(subjects_prefix).await?;
|
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();
|
let mut claims = Vec::new();
|
||||||
for (key, _) in subject_entries {
|
for (key, _) in subject_entries {
|
||||||
if let Some(subject) = key_codec::extract_subject_from_subjects_key(&key) {
|
if let Some(subject) = key_codec::extract_subject_from_subjects_key(&key) {
|
||||||
// Fetch all assertions for this subject via subject index
|
// Scan all assertion entries for this subject: {subject}\x00H:*
|
||||||
let subject_key = key_codec::subject_index_key(&subject);
|
let assertion_scan_prefix = key_codec::assertion_prefix(&subject);
|
||||||
let hash_list = state.store.scan_prefix(&subject_key).await?;
|
let assertion_entries = state.store.scan_prefix(&assertion_scan_prefix).await?;
|
||||||
|
|
||||||
for (_, hash_bytes) in hash_list {
|
for (_, data) in assertion_entries {
|
||||||
let hash_hex = hex::encode(&hash_bytes);
|
if let Ok(assertion) = stemedb_core::serde::deserialize_assertion_compat(&data) {
|
||||||
let assertion_key = key_codec::assertion_key(&subject, &hash_hex);
|
if let Ok(dto) = assertion_to_dto(&assertion) {
|
||||||
if let Some(data) = state.store.get(&assertion_key).await? {
|
claims.push(dto);
|
||||||
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);
|
let subject = format!("claim://{}/{}", concept_path, predicate);
|
||||||
|
|
||||||
// Scan subject index to find all hashes for this subject
|
// Scan assertion entries directly: {subject}\x00H:* -> serialized assertion bytes
|
||||||
let subject_key = key_codec::subject_index_key(&subject);
|
let assertion_scan_prefix = key_codec::assertion_prefix(&subject);
|
||||||
let hash_list = state.store.scan_prefix(&subject_key).await?;
|
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)));
|
return Err(ApiError::NotFound(format!("Claim not found: {}/{}", concept_path, predicate)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the first (most recent) assertion
|
let (_, data) = &assertion_entries[0];
|
||||||
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 = state.store.get(&assertion_key).await?.ok_or_else(|| {
|
let assertion = stemedb_core::serde::deserialize_assertion_compat(data)
|
||||||
ApiError::NotFound(format!("Claim not found: {}/{}", concept_path, predicate))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let assertion = stemedb_core::serde::deserialize_assertion_compat(&data)
|
|
||||||
.map_err(|e| ApiError::Serialization(format!("Failed to deserialize assertion: {e}")))?;
|
.map_err(|e| ApiError::Serialization(format!("Failed to deserialize assertion: {e}")))?;
|
||||||
|
|
||||||
assertion_to_dto(&assertion)
|
assertion_to_dto(&assertion)
|
||||||
@ -227,24 +216,17 @@ pub async fn delete_claim(
|
|||||||
// Load the claim first
|
// Load the claim first
|
||||||
let subject = format!("claim://{}/{}", concept_path, predicate);
|
let subject = format!("claim://{}/{}", concept_path, predicate);
|
||||||
|
|
||||||
// Scan subject index to find all hashes for this subject
|
// Scan assertion entries directly: {subject}\x00H:* -> serialized assertion bytes
|
||||||
let subject_key = key_codec::subject_index_key(&subject);
|
let assertion_scan_prefix = key_codec::assertion_prefix(&subject);
|
||||||
let hash_list = state.store.scan_prefix(&subject_key).await?;
|
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)));
|
return Err(ApiError::NotFound(format!("Claim not found: {}/{}", concept_path, predicate)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the first (most recent) assertion
|
let (_, data) = &assertion_entries[0];
|
||||||
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 = state.store.get(&assertion_key).await?.ok_or_else(|| {
|
let mut assertion = stemedb_core::serde::deserialize_assertion_compat(data)
|
||||||
ApiError::NotFound(format!("Claim not found: {}/{}", concept_path, predicate))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut assertion = stemedb_core::serde::deserialize_assertion_compat(&data)
|
|
||||||
.map_err(|e| ApiError::Serialization(format!("Failed to deserialize assertion: {e}")))?;
|
.map_err(|e| ApiError::Serialization(format!("Failed to deserialize assertion: {e}")))?;
|
||||||
|
|
||||||
// Mark as deprecated (append-only: create new version)
|
// Mark as deprecated (append-only: create new version)
|
||||||
|
|||||||
@ -114,10 +114,19 @@ main() {
|
|||||||
# Step 2: Start server
|
# Step 2: Start server
|
||||||
info "Starting API server..."
|
info "Starting API server..."
|
||||||
cd "$PROJECT_DIR"
|
cd "$PROJECT_DIR"
|
||||||
STEMEDB_WAL_DIR="$DATA_DIR/wal" \
|
# Use release binary if available (fast startup), fall back to cargo run
|
||||||
STEMEDB_DB_DIR="$DATA_DIR/db" \
|
local server_bin="$PROJECT_DIR/target/release/stemedb-api"
|
||||||
STEMEDB_BIND_ADDR="$API_HOST" \
|
if [[ -x "$server_bin" ]]; then
|
||||||
cargo run --package stemedb-api --quiet > "$LOG_FILE" 2>&1 &
|
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"
|
echo $! > "$PID_FILE"
|
||||||
|
|
||||||
# Step 3: Wait for health
|
# Step 3: Wait for health
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user