semantic-memory 0.5.1

Local-first hybrid semantic search (SQLite + FTS5 + usearch 2.25) with bitemporal truth and typed receipts
Documentation
# Phase 2 - Deterministic search context and search receipts

## Objective

Make search deterministic under an explicit context and make search behavior explainable through receipt-ready metadata.

## P1 issues covered

- F-004: filtered HNSW under-return fallback, if not completed in Phase 1
- F-006: replay-clean recency
- F-009: search receipt skeleton

## Design target

Add a search context similar to:

```rust
#[derive(Debug, Clone)]
pub struct SearchContext {
    pub evaluation_time: DateTime<Utc>,
    pub receipt_mode: ReceiptMode,
    pub exactness_profile: ExactnessProfile,
    pub request_id: Option<String>,
}

pub enum ReceiptMode {
    Disabled,
    ExplainOnly,
    ReturnReceipt,
}

pub enum ExactnessProfile {
    Default,
    PreferExact,
    AllowApproximate,
}
```

Default public APIs may create a context once:

```rust
let context = SearchContext::default_now();
```

Internal ranking must use `context.evaluation_time`, not repeated `Utc::now()` calls.

## Search receipt skeleton

Add a type equivalent to:

```rust
pub struct VectorSearchReceiptV1 {
    pub receipt_id: String,
    pub evaluation_time: DateTime<Utc>,
    pub query_embedding_digest: Option<String>,
    pub search_profile: String,
    pub candidate_backend: String,
    pub requested_candidates: usize,
    pub returned_candidates: usize,
    pub post_filter_candidates: usize,
    pub fallback: Option<String>,
    pub exact_rerank: bool,
    pub result_ids: Vec<String>,
    pub degradations: Vec<String>,
}
```

This can live as an internal struct first. It does not need full v11 artifact machinery.

## Filtered HNSW under-return rule

When HNSW returns candidates but post-filter candidates are insufficient, do not silently return weak results.

Minimum behavior:

```text
if post_filter_vector_hits < requested_top_k and filter_scope_has_more_possible_rows:
    run exact/brute-force vector search over filtered scope
    merge/rerank according to existing behavior
    set receipt fallback = "hnsw_filtered_underreturn_fallback"
```

If determining `filter_scope_has_more_possible_rows` is hard, use a conservative fallback when post-filter hits `< top_k` and filters are active.

## Tests to add

1. Recency score is identical under same `SearchContext.evaluation_time`.
2. Recency score changes predictably with different evaluation times.
3. Search no longer calls wall clock inside scoring helpers.
4. Sparse namespace/source HNSW search falls back when post-filter hits are too low.
5. Receipt records backend, requested/returned/post-filter counts, fallback, exact rerank, result IDs.
6. Receipt mode disabled preserves old API behavior/performance as much as possible.

## Acceptance criteria

- Internal ranking deterministic given context.
- Existing search APIs still work.
- New context-aware API exists.
- Filtered HNSW under-return falls back.
- Receipt data is returned or internally available.
- Tests cover frozen time and filtered fallback.

## Codex prompt

```text
Run Phase 2: deterministic search context and search receipts.

Add a SearchContext or equivalent so recency/ranking uses an explicit evaluation_time captured at the API boundary. Remove internal repeated Utc::now() behavior from scoring helpers. Add a receipt-ready search metadata type that records candidate backend, candidate counts, post-filter counts, fallback/degradation, exact rerank, and result IDs.

Add filtered-HNSW under-return fallback: if HNSW candidates exist but active namespace/source/session filters reduce vector hits below top_k, run exact/brute-force filtered fallback and mark the receipt/degradation.

Do not add TurboQuant in this phase. Keep old APIs working by wrapping them around default SearchContext.

Run search/HNSW tests plus cargo fmt/check. Report exact commands and remaining risks.
```

---