pub struct Engine { /* private fields */ }Implementations§
Source§impl Engine
impl Engine
pub fn open(path: impl Into<PathBuf>) -> Result<OpenedEngine, EngineOpenError>
Sourcepub fn open_with_choice(
path: impl Into<PathBuf>,
choice: EmbedderChoice,
) -> Result<OpenedEngine, EngineOpenError>
pub fn open_with_choice( path: impl Into<PathBuf>, choice: EmbedderChoice, ) -> Result<OpenedEngine, EngineOpenError>
Open an engine with an explicit EmbedderChoice.
Per dev/design/embedder.md §0 + the 0.7.1 EU-5 campaign, this is
the canonical entry point for selecting how the workspace’s
default embedder is supplied. See EmbedderChoice for the
semantics of each variant; in particular Default materializes
the pinned BGE embedder via the loader when the default-embedder
feature is enabled.
pub fn open_with_migration_event_sink( path: impl Into<PathBuf>, emit_migration_event: impl FnMut(&MigrationStepReport), ) -> Result<OpenedEngine, EngineOpenError>
pub fn path(&self) -> &Path
pub fn write( &self, batch: &[PreparedWrite], ) -> Result<WriteReceipt, EngineError>
Sourcepub fn ingest_with_extractor(
&self,
cmd: &[&str],
documents: &[ExtractDocument],
) -> Result<IngestWithExtractorReceipt, EngineError>
pub fn ingest_with_extractor( &self, cmd: &[&str], documents: &[ExtractDocument], ) -> Result<IngestWithExtractorReceipt, EngineError>
G11 (Slice 15) — BYO-LLM ingest: spawn an external extraction harness
speaking the fathomdb.extract.v1 NDJSON-over-stdio protocol, send
documents for extraction, and write the resulting entities
(→ canonical_nodes) and fact-edges (→ canonical_edges with G11
enrichment columns) to the store.
cmd is argv (first element = program, rest = args). Documents are
batched per the harness’s max_docs_per_request. Entity logical_id
is derived as sha256("<type>:<name>") (lowercase, hex-encoded) for
stable cross-re-ingestion identity. Edge logical_id is derived as
sha256("<from_lid>:<to_lid>:<relation>"). Both are consistent with
G0 supersession: re-ingesting the same document yields the same ids,
triggering tombstone-then-insert rather than accumulation.
Returns EngineError::Extractor on protocol errors (bad handshake,
subprocess spawn failure, JSON decode error). no_facts warnings from
the harness are not errors and do not affect the receipt counts.
Sourcepub fn consolidate_with_provider(
&self,
cmd: &[&str],
axes: &[ConsolidateAxis],
) -> Result<ConsolidateReceipt, EngineError>
pub fn consolidate_with_provider( &self, cmd: &[&str], axes: &[ConsolidateAxis], ) -> Result<ConsolidateReceipt, EngineError>
0.8.12 Slice 15 (OPP-2, ADR-0.8.12) — BYO-LLM CONSOLIDATION / RECENCY.
The SECOND consumer of the one provider_session transport (ADR-0.8.6):
consolidation reuses the exact NDJSON-over-stdio transport, hello/ready
handshake, supported_tasks negotiation, request_id framing, and
bounded-recv timeout — only the protocol string
(fathomdb.consolidate.v1) and the task-specific payload differ. There is
NO second transport and NO second handshake.
For each (subject, relation) axis, FathomDB assembles a candidate
cluster of competing active fact-edges DETERMINISTICALLY (CPU-only, no
LLM), sends it to the caller-supplied harness, and applies the returned
verdicts. CALLER-SIDE BYO-LLM: the harness is the caller’s subprocess;
the library never embeds or calls an LLM and makes NO network egress.
Load-bearing semantic (ADR-0.8.12 §2.1): consolidation records
supersession / recency METADATA only — invalidate sets t_invalid,
supersede/merge marks the row superseded via the existing G0 tombstone
column. Edge BODIES are NEVER rewritten and NO row is ever deleted (the
0.8.3 lesson: blind content-merge HURT accuracy). The original rows
survive; the engine stays deterministic.
Returns EngineError::Consolidator on any transport/handshake/protocol
fault or a malformed / out-of-cluster verdict.
pub fn search(&self, query: &str) -> Result<SearchResult, EngineError>
Sourcepub fn search_view(
&self,
query: &str,
view: &ReadView,
) -> Result<SearchResult, EngineError>
pub fn search_view( &self, query: &str, view: &ReadView, ) -> Result<SearchResult, EngineError>
0.8.20 Slice 15b fix-2 (R-20-NV / R-20-RV) — search under an explicit
ReadView, the escape hatch matching the one the five read verbs got in
Slice 10b. search(query) is exactly search_view(query, &ReadView::default()).
Scope: the VALIDITY axis only. include_out_of_window and
valid_as_of are honoured; the EXISTENCE flags (include_superseded,
include_inactive) are refused with
EngineError::InvalidArgument rather than silently ignored. Relaxing
superseded_at IS NULL on a retrieval path would resurrect the stale-body
leak the Slice-15 fix-1 review closed, and search hydrates from projection
indexes (search_index, vector_default) that are not version-complete —
so “include superseded” has no truthful answer here. Refusing says that;
ignoring would be the dead surface this fix exists to remove.
Governed surface: PROPOSED / NOT SIGNED (0.8.20 Slice 15b fix-2).
Sourcepub fn search_reranked_view(
&self,
query: &str,
filter: Option<SearchFilter>,
rerank_depth: usize,
use_graph_arm: bool,
alpha: f64,
pool_n: usize,
explain: bool,
view: &ReadView,
) -> Result<SearchResult, EngineError>
pub fn search_reranked_view( &self, query: &str, filter: Option<SearchFilter>, rerank_depth: usize, use_graph_arm: bool, alpha: f64, pool_n: usize, explain: bool, view: &ReadView, ) -> Result<SearchResult, EngineError>
0.8.20 Slice 15b fix-2 (R-20-NV / R-20-RV) — the FULL-arity view entry
point: search_reranked /
search_explained under an explicit
ReadView. This is what the Python and TypeScript search(..., view=)
bindings call, so a caller can combine a content filter, the CE knobs and
a validity view in one query — passing view must not silently disable
the filter, and passing a filter must not silently disable view.
search_reranked(q, f, d, g, a, p) is exactly
search_reranked_view(q, f, d, g, a, p, false, &ReadView::default()).
Validity axis only; existence flags are refused. See
search_view.
Governed surface: PROPOSED / NOT SIGNED (0.8.20 Slice 15b fix-2).
Sourcepub fn search_filtered(
&self,
query: &str,
filter: Option<SearchFilter>,
) -> Result<SearchResult, EngineError>
pub fn search_filtered( &self, query: &str, filter: Option<SearchFilter>, ) -> Result<SearchResult, EngineError>
G10 — hybrid search with an optional closed SearchFilter. None
(or an all-None filter) is the unfiltered path whose phase-1 SQL is
byte-identical to 0.7.2. The filter prunes the vector branch in the
single phase-1 candidates statement and constrains the text branch by the
same metadata. Ranking is the unconditional G9 RRF fusion.
Sourcepub fn search_filter(
&self,
query: &str,
filter: &Filter,
) -> Result<SearchResult, EngineError>
pub fn search_filter( &self, query: &str, filter: &Filter, ) -> Result<SearchResult, EngineError>
0.8.11 Slice 40 (#17) — unified-Filter entry point for the vec0 search
backend. Lowers the metadata subset to the indexed pre-KNN WHERE and
typed-rejects a FilterTerm::Json term with
EngineError::InvalidFilter (D3 no-demotion guarantee). This is the
unified surface the 0.8.15 router constraints block reasons over; the
shipped Engine::search_filtered(query, Option<SearchFilter>) stays
as sugar over the same path.
Sourcepub fn search_reranked(
&self,
query: &str,
filter: Option<SearchFilter>,
rerank_depth: usize,
use_graph_arm: bool,
alpha: f64,
pool_n: usize,
) -> Result<SearchResult, EngineError>
pub fn search_reranked( &self, query: &str, filter: Option<SearchFilter>, rerank_depth: usize, use_graph_arm: bool, alpha: f64, pool_n: usize, ) -> Result<SearchResult, EngineError>
0.8.1 Slice 10 (R1) / Slice 30 (R3) — search_reranked: hybrid search
with optional CE reranking and optional graph-BFS third arm. rerank_depth = 0 is the identity (soft-fallback) path, byte-identical to
search_filtered. rerank_depth = N > 0
applies the cross-encoder over the top-N fused hits (when the
default-reranker feature is enabled and the model is loaded); without the
model, the call falls back to the fused order.
use_graph_arm = false (the default) produces byte-identical results to
the pre-Slice-30 two-arm pipeline. use_graph_arm = true seeds a BFS over
temporal fact-edges from the top-10 fused hits and fuses the reachable
nodes as a third RRF arm.
Governed surface: re-exported from fathomdb facade.
Sourcepub fn search_explained(
&self,
query: &str,
filter: Option<SearchFilter>,
rerank_depth: usize,
use_graph_arm: bool,
alpha: f64,
pool_n: usize,
) -> Result<SearchResult, EngineError>
pub fn search_explained( &self, query: &str, filter: Option<SearchFilter>, rerank_depth: usize, use_graph_arm: bool, alpha: f64, pool_n: usize, ) -> Result<SearchResult, EngineError>
0.8.8 EXP-OBS (Slice 5) — search_explained: the opt-in explain=true
surface. Identical retrieval to search_reranked
(same fused/CE ranking, same results), additionally returning a
Explanation sidecar on SearchResult.explanation with per-hit arm
provenance + score breakdown + a query-level QueryTrace. The default
search/search_filtered/search_reranked paths are unaffected and stay
byte-identical (R-OBS-2).
Governed surface: re-exported from fathomdb facade.
Sourcepub fn search_text_only(&self, query: &str) -> Result<SearchResult, EngineError>
pub fn search_text_only(&self, query: &str) -> Result<SearchResult, EngineError>
0.8.18 Slice 5 (#5 vector-equivalence probe, R-VEQ-4) — the explicit
text-only / FTS-only search path. It does NOT embed the query and does
NOT route through the vector-dependent choke point
search_inner_with_stats, so it NEVER
raises EngineError::VectorEquivalenceMismatch and stays serviceable when
the engine opened in the degraded dense_disabled state (the D2 “keep FTS
servable” contract; codex R2 U1-2). Results come from the node-body FTS
branch only — no vector recall, no CE rerank, no graph arm. Available
regardless of degraded state; when dense is healthy it is simply a
text-only view of the same corpus.
Governed surface: re-exported from the fathomdb facade + Py/TS bindings.
Sourcepub fn search_text_only_view(
&self,
query: &str,
view: &ReadView,
) -> Result<SearchResult, EngineError>
pub fn search_text_only_view( &self, query: &str, view: &ReadView, ) -> Result<SearchResult, EngineError>
0.8.20 Slice 15b fix-2 (R-20-NV / R-20-RV) — search_text_only
under an explicit ReadView. Same validity-axis-only scope, and the same
typed refusal of the existence flags, as search_view.
Governed surface: PROPOSED / NOT SIGNED (0.8.20 Slice 15b fix-2).
Sourcepub fn search_projected_text(
&self,
query: &str,
name: &str,
filter: Option<SearchFilter>,
view: &ReadView,
) -> Result<SearchResult, EngineError>
pub fn search_projected_text( &self, query: &str, name: &str, filter: Option<SearchFilter>, view: &ReadView, ) -> Result<SearchResult, EngineError>
Search one declared searchable→FTS projection without invoking body
search, vector search, score fusion, or a fallback arm. Results carry the
ordinary text branch shape and are ordered by property-FTS bm25 ascending
then write cursor ascending.
Sourcepub fn dense_disabled(&self) -> bool
pub fn dense_disabled(&self) -> bool
0.8.18 Slice 5 (R-VEQ-6) — degraded-open observability accessor. true iff
the open-time #5 self-check found a vector-equivalence divergence and every
vector-dependent arm is refusing. Mirrors OpenReport.dense_disabled; read
lock-free.
Sourcepub fn dense_disabled_reason(&self) -> Option<String>
pub fn dense_disabled_reason(&self) -> Option<String>
0.8.18 Slice 5 (R-VEQ-6) — the human-readable reason for the degraded state
(which representation tripped), or None when dense is healthy.
Sourcepub fn vector_equivalence_refusal_count(&self) -> u64
pub fn vector_equivalence_refusal_count(&self) -> u64
0.8.18 Slice 5 (R-VEQ-6) — telemetry counter: number of query-time vector-dependent-arm refusals raised because the engine opened degraded. Observable pre/post-query.
Sourcepub fn enable_telemetry(&self, sink_path: &str) -> Result<(), EngineError>
pub fn enable_telemetry(&self, sink_path: &str) -> Result<(), EngineError>
0.8.8 Slice 15 (OPP-9) — enable opt-in telemetry capture to a local JSONL
sink_path (append-only). Off by default; once enabled, each search
records a query→result event and record_feedback appends agent labels.
Local file only — no network/egress. query_id + ts_monotonic_ms are
reset deterministically on enable. Idempotent re-enable resets the seq.
Sourcepub fn last_telemetry_query_id(&self) -> Option<String>
pub fn last_telemetry_query_id(&self) -> Option<String>
0.8.8 Slice 15 — the most-recent captured query_id (for record_feedback).
None when telemetry is off or no query has been captured yet.
Sourcepub fn record_feedback(
&self,
query_id: &str,
relevant_ids: &[u64],
irrelevant_ids: &[u64],
label_source: &str,
) -> Result<(), EngineError>
pub fn record_feedback( &self, query_id: &str, relevant_ids: &[u64], irrelevant_ids: &[u64], label_source: &str, ) -> Result<(), EngineError>
0.8.8 Slice 15 — append an agent-supplied relevance-label record for a
previously-captured query_id. label_source is the only exogenous string
(caller-declared, e.g. "agent:hermes").
ID-SPACE (Cause-A, 0.8.11.2 — honest record). relevant_ids /
irrelevant_ids are the interim SearchHit.id == write_cursor (the same
space as the captured event’s result_ids), NOT logical_id. The
signature is left byte-stable: the gold pipeline maps these write_cursor
keys to the cross-session-stable id via the capture event’s parallel
result_ids ↔ result_stable_ids arrays (eval/gold_capture.py), so no
new feedback parameter — and no binding-signature churn — is required.
Errors if telemetry is off.
Sourcepub fn _graph_frontier_stats_for_test(
&self,
query: &str,
) -> Result<GraphFrontierStats, EngineError>
pub fn _graph_frontier_stats_for_test( &self, query: &str, ) -> Result<GraphFrontierStats, EngineError>
G0 Phase-2 (BLOCK-1) test seam — runs the graph-arm retrieval path and
returns the frontier meter (GraphFrontierStats) for query. Mirrors the
sanctioned set_vector_stage_only_for_test / _configure_vector_kind_for_test
pattern: kept OFF the governed surface (test/eval-only), so the meter never
appears on SearchResult. Used by the recall harness to prove the
doc-seeded frontier is empty (resolved_seed_rate == 0.0) and, post-C1, the
0→>0 flip.
Sourcepub fn read_get(
&self,
logical_id: &str,
view: &ReadView,
) -> Result<Option<NodeRecord>, EngineError>
pub fn read_get( &self, logical_id: &str, view: &ReadView, ) -> Result<Option<NodeRecord>, EngineError>
Slice 30 (G2) — read.get: active-only point lookup by logical_id.
Delegates to Engine::read_get_many; returns the single slot. A
missing/superseded id is None (a normal absence, not an error). Reads
ride the ReaderWorkerPool DEFERRED-tx path (never the writer lock).
Sourcepub fn read_get_many(
&self,
logical_ids: &[String],
view: &ReadView,
) -> Result<Vec<Option<NodeRecord>>, EngineError>
pub fn read_get_many( &self, logical_ids: &[String], view: &ReadView, ) -> Result<Vec<Option<NodeRecord>>, EngineError>
Slice 30 (G2) — read.get_many: active-only point lookup over many
logical_ids. Returns one slot per requested id in REQUEST ORDER, None
where no active row carries that id (partial, never all-or-nothing).
Sourcepub fn graph_neighbors(
&self,
root_logical_id: &str,
depth: u32,
direction: TraversalDirection,
view: &ReadView,
) -> Result<Vec<NodeRecord>, EngineError>
pub fn graph_neighbors( &self, root_logical_id: &str, depth: u32, direction: TraversalDirection, view: &ReadView, ) -> Result<Vec<NodeRecord>, EngineError>
Slice 20 (G5) — read.neighbors: bounded BFS from root_logical_id
over canonical_edges. Returns nodes reachable within depth hops
(1..=3) in the given direction, excluding the root itself.
Hard cap: 50 results (engine-enforced LIMIT 50).
Traversal filter: superseded_at IS NULL AND (t_invalid IS NULL OR t_invalid > now).
Returns Err(EngineError::InvalidArgument) for depth > 3.
Returns Ok(vec![]) for an unknown/superseded root.
Reads ride the ReaderWorkerPool DEFERRED-tx path.
Sourcepub fn search_expand(
&self,
query: &str,
filter: Option<SearchFilter>,
depth: u32,
) -> Result<SearchExpandResult, EngineError>
pub fn search_expand( &self, query: &str, filter: Option<SearchFilter>, depth: u32, ) -> Result<SearchExpandResult, EngineError>
Slice 20 (G6) — search_expand: hybrid search (G1+G9) followed by
bounded BFS expansion (G5) of each search hit. Returns the original
search hits (with RRF scores) plus nodes reachable from any hit via
up to depth hops that are NOT already in the search hit set.
Returns Err(EngineError::InvalidArgument) for depth > 3.
A depth = 0 call returns search hits with their logical_ids resolved
but no BFS expansion. Reads ride the ReaderWorkerPool DEFERRED-tx path.
Snapshot note: the search phase (search_inner) and the expansion
phase (SearchExpand reader request) run in separate DEFERRED reader
transactions; a write that lands between them is visible to expansion
but not search (or vice-versa). In practice the window is negligible for
single-process embedded use. The expansion phase mitigates drift by
filtering search_hits to only include hits whose write_cursor is
still active in the expansion snapshot (superseded hits are dropped from
the result rather than surfaced with stale data).
Sourcepub fn read_collection(
&self,
collection: &str,
after_id: Option<i64>,
limit: usize,
) -> Result<Vec<OpStoreRow>, EngineError>
pub fn read_collection( &self, collection: &str, after_id: Option<i64>, limit: usize, ) -> Result<Vec<OpStoreRow>, EngineError>
Slice 30 (G3) — read.collection: paginated op-store read-back over
operational_mutations for collection, ORDER BY id. limit is
MANDATORY (clamped to the ~1M cap); after_id is the exclusive cursor.
Reads ride the ReaderWorkerPool DEFERRED-tx path.
Sourcepub fn read_mutations(
&self,
collection: &str,
after_id: Option<i64>,
limit: usize,
) -> Result<Vec<OpStoreRow>, EngineError>
pub fn read_mutations( &self, collection: &str, after_id: Option<i64>, limit: usize, ) -> Result<Vec<OpStoreRow>, EngineError>
Slice 30 (G3) — read.mutations: the mutation-log-oriented alias surface
over the SAME op-store read-back as Engine::read_collection.
Sourcepub fn read_list(
&self,
kind: &str,
predicates: &[Predicate],
limit: usize,
view: &ReadView,
) -> Result<Vec<NodeRecord>, EngineError>
pub fn read_list( &self, kind: &str, predicates: &[Predicate], limit: usize, view: &ReadView, ) -> Result<Vec<NodeRecord>, EngineError>
Slice 35 (G4) — read.list: list active canonical_nodes of a given
kind, optionally filtered by a closed Predicate set, up to limit
rows. Returns Vec<NodeRecord> (active only; superseded_at IS NULL).
Multiple predicates are combined as AND (D-F5). An empty predicate slice
returns all active nodes of the given kind up to limit (unfiltered path).
Compilation target: json_extract(body, '$.field') <op> ? with bound
parameters (injection-safe per D-F4). See dev/adr/ADR-0.8.0-filter-grammar.md.
Path validation happens at Predicate construction time; read_list
revalidates as defense-in-depth (enum variants are pub, so direct
struct-literal construction could bypass the constructors).
Sourcepub fn read_list_filter(
&self,
kind: &str,
filter: &Filter,
limit: usize,
view: &ReadView,
) -> Result<Vec<NodeRecord>, EngineError>
pub fn read_list_filter( &self, kind: &str, filter: &Filter, limit: usize, view: &ReadView, ) -> Result<Vec<NodeRecord>, EngineError>
0.8.11 Slice 40 (#17) — unified-Filter entry point for the
canonical_nodes read.list backend. Accepts the full FilterTerm
set (D3): Json runs the shipped allowlisted json_extract path;
Status/CreatedAfter lower to allowlisted json-paths; Kind/SourceType
constant-fold against the partition kind (a guaranteed-empty fold
returns an empty Vec without touching SQL). Dispatches to the same
Engine::read_list machinery the shipped Predicate surface uses, so
every inherited invariant (superseded_at IS NULL, json_valid(body),
the canonical_nodes(kind) index, parameterized binds) is preserved.
Sourcepub fn crossed_boundary_since(
&self,
since: i64,
view: &ReadView,
) -> Result<Vec<BoundaryCrossing>, EngineError>
pub fn crossed_boundary_since( &self, since: i64, view: &ReadView, ) -> Result<Vec<BoundaryCrossing>, EngineError>
0.8.20 Slice 10b (R-20-NV) — the validity-boundary hook: which nodes
crossed a [valid_from, valid_until) boundary in the half-open interval
(since, as_of]?
since and the resolved upper bound are INTEGER epoch SECONDS. The upper
bound is the view’s own instant (view.valid_as_of, defaulting to now),
so one instant governs both the boundary interval and the view — and, as
everywhere else on this path, it is BOUND, never a datetime('now')
literal, so the answer is deterministic for a fixed (since, as_of).
A node appears once, carrying whichever of the two boundaries it crossed; a window that both opened AND closed inside the interval reports both. Rows with an unbounded window on a side cannot cross that side, so a NULL/NULL row (every row predating schema step 22) never appears.
The view’s EXISTENCE flags still apply (so by default only current, active rows are considered), but its validity predicate does NOT: the question is about boundary crossings, not about being valid right now.
When the view relaxes validity entirely (include_out_of_window), the
interval is unbounded above.
This is world-time only. There is deliberately no transaction-time
(history_as_of) counterpart.
pub fn close(&self) -> Result<(), EngineError>
Sourcepub fn drain(&self, timeout_ms: u64) -> Result<(), EngineError>
pub fn drain(&self, timeout_ms: u64) -> Result<(), EngineError>
Block until in-flight writes drain or timeout_ms elapses.
Surface owned by dev/interfaces/rust.md § Engine-attached
instrumentation; semantics are owned by dev/design/lifecycle.md.
Sourcepub fn counters(&self) -> CounterSnapshot
pub fn counters(&self) -> CounterSnapshot
Snapshot of engine-internal counters.
Field set owned by dev/design/lifecycle.md.
Sourcepub fn set_profiling(&self, enabled: bool) -> Result<(), EngineError>
pub fn set_profiling(&self, enabled: bool) -> Result<(), EngineError>
Toggle response-cycle profiling.
Per dev/design/lifecycle.md § Per-statement profiling, profiling
is an opt-in surface that is independently toggleable on a running
engine without restart. AC-005a locks runtime toggleability.
Sourcepub fn set_slow_threshold_ms(&self, value: u64) -> Result<(), EngineError>
pub fn set_slow_threshold_ms(&self, value: u64) -> Result<(), EngineError>
Set the threshold above which an operation is reported as slow.
Per dev/design/lifecycle.md § Slow and heartbeat policy, the
threshold is runtime-configurable; mutating it changes detection
behavior on subsequent statements without restart (AC-007b).
Sourcepub fn subscribe(&self, subscriber: Arc<dyn Subscriber>) -> Subscription
pub fn subscribe(&self, subscriber: Arc<dyn Subscriber>) -> Subscription
Attach a host subscriber to engine events.
Dropping the returned Subscription detaches the subscriber.
Payload shape owned by dev/design/lifecycle.md and
dev/design/migrations.md.
Sourcepub fn embed_text(&self, text: &str) -> Result<Vec<f32>, EngineError>
pub fn embed_text(&self, text: &str) -> Result<Vec<f32>, EngineError>
Embed arbitrary text with the engine’s configured runtime embedder, returning the raw (un-centered) vector.
This is the read-path embed primitive: it mirrors the search
query-embedding path — a single, direct Embedder::embed call. The
per-embed() watchdog/circuit-breaker guards only the bulk
projection/write path (many embeds, fault isolation), not single
read-side embeds, so a direct call is consistent with how a query is
embedded. Callers get vectors under the engine’s pinned embedder
identity (fathomdb-bge-small-en-v1.5 by default) rather than a
parallel, possibly-divergent embedder.
Returns EngineError::EmbedderNotConfigured if the engine was opened
without an embedder (use_default_embedder = false).
Sourcepub fn drain_embedder_events(&self) -> Result<Vec<EmbedderEvent>, EngineError>
pub fn drain_embedder_events(&self) -> Result<Vec<EmbedderEvent>, EngineError>
0.7.2 PR-2b — NON-test observation seam. Drains and returns every
EmbedderEvent queued since the last drain (mean pin, manual mean
recompute). Production callers use
this to observe the synchronous recompute work; events are queued
only AFTER the recompute transaction is durable, so a rolled-back
recompute never surfaces. Mirrors the at-open
OpenReport.embedder_events channel for the steady-state path.
Sourcepub fn write_node_importance(
&self,
write_cursor: u64,
importance: f64,
) -> Result<(), EngineError>
pub fn write_node_importance( &self, write_cursor: u64, importance: f64, ) -> Result<(), EngineError>
0.8.16 Slice 5 / F9 (R-F9-1) — set the caller-supplied importance ranking
scalar on the canonical_nodes row identified by write_cursor (the
interim id SearchHit.id carries). Validates importance ∈ [0.0, 1.0],
mirroring the existing canonical_edges.confidence write-path check —
an out-of-range value is a deterministic EngineError::WriteValidation.
The 3-way sentinel: NOT calling this leaves the column NULL (never
assigned = graceful-absent, ranks NEUTRAL); 0.0 is the explicit floor;
(0.0, 1.0] is an explicit importance. Importance is a caller-supplied
scalar — the engine does NOT compute graph-centrality importance (ADR §4
non-goal). Engine-internal minimal surface for this keystone; SDK (Py/TS)
exposure is a Slice-40 concern.
Sourcepub fn node_importance(
&self,
write_cursor: u64,
) -> Result<Option<f64>, EngineError>
pub fn node_importance( &self, write_cursor: u64, ) -> Result<Option<f64>, EngineError>
0.8.16 Slice 5 / F9 (R-F9-1) — read back the importance scalar for the
canonical_nodes row identified by write_cursor. None = SQL NULL =
never assigned (graceful-absent). The reciprocal read for
Engine::write_node_importance.
Sourcepub fn transition(
&self,
logical_id: &str,
to_state: LifecycleState,
reason: Option<String>,
) -> Result<(), EngineError>
pub fn transition( &self, logical_id: &str, to_state: LifecycleState, reason: Option<String>, ) -> Result<(), EngineError>
OPP-12 Phase-1 (0.8.19 Slice 10, R-TR-1/2) — move a governed node between
existence states per the engine-enforced legal-transition table (design
§2): promote pending→active, reject pending→deleted, soft-delete
active→deleted, undelete deleted→active. to_state is a full
LifecycleState, but Pending (create-time only) and Purged
(purge-only) are never legal transition targets, nor are self-loops or
any move from a non-existent/purged row — each returns a typed
EngineError::IllegalTransition enumerating the legal targets.
reason semantics (design §3 gap-6): promote/undelete CLEAR reason to
NULL (the row is admitted; no standing cause); reject/soft-delete SET
reason to the supplied value (NULL allowed but the delete-family
expects it). reason is advisory — the engine never interprets it.
Keys on the BARE logical_id (l: space only); a Content(h:) or
Passage(p:) id raises EngineError::NotLifecycleAddressable.
The state flip mutates the single active (superseded_at IS NULL) row; a
deleted row STAYS node-FTS / vector indexed (gap-5) — only the
state='active' default filter excludes those shadows, so an undelete
needs no re-projection there.
0.8.20 Slice 15d fix-2 [P2] — the row-owned ATTRIBUTE projection
(canonical_attributes / property_search_index) is the exception: it has
NO read-side lifecycle filter (the property-FTS5 table cannot carry one), so
it is maintained AT REST to track the backfill’s set
(projected ⟺ active ∧ non-superseded). Promote/undelete PROJECT the declared
attributes; soft-delete PURGES them; reject is a no-op.
Sourcepub fn configure_projections(
&self,
specs: &[ProjectionSpec],
drop: &[String],
) -> Result<ProjectionDelta, EngineError>
pub fn configure_projections( &self, specs: &[ProjectionSpec], drop: &[String], ) -> Result<ProjectionDelta, EngineError>
0.8.20 Slice 15d (R-20-PR / C-1) — the projection registry as a
DECLARATIVE, IDEMPOTENT apply. The engine is the SOLE projection authority
(Q3): it diffs the supplied specs against the durable registry and
backfills the difference in ONE transaction. Cheap projections
(filterable, searchable→FTS) are built same-transaction; rankable
and the searchable→vector sub-target are PERSISTED but deferred (F9 /
Slice 20) — declaring them never errors (graceful-absent, Q6a).
0.8.20 Slice 23 (R-20-SV) — SPEC VALIDATION. A spec that carries an
fts or vector sub-object WITHOUT ProjectionRole::Searchable is an
INVALID SPEC and is refused with EngineError::WriteValidation (HITL
2026-07-24; see [apply_projection_config] for the full rationale). A
rejected request is a TOTAL no-op. read_projections is unaffected — it
is a pure read — so a LEGACY row in that shape still reports verbatim but
can no longer be re-applied.
drop is EXPLICIT (C3, api-surface.md:27): omission of a live
projection from specs does NOT drop it; removal requires naming it in
drop. An incompatible/destructive change to a live projection that is
NOT in drop is refused with EngineError::ProjectionDestructive, the
destructive delta surfaced — never silent data loss. Re-applying an
unchanged spec diffs to a no-op (ProjectionDelta::unchanged).
Pair with Engine::read_projections to see current state before
applying.
Sourcepub fn read_projections(&self) -> Result<Vec<ProjectionSpec>, EngineError>
pub fn read_projections(&self) -> Result<Vec<ProjectionSpec>, EngineError>
0.8.20 Slice 15d (R-20-PR) — read the current projection registry (C5
introspection: read.projections). Returns every declared
ProjectionSpec sorted by name, so a caller can inspect current state
(and the destructive delta a change would cause) BEFORE applying. Pure
read; never mutates.
0.8.20 Slice 20 (R-20-DR) — this is ALSO the surface that populates the
engine-set ProjectionVector::dense_readiness READ METADATA. It is
derived here, on the way out (see [derive_dense_readiness]); the durable
registry stores no readiness. Only a spec that declares the
searchable→vector sub-object carries one — filterable and
searchable→FTS are same-transaction and have no readiness axis.
Sourcepub fn purge(&self, logical_id: &str) -> Result<(), EngineError>
pub fn purge(&self, logical_id: &str) -> Result<(), EngineError>
OPP-12 Phase-1 (0.8.19 Slice 10, R-PG-1/2) — irreversibly hard-erase a
governed node. A SEPARATE verb from Engine::transition (NOT on the
recovery_denylist). Precondition: DELETED-FIRST — legal only from
deleted (else a typed EngineError::IllegalTransition to purged);
IDEMPOTENT — purging an already-absent/already-purged id is a no-op
success. Keys on the bare logical_id (l: only); h:/p: →
EngineError::NotLifecycleAddressable.
In ONE transaction, physically erases every ROW-OWNED target for the node
(design §3 / gap-3): all canonical_nodes versions; its search_index,
search_index_edges, search_index_v2 FTS rows; its vector_default
(vec0) + _fathomdb_vector_rows vectors; its _fathomdb_projection_terminal
bookkeeping; and — CASCADE-REMOVE, no content-free stubs — every
canonical_edges row touching it (from_id/to_id) plus those edges’
projection shadows. The global/kind-level registries
_fathomdb_projection_state and _fathomdb_vector_kinds are NOT keyed to
a node id and are DELIBERATELY untouched.
Erasure completeness relies on the standing PRAGMA secure_delete=ON
(design §3 gap-4) which zeroes every freed page — so no per-purge VACUUM.
(Freelist content written on a pre-20 DB before secure_delete was on is a
documented residual; there is no forced migration-time VACUUM.)
Sourcepub fn erase_source(&self, source_id: &str) -> Result<ExciseReport, EngineError>
pub fn erase_source(&self, source_id: &str) -> Result<ExciseReport, EngineError>
0.8.20 Slice 5d (R-20-E4, design §4 item 9b) — the governed SDK
erasure verb. Deletes every canonical row attributable to source_id,
plus its row-owned projections, and finishes the erasure at rest.
This is NOT operator-gated: erasing content a consumer wrote is an
application obligation, not a recovery workflow. Before this slice the
only erasure path was [Engine::excise_source], which lives behind the
operator feature (i.e. the CLI), so an SDK-only consumer holding a
deletion obligation over ANONYMOUS content — content with no
logical_id, therefore not reachable by Engine::purge — had no way
to discharge it at all. That gap is what R-20-E4 closes.
One engine path. erase_source and excise_source are the SAME
operation: both delegate to Engine::erase_source_shared. They are
not competing implementations, and no behaviour is duplicated.
Validation differs, deliberately. erase_source admits only ids
SourceId::new would admit, so a caller cannot aim the governed verb
at the engine’s reserved _-prefixed namespace (_engine:* substrate,
or the _legacy:pre-0.8.20 cohort migration step 21 back-filled — a
single call against which would erase every pre-0.8.20 anonymous row).
excise_source stays permissive precisely BECAUSE it is the recovery
seam: R-20-E8 requires an operator to be able to excise _legacy:.
Not a recovery verb. erase_source carries no REQ-054
recovery-denylist name ({recover, restore, repair, fix, rebuild}); it
is a lifecycle verb alongside transition/purge. AC-041 is unaffected.
§Errors
EngineError::WriteValidation for an empty, whitespace-only or
reserved source_id; EngineError::ErasureIncomplete if the erasure
could not be completed at rest (see Engine::complete_erasure_at_rest).