Skip to main content

Engine

Struct Engine 

Source
pub struct Engine { /* private fields */ }
Expand description

Unified engine. Holds a list of mounted backends and routes mem-named operations to the right one.

Send so the engine can sit behind a Mutex (today’s pattern in the MCP server). The trait object’s Send + Sync bound on MemBackend keeps the inner backends thread-safe; the engine itself is single-threaded by design (the lazy memos are OnceCell, which is !Sync).

Debug is hand-written to avoid requiring Debug on the dyn MemBackend trait object — backend impls are free to stay non-Debug.

§Load-on-init

Engine::from_mounts walks each backend at construction time (list_entities + read_entity + parse) and populates a single shared Store with entities and edges from every mount. Each mount’s schema resolves from its own pin (the backend config’s schema, or the mount-record assertion as fallback) through the SchemaResolver, so schemas holds genuinely heterogeneous schemas in a multi-schema workspace. Per-file errors don’t fail construction; they collect into Engine::load_errors for the operator to inspect.

Implementations§

Source§

impl Engine

Source

pub fn apply_external_commit( &mut self, envelope: &CommitEnvelope, ) -> Result<(), EngineError>

Replay an externally-produced commit envelope into the in-memory store.

Refuses with EngineError::UnknownMem when the envelope names a mem the engine has no mount for. Parse failures on any change variant surface as EngineError::Parse and abort the apply before any store mutation lands — the post-state is either coherent at the envelope SHA or unchanged. (The store mutation loop below uses a staged scratch list precisely to preserve this all-or-nothing property; do not refactor it into per-change in-place mutations without restoring an equivalent guarantee.)

Empty changes is a valid envelope: the head cursor advances and a MemChangedEvent fires, but no store mutation happens. Lets replay drivers signal “we saw a commit, here is its SHA, no entity-level changes” — useful for empty commits (e.g. tag-only or merge commits without tree changes).

Source§

impl Engine

Source

pub fn from_archive_bytes(bytes: Vec<u8>) -> Result<Self, FromArchiveBytesError>

Hydrate an engine from sealed archive bytes (.mem).

Validates the bytes through the archive ingress validator (extract_entries), reads the embedded .memstead/config.json for mem name + schema pin, loads any embedded schema package (.memstead/schema/) into the engine’s schema catalogue, and constructs a single-mount read-only engine backed by the bytes. No temp file, no on-disk artifact — the bytes are the storage.

The resulting engine refuses mutations (memstead_create, memstead_update, memstead_delete, memstead_relate, memstead_rename) via the existing read-only-mount / sealed-backend envelope. Read operations work for the embedded mem.

Source

pub fn from_archive_bytes_with_limits( bytes: Vec<u8>, limits: &ValidatorLimits, ) -> Result<Self, FromArchiveBytesError>

Variant of Self::from_archive_bytes with caller-supplied limits. Bridge / registry deployments tune the caps; the default ladder (ValidatorLimits::DEFAULT) is what from_archive_bytes picks.

Source

pub fn export_mem_to_bytes( &self, mem_name: &str, ) -> Result<Vec<u8>, EngineError>

Export the named mem’s current state as .mem archive bytes.

Symmetric to Self::from_archive_bytes: a mem name in, a self-contained byte buffer out. The bytes validate against extract_entries standalone — any consumer of sealed archives accepts them. Feeding the bytes back into Engine::from_archive_bytes yields an engine that returns identical reads against the exported mem.

Returns EngineError::UnknownMem when the name resolves to no mount; EngineError::Backend wrapping crate::backend::BackendError::Sealed when the mem is archive-mounted (already-an-archive, no meaningful re-export); EngineError::InvalidInput when the mem has no loaded MemConfig; EngineError::MemConfigIncomplete when the loaded config is missing version. The git-branch byte-export path lifts in a follow-up; today it surfaces as EngineError::Backend wrapping the unmounted-hook message.

Source§

impl Engine

Source

pub fn from_mounts( mounts: Vec<(Mount, Box<dyn MemBackend>)>, ) -> Result<Self, EngineError>

Build an engine from (mount, backend) pairs. The backend is the implementor that will serve reads / writes for that mount’s mem.

Returns EngineError::DuplicateMem when two mounts name the same mem; that’s a configuration error the caller must fix before the engine can route deterministically. An empty mount list is allowed (returns an engine that errors UnknownMem on every read) — useful for tests; production callers will reject empty inputs at the persistence-adapter layer.

Source

pub fn from_mounts_with_schemas_dir( mounts: Vec<(Mount, Box<dyn MemBackend>)>, schemas_dir: Option<&Path>, ) -> Result<Self, EngineError>

Construct an engine from mounts plus an optional workspace schemas directory. Loads every subdirectory of schemas_dir as a workspace-authored schema and combines with the builtin catalogue for per-mem schema-pin resolution. Workspace schemas take precedence on (name, version) collision — matches full’s behaviour.

schemas_dir = None is equivalent to Self::from_mounts. Used by engine_from_workspace_root to thread the [schemas_dir] workspace-toml entry into schema resolution.

Source

pub fn from_mounts_with_schemas_dir_and_extra( mounts: Vec<(Mount, Box<dyn MemBackend>)>, schemas_dir: Option<&Path>, extra: Vec<Arc<Schema>>, ) -> Result<Self, EngineError>

Like Self::from_mounts_with_schemas_dir but layers additional, pre-loaded local-storage schemas (e.g. those a git-branch backend reads from its __MEMSTEAD:schemas/ ref via SchemaSource) on top of the folder schemas_dir set. Both are local-storage schemas — they override built-ins on (name, version) collision. The git-branch boot path uses this to make ref-installed schemas resolvable, which from_mounts_with_schemas_dir (folder only) does not.

Source

pub fn from_workspace_root(workspace_root: &Path) -> Result<Self, BootError>

Boot an engine from a workspace root using only lean-flavour backends (folder + archive). The MCP filesystem server, the CLI’s lean dispatcher, and the macOS UniFFI consumer all reach the new engine through this entry point — replacing per-flavour init code with one call.

Loads the workspace through crate::FileWorkspaceStore, instantiates each mount’s backend via crate::instantiate_lean_backend, and constructs the engine via Engine::from_mounts.

Errors:

Source§

impl Engine

Source

pub fn record_check( &mut self, mem_name: &str, entity_id: &str, verdict: Verdict, method: Option<&str>, actor: Actor, client: Option<&ClientId>, ) -> Result<CheckRecord, EngineError>

Record a check of one entity. Refuses typed on unknown mem (quarantine included), unknown entity, read-only mounts, and on any persistence failure (CHECK_NOT_RECORDED) — recording is never best-effort, because a caller who believes an unrecorded check landed is the exact dishonesty this tier removes. The declared role rides engine session state (Engine::set_role), same as every mutation.

Source

pub fn entity_check_state( &self, mem_name: &str, entity_id: &str, ) -> Result<(CheckState, Option<CheckRecord>), EngineError>

Derive one entity’s check state and newest check record. Refuses typed on unknown mem/entity; an engine with no workspace root has no check store and honestly derives never_checked (no recorded checks exist).

Source§

impl Engine

Source

pub fn reload_if_stale(&mut self, mem: Option<&str>) -> Vec<WarningHint>

Reload-before-operation: before any read or write executes, check the mem ref; if it advanced past the engine’s cached last_known_head, reload the affected mem(s) and return one [WarningHint::MemReloaded] per reload so the caller can surface the drift to the agent (the response itself already carries fresh content — the warning explains why state shifted).

The ref check runs on every call — there is no throttle window. A per-operation current_head() read is microseconds, effectively free at LLM latencies, and a throttle that let an operation execute against an already-moved ref would reintroduce the exact silent-staleness this guards against. This is the correctness floor: no operation acts on a projection that is behind git truth.

mem = Some(name) scopes the probe to one mount; None scans every mount. Read handlers that target a known mem (memstead_entity derives the mem from the id; memstead_changes_since takes it as a param) and every mutation (which knows its target mem) pass a name; tools that scan multi-mem (memstead_search without a mem filter, memstead_overview, memstead_health) pass None.

Behaviour matrix per mount:

  • cached Some(old) + on-disk Some(new), old != new → reload the mem, emit MemReloaded, refresh the cached head to new.
  • cached None + on-disk Some(new) → silently capture the first observed head as the baseline (no warning — there’s no prior in-memory snapshot to be stale against).
  • cached / on-disk match, on-disk None (folder, archive, refdb hiccup), or current_head errors → no-op.

Reload errors are warn-logged and the affected mem is skipped — the caller’s response is still served from the (now stale) in-memory snapshot rather than failing the entire request. The next operation retries.

Cache invalidation rides on reload_one_mem — community and search-index memos drop when any mem reloads.

Source

pub fn take_mem_changed_notices(&mut self) -> Vec<MemChangedNotice>

Drain the reload-before-operation notices accumulated since the last drain. The response layer calls this after an operation completes to attach the structured mem_changed notice. Every handler that can trigger a reload (directly via Self::reload_if_stale or indirectly through a mutation) must drain, or an undrained notice leaks into the next operation’s response.

Source

pub fn mem_changed_notice( &self, mem: &str, from_head: &str, to_head: &str, ) -> MemChangedNotice

Build a crate::ops::MemChangedNotice describing the per-entity delta a reload applied to mem (from from_head to to_head). Derived from Self::changes_since so it carries rename detection on git-branch mounts; on any backend error (e.g. an unresolvable cursor) it falls back to an empty delta — the heads alone still tell the agent the mem moved.

Callers pair this with Self::reload_if_stale: a returned crate::ops::WarningHint::MemReloaded carries the old_head / new_head to pass here. The delta matches the transition the reload applied (changes_since walks the same from_head → current range).

Source

pub fn changes_since( &self, mem: &str, since: &str, rename_similarity: Option<f32>, ) -> Result<ChangesReport, EngineError>

Per-entity events for mem between since and the backend’s current state.

  1. Resolves the mount (returns EngineError::UnknownMem on unknown mem).
  2. Validates rename_similarity against [RENAME_SIMILARITY_MIN, RENAME_SIMILARITY_MAX]. Out-of-range values refuse with EngineError::InvalidInput carrying details.allowed_range and details.requested. None falls back to crate::ops::RENAME_SIMILARITY_DEFAULT.
  3. Dispatches on the mount’s MountStorage:
    • Folder mounts synthesize from the JSONL changelog via crate::ops::folder_changes_since.
    • Git-branch mounts call the registered [GitBranchOps::changes_since] hook (real tree-diff with rename detection); missing hook = full flavour not loaded and the report comes back empty.
    • Archive mounts return an empty report.
  4. Enriches each envelope’s title / entity_type from the in-memory store (best-effort — Removed envelopes always leave both None; missing-from-store entities also leave them None).
  5. Returns crate::ops::ChangesReport with mem, since (echoed), head (backend-resolved current cursor), enriched changes, and any clamping warnings.
Source

pub fn fetch( &self, mem: &str, remote: &str, refspecs: &[String], ) -> Result<FetchOutcome, EngineError>

Fetch updates from remote into the workspace’s mem-repo. Advances remote-tracking refs only; the local branch pointer is not moved.

refspecs is forwarded verbatim to git fetch. An empty list uses the remote’s configured defaults.

Refusal codes: UNKNOWN_MEM, UNKNOWN_REMOTE, INVALID_INPUT (folder / archive mounts).

V1 atomicity: schema-validation quarantine for fetched commits is not yet wired. The remote-tracking refs advance unconditionally on a successful fetch; downstream schema validation runs on read via the engine’s existing reload pipeline.

Source

pub fn pull( &mut self, mem: &str, remote: &str, ) -> Result<PullOutcome, EngineError>

Pull updates from remote into the named mem’s branch. Fetches into the remote-tracking ref, runs a pre-merge schema validation pass against the prospective state, then fast-forwards the local branch. Refuses with LOCAL_DIVERGENCE for diverged local branches and with SCHEMA_VIOLATION_IN_FETCH when the prospective state fails schema validation — in both refusal cases the local branch pointer is untouched (the underlying fetch has updated refs/remotes/* but the engine has not promoted the new state).

Source

pub fn push( &self, mem: &str, remote: &str, force: bool, ) -> Result<PushOutcome, EngineError>

Push the named mem’s branch to remote. Runs a pre-push schema validation pass against the local branch tree; refuses with LOCAL_INVALID_STATE when the local state fails schema validation (the remote is not contacted in that case). Refuses with NON_FAST_FORWARD when the push is not a fast-forward and force: false; with force: true runs a --force-with-lease push instead.

Source

pub fn remote_add( &self, name: &str, url: &str, ) -> Result<RemoteAddOutcome, EngineError>

Configure (or re-point) a named remote on the workspace’s mem-repo, so fetch / pull / push have somewhere to go. Upsert semantics — safe to re-run with a new URL. The mem-repo is shared by every git-branch mount, so the op is workspace-level: any git-branch mount locates it; refuses INVALID_INPUT when the workspace has none.

Source

pub fn branch_reset( &mut self, mem: &str, target_sha: &str, expected_head: Option<&str>, ) -> Result<BranchResetOutcome, EngineError>

Reset a mem’s branch pointer to target_sha. The only engine surface that moves a branch pointer over existing commits — every other mutation appends. Refuses if any commit that would be discarded by the reset is already reachable from a refs/remotes/* ref (the engine’s definition of “pushed”).

target_sha accepts anything gix::rev_parse_single admits: a SHA, an abbreviated SHA, a branch name, a tag. The branch itself (refs/heads/<mem>) must exist.

Refusal codes:

Emits a crate::engine::events::MemChangedEvent on success when the SHA actually changed; the reset’s effect is observable through the same change-event surface every commit flows through. Engine’s cached last_known_head for the affected mount is rewound to the new SHA so the next drift probe doesn’t flag the reset as a sibling-writer surprise.

Source

pub fn branch_reset_stranded_refs( &self, mem: &str, target_sha: &str, ) -> Result<Vec<StrandedCrossMemRef>, EngineError>

Cross-mem references that a reset of mem to target_sha would strand: incoming edges from entities in other mems whose target exists at the current head but would not exist at the target commit — entities created after the target, or renamed to their current id after it (the reset re-materialises the old id, so references to the new id dangle either way).

A read — computes against the live store and the commit history, moves nothing. The human surface calls this fresh at confirmation-dialog time and warns before branch_reset. Sorted (from_id, to_id, rel_type) for stable rendering.

Refusals mirror changes_since: UnknownMem, InvalidCursor for an unresolvable target_sha, InvalidInput for non-git-backed mounts.

Source

pub fn diff( &self, mem: &str, ref_a: &str, ref_b: &str, config: Option<DiffConfig>, ) -> Result<Diff, EngineError>

Two-ref structural diff. Produces a per-entity crate::ops::Diff comparing the trees at ref_a and ref_b for the named mem’s storage. Folder and archive backends carry no git refs and refuse via EngineError::InvalidInput; the git-branch backend routes through [GitBranchOps::diff] when the full flavour is loaded.

mem selects the storage context (the gitdir, for git-branch mounts). ref_a / ref_b are arbitrary refs the underlying git layer accepts — branch names, commit SHAs, tag names — so cross-mem diffs work via fully-qualified refs (refs/heads/<other-mem>) without a separate API.

Refusal codes:

Source§

impl Engine

Source

pub fn render_due_brief( &self, today: &str, window: &DueWindow, mem_filter: Option<&str>, ) -> Result<String, String>

Render the due-brief: every open entity whose declared due date falls inside (-∞, today + window], across every mem whose pinned schema declares the axis (writable and read-only mounts alike), optionally filtered to one mem. today is an ISO YYYY-MM-DD string — the caller takes it once per invocation, tests inject it. A workspace with no declaring schema renders an honest empty brief, not an error.

Source§

impl Engine

Source

pub fn subscribe_mem_changes( &self, mem: &str, callback: EventCallback, ) -> Result<SubscriptionHandle, EngineError>

Subscribe to commit events on mem. The returned SubscriptionHandle keeps the registration alive; dropping it (or calling unsubscribe()) removes the callback.

callback runs on the engine’s mutation thread synchronously — by design, per the Core’s runtime-agnostic contract. Consumers that cannot block the writer must decouple inside the callback (channel send, dedicated thread, async runtime queue). The opt-in tokio feature lifts this into a broadcast::Receiver for tokio-resident consumers; the file-watcher feature provides a cross-process variant for readers without a writer engine.

Read-only mounts (archive or ReadOnly capability) accept the subscription but never emit — no mutations land in those mems through this engine. Unknown mems refuse with crate::EngineError::UnknownMem; the typed code is UNKNOWN_MEM.

Source§

impl Engine

Source

pub fn subscribe_mem_changes_broadcast( &self, mem: &str, ) -> Result<(SubscriptionHandle, Receiver<MemChangedEvent>), EngineError>

Tokio-broadcast convenience over the callback subscribe API. Returns a (SubscriptionHandle, broadcast::Receiver) pair: the handle keeps the registration alive (drop to unsubscribe); the receiver yields MemChangedEvents on every commit.

Backpressure follows tokio::sync::broadcast semantics: a subscriber that falls behind by more than the channel capacity (DEFAULT_BROADCAST_CAPACITY) sees RecvError::Lagged(n) on its next recv() and the channel resumes from there. Slow subscribers do not block the writer — that is the whole point of the tokio convenience over the raw callback path, where a slow callback blocks the mutation thread by design.

Use Self::subscribe_mem_changes_broadcast_with_capacity when the default capacity is too small (high-burst write loops) or too large (memory-constrained deployments).

Source

pub fn subscribe_mem_changes_broadcast_with_capacity( &self, mem: &str, capacity: usize, ) -> Result<(SubscriptionHandle, Receiver<MemChangedEvent>), EngineError>

Caller-tunable variant of Self::subscribe_mem_changes_broadcast. capacity is the tokio-broadcast channel buffer size; values below 1 panic per the tokio contract.

Source§

impl Engine

Source

pub fn render_html_export( &self, mem: &str, export_date: &str, ) -> Result<String, EngineError>

Render one mem as a single self-contained HTML document. export_date is an ISO date stamped once in the identity block — the only environmental input besides the store.

Source§

impl Engine

Source

pub fn entity_provenance( &self, mem: &str, entity_id: &str, ) -> Result<EntityProvenance, EngineError>

Derive an entity’s provenance block (agent-trust plan 13): created-by (the oldest recorded touch, only when it IS the creation) and last-modified-by (the newest touch), each with actor identity, client, declared role, and timestamp — read from the same append-only record entity_history serves, so no verb can alter it after the fact. Pages through the full story to reach the creation record when histories exceed one page.

Source

pub fn entity_history( &self, mem: &str, entity_id: &str, page_size: Option<usize>, cursor: Option<&str>, ) -> Result<EntityHistoryReport, EngineError>

An entity’s recorded history: every touch, newest-first, with rename chains followed so the story starts at the entity’s first appearance under any prior id. Bounded and pageable — page_size clamps to HISTORY_PAGE_MAX, cursor continues a prior page (INVALID_CURSOR when it matches no touch).

Refusals: UNKNOWN_MEM, ENTITY_NOT_FOUND (an unknown id never yields an empty story), INVALID_INPUT on archive mounts (their seam records no history — refusing beats fabricating emptiness).

Source§

impl Engine

Source

pub fn set_settings(&mut self, settings: WorkspaceSettings)

Replace the workspace-level settings. Called by Self::from_workspace_root (and the full counterpart) after reading .memstead/workspace.toml. Tests / direct callers leave the default empty value in place. Cheap clone — settings carry only data shapes (raw rule lists, link policy map), no compiled matchers. Invalidates the lazy create_rule_set_memo so the next synthesis call rebuilds from the new policy.

Source

pub fn set_backend_factory(&mut self, factory: BackendFactory)

Replace the backend factory. Full consumers call this once at boot (engine_from_workspace_root) to install memstead_git_branch::storage::instantiate_full_backend so the engine can materialise git-branch backends on top of folder + archive. Lean consumers leave the default in place.

Source

pub fn set_mutation_clock(&mut self, clock: MutationClock)

Replace the mutation-timestamp clock — the source every engine-stamped metadata field (init_timestamp / auto_timestamp schema flags: created_date, last_modified) reads. A testing affordance for suites that assert over canonical entity bytes (e.g. cross-surface hash parity): pin both engines to the same constant and byte-level nondeterminism from wall-clock seconds disappears. Production code never calls this — the default installed at construction is the system clock, and the stamped format is unchanged either way.

Source

pub fn set_role(&mut self, role: Role)

Set the caller-declared role for subsequent mutations (agent-trust plan 13). The surface calls this before every mutation with the per-call parameter resolved against its session default (per-call wins); Role::Unspecified records as absence.

Source

pub fn current_role(&self) -> Role

The currently declared role — what the next mutation records.

Source

pub fn set_git_branch_ops(&mut self, ops: GitBranchOps)

Install the git-branch ops bundle. Full boot (memstead_git_branch::engine_from_workspace_root) calls this once at construction. Lean consumers leave it unset and the git-branch dispatch branches collapse to typed errors / empty reports — lean has no git-branch mounts.

Source

pub fn install_schema( &self, name: &str, version: &str, files: &[(String, Vec<u8>)], ) -> Result<String, EngineError>

Install a schema package onto the workspace’s git-branch backend — the unified __MEMSTEAD:schemas/<name>@<version>/ ref. files are (relative-path, bytes) pairs (schema.yaml, types/<t>.yaml, optional mem-template.json). Returns the resulting commit sha; idempotent at the storage layer (an identical re-install produces no new commit).

Folder workspaces install schemas by writing under <workspace>/.memstead/schemas/ directly; this is the git-branch path, where the engine owns the mem-repo and the write must route through it. Errors when no git-branch ops are wired (lean flavour) or no git-branch mount exists to resolve the shared mem-repo gitdir from. The caller reloads (or restarts) to pick the new schema into the resolution catalogue.

Source

pub fn stage_sealed_schema( &mut self, mem: &str, pin: &SchemaRef, files: &[(String, Vec<u8>)], ) -> Result<SchemaStaging, EngineError>

Make a sealed third-party schema package resolvable in this workspace — the install-time half of “a published mem installs on the strength of the schema it carries”.

The package (package-relative files: schema.yaml, types/<t>.yaml, schema-format.json) is written into the workspace’s local schema storage, the same storage Self::install_schema writes to and the pin resolver reads — so the mount that follows resolves without a fourth mechanism and a second mem pinning the same schema finds it already there. The loaded schema also lands in this engine’s live catalogue, so the caller mounts in the same process without a reload.

Two things it deliberately does NOT do. It does not run the authoring gate (Self::validate_schema_package): these are a third party’s sealed bytes, the installing user cannot fix them, and the archive validator has already admitted them under the sealed reading — applying the authoring gate here would make an archive that is valid to publish invalid to install. And it does not append the format marker: presence of the marker IS the package’s metadata-polarity generation, so injecting one would rewrite the meaning of bytes the publisher sealed.

Idempotent: a pin the engine can already resolve returns SchemaStaging::AlreadyResolvable with nothing written.

Source

pub fn validate_schema_package( name: &str, version: &str, files: &[(String, Vec<u8>)], ) -> Result<(), EngineError>

Validate a schema package’s files before they are sealed. Runs the full loader (structural + semantic) plus the section-heading round-trip gate, and checks the manifest’s declared identity matches the (name, version) the package is being installed under — a mismatch would seal the schema under a ref its own manifest contradicts.

pub so the below-boot install path (memstead-git-branch’s repair surface) runs the SAME gate as this booted path — the two must never fork into separate validation regimes.

Source

pub fn validate_schema_exemplars(schema: &Arc<Schema>) -> Result<(), String>

Validate every type exemplar a schema carries by running it through the REAL create validation stage (agent-trust plan 09): an in-memory engine is booted with the candidate schema pinned on a virtual mem, and each exemplar is submitted as a dry_run create — the same gates a real write runs (sections, metadata + enums, rel-type vocabulary, edge shape, description posture), commit-free by construction. Placeholder relation targets are bare slugs scoped to the virtual mem, so target existence is never checked (an absent target is the legal would-be-stub path).

Returns the defect as a message naming the type — the caller wraps it in its own typed envelope (SchemaPackageInvalid on the install path). There is deliberately no warn-and-carry mode: a non-conformant exemplar refuses, because the whole value of an exemplar is the impossibility of drift.

pub so the built-in suite gates every shipped exemplar through the SAME validator (a broken built-in exemplar fails CI), and the below-boot install path shares the gate via Self::validate_schema_package.

Source

pub fn unregister_writable_mem( &mut self, mem_name: &str, ) -> Result<Option<Box<dyn MemBackend>>, EngineError>

Unregister a writable mem at runtime. Engine-level primitive that memstead_mem_delete builds on.

Removes the named mount from Self::mounts, drops the mem’s entities from the store, refreshes the [MemRouterSnapshot] via Arc::make_mut (COW swap so readers holding a pre-swap snapshot see the pre-state for their lifetime), and invalidates the community + search memos. Does NOT touch the backend’s on-disk state — the caller (delete_mem orchestrator) decides whether to remove the directory / gitdir after this returns.

Returns Ok(Some(backend)) when the mem was present and unregistered — the caller can drive any backend-specific follow-up cleanup (backend.delete_artifacts() for the mem-repo branch + __MEMSTEAD config when delete_files=true). Returns Ok(None) when no mount named the mem (idempotent — repeated calls are safe).

Source

pub fn register_read_mount( &mut self, mount: Mount, backend: Box<dyn MemBackend>, origin: MemOrigin, ) -> Result<(), EngineError>

Register a read-only mount at runtime — the install path’s engine primitive. Same registration pipeline as Self::register_writable_mem (collision probe, config read, schema resolution, entity load, router swap); the router branch lands the mount in the read-only slot for capability: ReadOnly + Archive storage, so is_writable stays false and archive_path_for_mem resolves.

Source

pub fn unregister_read_mount( &mut self, mem_name: &str, ) -> Result<Option<Box<dyn MemBackend>>, EngineError>

Unregister a read-only mount at runtime — the uninstall path’s engine primitive, mirroring Self::unregister_writable_mem for the read-only slot. Returns Ok(None) when the name is not a registered read-only mount (writable mems are the delete/unregister verbs’ business, deliberately not this one’s). Registration removal only — the backing archive file (global cache) is never touched.

Source

pub fn push_load_warning(&mut self, warning: WarningHint)

Append a typed load-time warning from outside the engine’s own load pipeline — the boot orchestrators (which live in the full crate) use this to surface one-time migrations they perform around engine construction.

Source

pub fn register_writable_mem( &mut self, mount: Mount, backend: Box<dyn MemBackend>, origin: MemOrigin, ) -> Result<(), EngineError>

Register a writable mem at runtime. Engine-level primitive that memstead_mem_create builds on.

Steps:

  1. Name collision probe against the current mem_router snapshot. Writable AND read-only entries collide; the error surfaces the colliding source so the orchestrator can render a recovery hint.
  2. Schema resolution via the built-in catalogue (mirrors Self::from_mounts; workspace-authored schema resolution lifts later).
  3. Per-mem config load (folder backends only; git-branch / archive return None — same contract as Self::from_mounts).
  4. Entity load via the backend, parse, push into the engine’s store with a LoadCollector so drift warnings forward to self.load_warnings.
  5. Insert schema into Self::schemas.
  6. Push the [MountedBackend] into Self::mounts.
  7. COW snapshot swap on Self::mem_router via Arc::make_mut + add_writable(name, dir, origin, mem_path). Folder mounts surface their on-disk path; other backends register with dir: None (matches full’s contract). mem_path carries the create-time organisational path component (mirrors MemCreateParams.path) — the delete-side lifecycle composer reads it back to rebuild the <mem_path>/<name> candidate the create-side composer matched against. Caller threads None for flat-layout registrations and Some(p) for hierarchical ones.
  8. Invalidate community + search memos.

Returns Err(EngineError::MemNameCollision) when the name is already registered. Other failures (schema-not-found, backend read errors) propagate as their typed variants. On failure no engine mutation happens: every potentially- mutating step runs only after the collision probe succeeds, and intermediate failures propagate before the mount / router are touched.

Source

pub fn full_refresh(&mut self) -> FullRefreshReport

Additive full refresh — the warm-server half of “restart the process”: re-scan the schema sources and the mount manifest, making newly installed schema versions resolvable and newly registered mems usable, WITHOUT applying removals. The asymmetry is deliberate and is the whole safety argument: adding extends what the in-memory store can answer, while removing can strand entities, in-flight handles, and cached hashes the process is still serving. Removals are skipped and reported; a restart applies them.

Failure model is per-item: a schema source or a mount that fails to refresh lands in failures and never surfaces as newly available; the others proceed. Each mount registration is all-or-nothing (every fallible step runs before the store is touched), so a failed item leaves no half-updated state. The workspace-global passes (relation validation, alias remap, memo invalidation) run ONCE per refresh regardless of how many mounts attached.

A newly mounted mem starts cold and loads like any other mount. Content reload of pre-existing mems is NOT part of this method — callers that want both (the memstead_reload full=true surface) run the existing content-reload sweep alongside.

Source

pub fn set_workspace_root(&mut self, root: PathBuf)

Override the workspace root after construction. The full boot helper memstead_git_branch::engine_from_workspace_root calls this so the engine knows the path even when the boot route runs through the full adapter rather than Self::from_workspace_root.

Source

pub fn persist_state(&self) -> Result<(), EngineError>

Persist the engine’s current mount list to the workspace store so a freshly-booted sibling process observes the same mem membership. Called by [crate::mem_management::create_mem] / [crate::mem_management::delete_mem] after the in-memory router mutation lands — without this, the per-mem content (branch + __MEMSTEAD config blob, or folder + .memstead/config.json) is already on disk, but the next process boot reads an empty .memstead/state/mounts.json and the engine starts with zero writable mems.

No-op when workspace_root is unset (tests / ad-hoc consumers that build the engine directly from a mount list). Production boot paths (Engine::from_workspace_root and the full counterpart) always set the root, so the engine-side fix covers every caller — including the future UniFFI binding — by construction.

Hardcoded against crate::FileWorkspaceStore because that is the only V1 adapter; a future SQLite or remote adapter would install through a setter mirroring Self::set_backend_factory.

Source

pub fn set_mem_schema( &mut self, mem: &str, target: &SchemaRef, ) -> Result<SetSchemaOutcome, EngineError>

Set a mem’s schema pin — the conformance-gated schema-migration trigger. Behaviour per the pinned contract:

  • requested == current pin → Noop, no state change.
  • requested != pin, mem integral against the target → atomic switch (schema_pin = target, migration state cleared) in one workspace-store write → Switched.
  • requested != pin, mem NOT integral → enter (or stay in) dual-pin: migration_target = target, writes validate against the target from this call on, findings carries the non-integral entities → MigrationStarted (first call) / MigrationPending (same target re-issued).
  • re-issued with the in-flight target once every entity is integral → atomic switch → Switched.

The trigger is a label change gated by the conformance check — no content hashing. The response hands the agent findings and nothing else (no migration scripts, no hints); each repair write is validated strictly against the target.

Source

pub fn export_markdown( &self, mem_filter: Option<&str>, schema_filter: Option<&str>, ) -> Result<ExportResult, EngineError>

Regenerate entity markdown files from the in-memory store.

Dispatch:

  • When mem_filter is Some(name), only that mem’s mount is considered. If its active backend doesn’t support markdown regeneration in place (today: anything other than MountStorage::Folder), the call refuses with EngineError::MarkdownExportUnsupportedBackend carrying the active backend’s id and the supported-backend list.
  • When mem_filter is None, every mount is iterated. Folder mounts regenerate as today; non-folder mounts are recorded in crate::ops::ExportResult::skipped_mounts so the caller can surface the partial-success shape.

Per-folder-mount behaviour: iterate the store, regenerate each non-stub entity belonging to the mount’s mem, compare to the on-disk file, write if changed.

schema_filter narrows the per-entity-type subset: when Some(name), only entities whose entity_type matches are regenerated. None exports every type.

Pre-fix this returned ExportResult { written: 0, unchanged: 0 } for git-branch / archive mounts — a successful-looking no-op that masked the backend-incompatibility. The typed refusal (per-mem) and the skipped_mounts channel (workspace-wide) give the caller an agent-actionable signal in one round-trip.

Source

pub fn export_mem( &self, mem_name: &str, output_path: &Path, ) -> Result<MemExportResult, EngineError>

Export a mem as a portable .mem archive.

Dispatch is internal: the engine looks up the mount whose mem name matches and branches on its MountStorage. Folder mounts produce a snapshot archive (current .md files + config); git-branch mounts invoke the registered GitBranchOps::export hook to produce a history archive (the per-mem branch tip’s tree); archive mounts reject with BackendError::Sealed (already-an-archive — no meaningful re-export).

The mem’s MemConfig is looked up via Self::mem_config_for; unloaded configs (folder mounts without a .memstead/config.json, git-branch mounts without a __MEMSTEAD:mems/<mem>/config.json) surface as EngineError::InvalidInput. Workspace-level schema dir is threaded from self.settings.schemas_dir for the schema-source resolution chain.

Source

pub fn record_pipeline_edit_provenance( &self, mem: &str, kind: &str, edits: &[(String, Option<Vec<u8>>)], note: Option<&str>, verb: &str, ) -> Result<(), BackendError>

Update a mem’s version field in its per-mem config and persist it through the backend. Backend-symmetric: folder backends rewrite .memstead/config.json; git-branch backends commit __MEMSTEAD:mems/<mem>/config.json. Archive mounts reject with BackendError::Sealed.

Returns the (mem, old_version, new_version) triple so callers can surface the change without an extra read. Reads the current value from the in-memory MemConfig and updates it on success, keeping the next call free of a stale-version read.

EngineError::UnknownMem when the name resolves to no mount; EngineError::ReadOnlyMount when the mount is sealed for writes; EngineError::InvalidInput when the mount has no loaded MemConfig (folder mount with no .memstead/config.json; the residual missing-config path is distinct from the missing-version path). F1. Record pipeline-edit provenance through mem’s backend — the bridge the pipeline-edit block (outside the engine module) uses to reach a mount’s backend. A mem that isn’t currently mounted is a successful no-op: pipeline configs may reference unmounted mems, and provenance is recorded against the mounted set.

Source

pub fn set_mem_version( &mut self, mem_name: &str, new_version: Version, note: Option<&str>, ) -> Result<SetMemVersionOutcome, EngineError>

Source

pub fn set_mem_description( &mut self, mem_name: &str, new_description: Option<String>, note: Option<&str>, ) -> Result<SetMemDescriptionOutcome, EngineError>

Update a mem’s description field in its per-mem config and persist it through the backend — the one-line text mem-archive export embeds and the registry card surfaces. None clears the field. Same backend symmetry, drift probe, and provenance-note posture as Self::set_mem_version; archive mounts reject with BackendError::Sealed.

Source

pub fn set_mem_title( &mut self, mem_name: &str, new_title: Option<String>, note: Option<&str>, ) -> Result<SetMemTitleOutcome, EngineError>

Update a mem’s display title — free text, NOT identity: the mem name stays the sole handle everywhere. None clears it. Mirrors Self::set_mem_description in backend symmetry, drift probe, and provenance-note posture.

Source

pub fn set_mem_subject( &mut self, mem_name: &str, new_subject: Option<MemSubject>, note: Option<&str>, ) -> Result<SetMemSubjectOutcome, EngineError>

Update a mem’s subject block — scope, method, deliberate exclusions, published verbatim. None clears the block AS A UNIT. Mirrors Self::set_mem_description.

Source

pub fn set_mem_internal( &mut self, mem_name: &str, internal: bool, note: Option<&str>, ) -> Result<bool, EngineError>

Mark (or unmark) a mem as internal — hidden from the default memstead_overview roster and public projections, while remaining a real, schema-validated, diffable mem (inspectable when explicitly scoped by name, and deletable). The ingest process-state redesign (candidate (b)) flags each ingest/<name> process mem this way so it does not clutter the roster alongside real content.

Stored as the top-level internal config field (captured by the flattened extra map). Backend-symmetric like Self::set_mem_description; EngineError::UnknownMem / ReadOnlyMount / InvalidInput on the usual failures.

Source

pub fn set_mem_sync_state( &mut self, mem_name: &str, key: &str, token: &str, note: Option<&str>, ) -> Result<SetMemSyncStateOutcome, EngineError>

Set (or clear) one opaque sync-state token in a mem’s per-mem config and persist it through the backend. The ingest layer calls this after a successful pass over a source’s changed slice to record “the source state the graph was last synced against”.

key and token are both opaque to the engine: the key is conventionally "<ingest>/<facet>" but the engine treats it as an arbitrary string; the token’s meaning belongs to the medium-type layer (git → commit id, graph → snapshot token, filesystem → a JSON-stringified stat digest). The engine never parses either. An empty token removes the key — the surface for clearing a baseline (which the next ingest pass re-seeds at the current source state).

Backend-symmetric like Self::set_mem_version: folder backends rewrite .memstead/config.json; git-branch backends commit __MEMSTEAD:mems/<mem>/config.json. Archive mounts reject with BackendError::Sealed.

Returns the (mem, key, previous-token) triple so callers can surface the change without an extra read. EngineError::UnknownMem when the name resolves to no mount; EngineError::ReadOnlyMount when the mount is sealed for writes; EngineError::InvalidInput when the mount has no loaded MemConfig.

Source

pub fn reload_one_mem(&mut self, mem: &str) -> Result<ReloadResult, EngineError>

Re-read the named mount’s backend entities and refresh the in-memory store for that mem. Returns the diff against the pre-reload snapshot — added (ids newly present), removed (ids no longer present), changed (same id, different content_hash).

Operator-triggered: useful when an external writer modified disk while this engine instance was alive (the lean flavour assumes single-writer; this primitive is the escape hatch when that assumption breaks). On the happy path the diff is empty.

Drift detection (whether disk did change) is not part of this surface — callers that want to short-circuit on “nothing changed” must compare added.is_empty() && changed.is_empty() && removed.is_empty() against the result. Backend-specific drift signals (git HEAD comparison, mtime check) live in the full-flavour engine where they have meaning.

Invalidates community + search-index memos on success.

Source

pub fn reload_one_mem_report( &mut self, mem: &str, ) -> Result<ReloadReport, EngineError>

Rich-shape variant of Self::reload_one_mem that returns a crate::ops::ReloadReport (mem + head_before + head_after + entities_loaded + changed_entity_ids) instead of the slim crate::ops::ReloadResult. Handler-facing wrapper consumed by the memstead_reload MCP tool — the rich shape is the wire contract MCP callers depend on; the slim form stays for programmatic consumers that just want the diff lists.

head_before is the engine’s prior cursor for this mem (its cached last_known_head), not the current on-disk tip: when a sibling has committed since, the tip has already advanced, so reporting it would make the advertised changes_since(since=head_before) recipe span an empty range. head_after is the freshly-peeled tip from crate::backend::MemBackend::current_head; the reload also advances the cursor to it, so a follow-up staleness probe does not re-reload the same window. Backends without history (folder, archive) carry no cursor and return Ok(None); both fields fall back to crate::ops::EMPTY_TREE_SHA for wire-shape stability.

entities_loaded is the post-reload non-stub count for the mem — same semantic as full’s report.

changed_entity_ids is the union of added ∪ changed ∪ removed from the underlying crate::ops::ReloadResult so callers don’t have to merge three lists themselves — matches full’s bundled wire shape.

Source

pub fn reload_each_writable_mem_reports( &mut self, ) -> Result<Vec<ReloadReport>, EngineError>

Batched rich-shape variant — returns one crate::ops::ReloadReport per mounted mem in declaration order. Counterpart to Self::reload_each_writable_mem (slim) that the memstead_reload MCP tool’s no-mem path consumes.

load_warnings semantics ride on the per-mem contract: each Self::reload_one_mem in the sweep refreshes its own mem’s slice of the engine-wide accumulator, so a full sweep leaves the accumulator equivalent to a fresh boot. (Earlier this variant discarded every reload warning while its slim counterpart repopulated — the MCP workspace-wide reload could never clear a stale warning.) On first-error-abort, mems reloaded before the failure carry refreshed slices and the rest keep their boot-time entries — no slice is lost.

Also re-reads .memstead/workspace.toml and refreshes crate::workspace::WorkspaceSettings before sweeping the mems — this is the pairing with the CLI’s memstead workspace allow-create / grant-cross-link / set-mutations family. Without this re-read, a CLI write would land on disk but the running MCP would still serve the engine’s boot-time policy snapshot; every subsequent memstead_mem_create against the new allowlist would fail with MEM_PATH_NOT_ALLOWED until process restart. The workspace-wide form runs the heavier path; the per-mem form (reload_one_mem_report) intentionally skips the workspace re-read — content drift doesn’t imply policy drift.

Reload of workspace.toml is best-effort: a missing or unparseable file leaves the existing settings untouched. The per-mem sweep is the primary contract — settings refresh is the additive bonus.

First-error-aborts: if any mem’s reload fails, the loop stops and the error propagates. Mems reloaded before the failing one are already mutated in the store; the returned error has no rollback. Operators run the per-mem form to retry the failing mem explicitly.

Source

pub fn reload_each_writable_mem( &mut self, ) -> Result<Vec<(String, ReloadResult)>, EngineError>

Reload every mounted mem in declaration order; returns one (mem, ReloadResult) per mount.

Failure model is first-error-aborts: if any mem’s reload fails, the loop stops and the error propagates. Mems reloaded before the failing one are already mutated in the store; the returned error has no rollback. Operators run the per-mem form to retry the failing mem explicitly.

Caller-friendly batching wrapper around Self::reload_one_mem; internal cache invalidation happens once per mem (the inner call invalidates) so an N-mem batch invalidates the memos N times. That’s wasteful for large workspaces; once the memstead_reload MCP handler migrates we can tighten this to one invalidation at the end.

Source§

impl Engine

Source

pub const BATCH_ERROR_REPORT_CAP: usize = 50

Cap on fully-detailed error envelopes in a refused batch’s report — bounded reporting for very large failing batches. Entries beyond the cap still carry action: "error"; the result’s errors_suppressed counts them. Never a silent truncation.

Source

pub fn create_entity( &mut self, args: CreateEntityArgs, actor: Actor, client: Option<&ClientId>, note: Option<&str>, ) -> Result<CreateEntityOutcome, EngineError>

Create a new entity in args.mem. Six concerns wired here in one shape regardless of which backend serves the mount:

  1. Capability gating — rejects mounts with ReadOnly capability before reaching the backend.
  2. Validator pipelinevalidate_section_keys + parse_metadata_value enforce the pinned schema’s strictness; typed ValidationError lifts to EngineError::Validation.
  3. Provenance — a Provenance record routes through backend.append_provenance (folder writes JSONL, git-branch no-ops since the commit subject + trailers carry the same fields).
  4. Write + commit atomicitybackend.write_entity then backend.commit with the canonical memstead: create <id> subject so the git-branch backend’s read_provenance can recover the kind.
  5. Store update — re-parse the freshly-generated markdown so the in-memory Store mirrors disk (including generator-determined content_hash).
  6. Error envelopeBackendError::Sealed lifts via the Backend variant so MCP callers see the typed payload intact; HashMismatch propagates likewise.
Source

pub fn batch_create( &mut self, creates: Vec<(CreateEntityArgs, Option<String>)>, actor: Actor, client: Option<&ClientId>, dry_run: bool, ) -> Result<BatchResult, EngineError>

Atomic batch create — the create-side sibling of Self::batch_update, with one upgrade and one addition:

  • Report-all refusal. Every failing entry is identified with its index and typed {code, message, details} envelope (the family’s upgraded contract) — bounded at Self::BATCH_ERROR_REPORT_CAP detailed envelopes, with errors_suppressed counting the rest. A refused batch writes NOTHING: no entity, no edge, no head movement.
  • Intra-batch references resolve as REAL targets. Every entity in the batch is staged (a skeleton store entry carrying its declared type) before per-entry validation runs, so an edge to a sibling created in the same batch gets full target-type shape validation, no transient stub, and no stub warning — the batch validates as one graph state, cycles included where the schema permits them. Duplicates within the batch are refused in the identity pass.

One workspace load (the caller’s), one commit per touched mem (subject memstead: batch-create (N entities)), per-entry provenance notes exactly like batch_update.

Rehearsal (dry_run: true): the FULL validation pass runs — identity, skeleton staging (so intra-batch references resolve as real targets, cycles included), per-entry prepare, report-all refusals — then the batch stops before any write. A legal batch returns the would-be receipt (applied: true, per-entry "created" with the prospective ids) with the marker form’s empty commit_sha; an illegal one returns the same refusal a real call would. Nothing is written, committed, or stubbed.

Source

pub fn create_entity_with_ctx( &mut self, args: CreateEntityArgs, ctx: &CommitContext<'_>, ) -> Result<CreateEntityOutcome, EngineError>

CommitContext-bundling wrapper around Self::create_entity. Destructures CommitContext into (actor, client, note) and delegates.

Source§

impl Engine

Source

pub fn classify_delete_referrers(&self, id: &EntityId) -> DeleteReferrers

Classify an entity’s incoming referrers by the source mount’s capability — the read-only core of the delete guard. Write-Mem referrers block the delete; ReadOnly referrers trigger the residual-stub demotion. Per-source dedup collapses an N-edge source into one ReferrerInfo carrying every rel-type. A referrer in an unmounted mem is treated as Write (safe-by-default: refuse rather than silently demote).

Pure read — no disk, commit, lock, or store mutation. Used by Self::delete_entity and the CLI delete --dry-run preview so both compute the same verdict from one implementation.

Source

pub fn delete_entity( &mut self, args: DeleteEntityArgs, actor: Actor, client: Option<&ClientId>, note: Option<&str>, ) -> Result<DeleteEntityOutcome, EngineError>

Delete an entity from its mount.

Binary semantics — there is no force flag. The engine partitions incoming references by the source mem’s MountCapability:

  • any Write-Mem referrers → refuse with typed EngineError::HasIncomingRefs carrying the structured referrer list;
  • only ReadOnly-mount referrers → delete the file + commit, then demote the in-memory entity to a stub at the same id so the surviving incoming edges keep a valid target. A RESIDUAL_STUB_FOR_READONLY_REFERRERS warning rides on the outcome;
  • no referrers → clean removal (file + store entry + cascading edges). Orphaned stubs whose last incoming edge was the deleted entity are GC’d.

Optimistic-locking via args.expected_hash matches update_entity. Stubs (no on-disk file) skip the backend write and commit but still log provenance.

Source

pub fn delete_entity_with_ctx( &mut self, id: &EntityId, expected_hash: &str, ctx: &CommitContext<'_>, ) -> Result<DeleteEntityOutcome, EngineError>

Positional + CommitContext wrapper around Self::delete_entity. Bundles id + expected_hash into a DeleteEntityArgs.

Source§

impl Engine

Source

pub fn rewrite_mem_references( &mut self, old_mem: &str, new_mem: &str, note: Option<&str>, ) -> Result<MemSweepOutcome, EngineError>

Rewrite every textual reference to mem old_mem so it carries new_mem instead: cross-mem wiki-links and Relationships entries in every writable peer mem, full-id self-references inside old_mem itself, and old_mem’s anchors-sidecar keys (<old>--<slug><new>--<slug>). One commit per affected mem; unaffected mems get no commit. Idempotent: a second run finds nothing left to rewrite and commits nothing — which is exactly what makes an interrupted mem rename completable by re-issuing it.

Read-only mounts are skipped (no write access); their stale references surface as load-time stubs on the next boot.

Source§

impl Engine

Source

pub fn apply_parse_recovery( &mut self, actor: Actor, client: Option<&ClientId>, note: Option<&str>, ) -> Result<ParseRecoveryReport, EngineError>

Walk load_warnings, dispatch the remove_explicit_relation recovery for every writable-origin PARSED_RELATION_INVALID, and report each entry on the response. Read-only-origin warnings cannot be acted on (the engine has no write access to their source markdown) and surface as outcome: "skipped" with reason: "readonly_mount".

Failure model: per-entry failures land on the response as outcome: "failed" with the underlying engine error code in reason. The bulk-fix continues past per-entry failures so a single bad source doesn’t strand the rest of the batch. Only engine-level errors (reload failure, broken workspace state) abort the call and propagate via Err.

Idempotency: after the per-source re-renders land, the method runs reload_each_writable_mem so subsequent calls to health / load_warnings reflect the post-recovery state. Re-running on an already-clean workspace returns an empty entries list with no commits.

Source§

impl Engine

Source

pub fn relate_entity( &mut self, args: RelateEntityArgs, actor: Actor, client: Option<&ClientId>, note: Option<&str>, ) -> Result<RelateEntityOutcome, EngineError>

Add or remove a typed relationship on args.source.

Cross-mem relate is policy-gated through Engine::cross_mem_link_allowed — the workspace’s [cross_mem_links] table (or per-create-rule default_cross_links synthesis) decides whether the edge is permitted. Disallowed pairings surface EngineError::CrossMemLinkNotAllowed. Cross-mem relate only writes the source entity’s markdown — the target mem is never written to. Auto-stub for absent targets works for Write target mems; ReadOnly target mems reject absent targets with EngineError::CrossMemTargetNotFound because the engine cannot persist a stub through the read-only boundary.

Schema-undeclared rel types surface either as validation errors (strict mode) or as ride-along warnings on the outcome (open mode).

Source

pub fn batch_relate( &mut self, relates: Vec<(RelateEntityArgs, Option<String>)>, actor: Actor, client: Option<&ClientId>, dry_run: bool, ) -> Result<BatchResult, EngineError>

Atomic batch relate — the edge-side sibling of Self::batch_create / Self::batch_update. One list carrying both additions and removals, applied in order: each entry validates against the graph state produced by every prior valid entry (an add followed by a remove of the same edge nets to no edge; an acyclic check sees edges added earlier in the same batch). Per-entry shape mirrors what relate accepts.

  • All-or-nothing, report-all. A single invalid entry refuses the whole batch — no edge changes, no head movement — and the refusal identifies EVERY failing entry with its typed {code, message, details} envelope, bounded at Self::BATCH_ERROR_REPORT_CAP with errors_suppressed counting the rest. An entry after a failing one validates against the state as of the prior valid entries, so a dependent entry may cascade — every reported code is still a true refusal of the submitted file.
  • One commit per touched mem (subject memstead: batch-relate (N edges)), per-entry provenance notes, exactly like the rest of the family. No-op entries (idempotent re-add / absent remove) report "noop" and produce no write.
  • Orphan-stub GC runs over every removed edge’s target after the commit, same predicate as the single-item path (the collected ids are not part of BatchResult’s fixed family shape).

Rehearsal (dry_run: true): the FULL in-order validation pass runs — each entry staged against the state its predecessors produced, identical refusals, identical report-all envelope — then the batch stops before any commit and rolls the staged state back. A legal batch returns the would-be receipt (per-entry actions, would-be orphan_stubs_removed computed on the staged state) with the marker form’s empty commit_sha; an illegal one returns the same refusal a real call would. No edge, stub, or commit lands.

Source

pub fn relate( &mut self, from: &EntityId, to: &EntityId, rel_type: &str, remove: bool, ctx: &CommitContext<'_>, ) -> Result<RelateEntityOutcome, EngineError>

Positional-args alias for Self::relate_entity. Bundles the positional inputs into a RelateEntityArgs (with expected_hash: None) and delegates to Self::relate_entity. The CommitContext is destructured into the 4-tuple (actor, client, note) the unified mutation surface accepts.

Source§

impl Engine

Source

pub fn rename_entity_with_ctx( &mut self, old_id: &EntityId, new_title: &str, expected_hash: &str, ctx: &CommitContext<'_>, ) -> Result<RenameEntityOutcome, EngineError>

Positional + CommitContext wrapper around Self::rename_entity.

Source

pub fn rename_entity( &mut self, args: RenameEntityArgs, actor: Actor, client: Option<&ClientId>, note: Option<&str>, ) -> Result<RenameEntityOutcome, EngineError>

Rename an entity by changing its title — the slug, id, and on-disk file path follow.

Same-mem referrers and self-references are rewritten atomically. The renaming entity is treated as the first referrer of itself: every entry in its own relationships list whose target equals the old id is updated to point at the new id, and every [[<old-slug>]] token in its own section bodies is rewritten to the new slug (respecting fenced-code and inline-code masking). Every other entity in the same mem that pointed at the old id (via an explicit relation or an inline body wiki-link) gets the same two- surface rewrite. All rewrites land in one per-mem commit. Cross-mem referrers and ReadOnly-mount referrers are not yet walked — those land with the multi-mem atomicity machinery and the residual-stub demotion path respectively.

Source§

impl Engine

Source

pub fn update_entity( &mut self, args: UpdateEntityArgs, actor: Actor, client: Option<&ClientId>, note: Option<&str>, ) -> Result<UpdateEntityOutcome, EngineError>

Update an entity’s sections and/or metadata.

Same six-concern shape as Engine::create_entity. Optimistic locking via args.expected_hash: when Some, must match the store’s current content_hash or returns EngineError::HashMismatch. The new engine’s MCP-facing callers should always pass the hash; None is the --force-style escape hatch.

Internally a two-step pipeline: Self::prepare_update runs all validation and computes the post-mutation markdown without committing, then Self::commit_prepared_update stages and commits the result. The split lets Self::batch_update prepare every item up front and commit the whole batch as one atomic unit.

Source

pub fn batch_update( &mut self, updates: Vec<(UpdateEntityArgs, Option<String>)>, actor: Actor, client: Option<&ClientId>, dry_run: bool, ) -> Result<BatchResult, EngineError>

Apply a batch of UpdateEntityArgs atomically — all or nothing. Surfaces BatchResult for memstead batch-update consumers.

The batch validates and prepares every item first (each with its own optimistic-lock check), then commits the whole set as one commit per mem. If any item fails — validation error, HASH_MISMATCH, entity-not-found, any per-item refusal — nothing is committed: the on-disk mem and the in-memory store are restored to exactly their pre-call state, and the result is marked applied: false with EVERY failing item carrying a typed {code, message, details} error envelope (the family’s report-all contract — bounded at Self::BATCH_ERROR_REPORT_CAP detailed envelopes, with errors_suppressed counting the rest) and every valid item marked "not_applied", so one repair cycle fixes the file.

On success the returned commit_sha is the single batch commit — an honest memstead_changes_since cursor / revert handle. Each item’s per-entry note rides into its own provenance record.

Empty batches return applied: true with zero counts and no commit. A batch where every item is a no-op (content unchanged) likewise applies with an empty commit_sha.

Rehearsal (dry_run: true): the FULL per-item validation pass runs — identical refusals, identical report-all envelope — then the batch stops before any write or commit. A legal batch returns the would-be receipt (applied: true, per-entry actions) with the marker form’s empty commit_sha; an illegal one returns the same refusal a real call would. Nothing is staged, committed, or stamped.

Atomicity is per-mem: for the common single-mem batch a commit-time backend failure rolls the whole batch back. A batch spanning multiple mems commits each mem in turn; if a later mem’s commit fails, already-committed mems stay committed (true cross-mem two-phase commit is out of scope) — but the dominant failure mode, a per-item validation/hash refusal, is always fully atomic because no commit happens until every item has passed.

Source

pub fn update_entity_with_ctx( &mut self, args: UpdateEntityArgs, ctx: &CommitContext<'_>, ) -> Result<UpdateEntityOutcome, EngineError>

CommitContext-bundling wrapper around Self::update_entity. See Self::create_entity_with_ctx for the rationale.

Source§

impl Engine

Source

pub fn record_anchor_observed_hashes( &mut self, mem_name: &str, observed: &[ObservedArtifactHash], note: Option<&str>, ) -> Result<usize, EngineError>

Record verify-observed prepared-content hashes onto hash-less hash-bearing anchors in mem_name’s anchors sidecar — the measurement-bookkeeping backfill the verify pass hands over via crate::ingest::VerifyOutcome::hash_backfill.

This mutates only the engine-owned sidecar (crate::anchor::ANCHOR_SIDECAR_PATH): no entity content, no section, no _hash is touched — an anchor-only commit yields zero entity deltas by construction. Guards enforced at this write seam, not left to callers:

  • only a hash-bearing class (anchored / derived) may gain a hash — an authored / informed-by anchor is never written, whatever the caller observed;
  • an anchor that already carries a hash is never overwritten — the recorded hash is the drift baseline, so the backfill is idempotent (a second identical call stages nothing and produces no commit).

Returns how many anchors gained a hash. Zero writes ⇒ no commit.

Source§

impl Engine

Source

pub fn store(&self) -> &Store

In-memory store populated at construction time from every mount’s backend. Read-only at this point in the rebuild — mutation paths land in a later session.

Source

pub fn schemas(&self) -> &HashMap<String, Arc<Schema>>

Per-mem schema, keyed by mount’s mem name. Each entry is the schema resolved from that mount’s pin at boot, so the map holds genuinely heterogeneous schemas in a multi-schema workspace.

Source

pub fn workspace_schemas(&self) -> &[Arc<Schema>]

Workspace-authored schemas loaded from WorkspaceSettings.schemas_dir at construction. Distinct from Self::schemas (per-mem, only schemas pinned by a mount): this slice carries every workspace-loaded schema regardless of whether a mem pins it. Used by memstead_overview to enumerate schemas referenced by mem_create_rules.schemas[] but not pinned by any mem — agents see what could be pinned without looking up the workspace.toml directly.

Source

pub fn builtin_schemas(&self) -> &[Arc<Schema>]

Embedded built-in schemas loaded once at boot. Handlers resolving a schema pin by <name>@<version> (MCP’s memstead_schema, memstead_overview rendering) walk mem-pinned, workspace, and built-in catalogues in order — built-ins are the catch-all when no mem or workspace dir pins the schema. Workspace schemas shadow built-ins on (name, version) collision; resolve from workspace_schemas() first.

Source

pub fn schema_origin(&self, schema: &Arc<Schema>) -> OriginClass

Classify a schema’s trust origin — the single authority every read surface consults before serving a schema’s instruction-prose.

A schema is [OriginClass::FirstParty] iff it is an engine built-in or pinned by a writable mount in this workspace. Built-ins are compiled into the binary — unforgeable. A non-built-in schema earns first-party status only once the operator adopts it by writably mounting a mem that pins it: writing into a mem is the act that legitimately needs a schema’s authoring prose (system_message, write_rules, …), and the mount’s writable posture is set by the consumer’s own config — a publisher cannot forge it.

Everything else is [OriginClass::ThirdParty]: a schema present in the catalogue but pinned only by read-only mounts (a registry- installed read-mem or an adopted foreign folder/clone), or one the engine cannot vouch for at all. Its prose is served structural-only so a stranger’s free-text never reaches a consuming agent as instructions. This classifies by the mount graph — never by scanning the schema’s content, which a publisher controls — and ThirdParty is the safe default for any ambiguous origin.

Note a read-only mount pinning a built-in schema (e.g. a registry mem on default@1.0.0) resolves to the consumer’s own clean copy and stays first-party — the de-framing targets only foreign, non-built-in schemas that no writable mem has adopted.

Source

pub fn mem_origin_class(&self, mem: &str) -> OriginClass

Classify a mem’s data trust origin — the authority every read surface consults before serving an entity’s content (bodies, snippets, titles). A writable mount is [OriginClass::FirstParty]: its content is authored in this workspace. Anything else — a read-only mount (a registry-installed read-mem or an adopted foreign folder/clone) or an unknown mem — is [OriginClass::ThirdParty], so the consuming agent/host treats the content as quoted, untrusted data.

This reads the deployment’s declaration when one exists (see Self::declare_mem_origin), else the mount’s already-decided writable/read-only posture (fixed at adopt/mount time) — it never scans content, and both levers are consumer-side config, so a publisher cannot forge first-party. Distinct from Self::schema_origin, which governs a schema’s instruction-prose: the data channel and the instruction channel are separate vectors with separate authorities.

Source

pub fn declare_mem_origin( &mut self, mem: impl Into<String>, origin: OriginClass, )

Declare a mem’s data-trust origin as a deployment fact — the embedding process (a curated hosted read tier, an app that vouches for a bundled mem) overrides the writability inference for one mem. Composition-layer-only by design: not persisted, not reachable over MCP, never derived from mem content — the operator running the process is the only authority that can set it, so a served mem the deployment does not vouch for keeps reporting third-party on every surface. (Deliberately absent from UniFFI/CLI: those surfaces operate a workspace, not a deployment; the CLI counterpart would be a workspace-config knob no use case demands yet.)

Source

pub fn load_errors(&self) -> &[(PathBuf, String)]

Per-file errors collected during load. Non-fatal: the engine continues with whatever did parse. Empty when every backend’s content parses cleanly.

Source

pub fn settings(&self) -> &WorkspaceSettings

Workspace-level operator policy (mem create/delete rules, cross-mem links). Defaults to empty; populated via Engine::set_settings after construction. Surfaced for MCP handlers (memstead_health { include_config: true }, memstead_overview’s lifecycle-namespaces section) and other consumers that need to read workspace policy.

Source

pub fn pipeline_configs(&self) -> &BindingConfigs

The pipeline configs — the v2 single-record binding store — loaded from the workspace at boot: the read-only queryable surface the loader exposes. Empty for engines not booted from a workspace root, or for a workspace that declares no pipelines. The ingest skill, future MCP tools, and the macOS app consume this structured form rather than re-reading the JSON folders.

Source

pub fn pipeline_configs_json(&self) -> String

The pipeline configs serialized as a JSON string — the read counterpart of the add_projection_json edit entry point. Serialization-boundary callers (UniFFI, where serde does not live) get the store in one call and deserialize on their side.

Shape: { "bindings": [{ mem, name, config }] } — the v2 single-record store (config carries the whole binding: inline sources, operations, everything). The mediums / facets / ingests keys are gone with their record kinds. This reads the live binding store fresh (like the brief path) rather than the in-memory snapshot, so an edit shows back immediately. A missing root or a legacy/unreadable store yields the fallback empty object.

Source

pub fn set_pipeline_configs(&mut self, configs: BindingConfigs)

Overwrite the in-memory pipeline configs. The workspace-root boot paths call this after crate::pipeline_store::load_pipeline_configs; exposed so the full boot helper (a separate crate) can populate the same surface.

Source

pub fn note_missing_warning( &self, tool: &str, note: Option<&str>, ) -> Option<WarningHint>

Build a WarningHint::NoteMissing when the workspace has [mutations].require_notes = true and the caller omitted (or passed a blank/whitespace-only) note; None otherwise.

This is the single enforcement point for the require_notes provenance nudge. Every mutation that accepts a note calls it on its commit-landing path and pushes the result onto the outcome’s warnings, so both the CLI and the MCP transports inherit identical behaviour from the engine response rather than each re-deriving the policy at its own boundary (the drift that left the policy decorative on the CLI). tool becomes the warning’s details.tool — callers pass the engine-level verb (create_entity, update_entity, relate_entity, delete_entity, rename_entity, create_mem, delete_mem), matching the commit Tool: provenance trailer. The mutation still commits — the policy nudges, it never blocks.

Source

pub fn backend_factory(&self) -> BackendFactory

Backend factory currently installed on this engine. Returned by value because BackendFactory is a function pointer (Copy). Used by [crate::mem_management::create_mem] to materialise the backend for a freshly-registered mount; consumers that need to instantiate a backend ad-hoc can call this directly.

Source

pub fn git_branch_ops(&self) -> Option<GitBranchOps>

Git-branch ops bundle currently installed on this engine. None on lean-flavor engines that don’t see mem-repo mounts. Returned by value because super::GitBranchOps is Copy. create_mem reaches for the bundle to drive prune_residue against an unmounted gitdir when the ForceOverwrite recovery action is selected.

Source

pub fn get_entity(&self, id: &EntityId) -> Option<&Entity>

Convenience: look up a parsed entity by id. Returns None for unknown ids, including stub entries created for unresolved inline-link targets — callers that want to distinguish real from stub branch on Entity::stub.

Source

pub fn entity_anchors(&self, id: &EntityId) -> Vec<Anchor>

The stored provenance anchors for id, read from its mem’s anchors sidecar. Empty for an entity with none, an unknown mem, or a backend that does not persist anchors (a pre-anchor archive / any sealed read-only mount). Additive read surface (E3a): the resolution model lives in crate::anchor (crate::anchor::resolve_anchor / crate::anchor::compose_entity_anchors); the live per-anchor state (which requires observing the source artifacts through the medium/preparation pipeline) is E3b’s concern.

Source

pub fn entity_anchors_resolved(&self, id: &EntityId) -> Vec<ResolvedAnchor>

The stored anchors for id, each paired with its live resolution state when the engine could observe the source artifact this pass.

Additive over Self::entity_anchors: the durable data is unchanged; state is the crate::anchor::resolve_anchor outcome against an observation the engine produces here. It is produced only for a path-namespace, single-medium mem (codebase / filesystem) whose medium root resolves from the workspace — the engine observes working-tree existence at the current HEAD:

  • artifact absent ⇒ AnchorState::Orphaned;
  • artifact present, non-hash class (authored / informed-by) ⇒ Resolves;
  • artifact present, hash-bearing class (anchored / derived) ⇒ the prepared-content hash comparison decides: Resolves on a match, Drifted on a stable-medium mismatch, Recheck on an unstable medium or when a hash is unavailable on either side (a hash-less anchor, a tree grain, an unreadable artifact).

state is None (unobserved — never a fabricated state) when the mem has no single path-medium, no workspace root, or the grain/namespace is not a filesystem path. Non-path mediums / commit-pinned reads stay deferred (E3b’s remaining leg).

Source

pub fn anchors_referencing_artifact( &self, artifact_path: &str, ) -> Vec<(EntityId, Anchor)>

Reverse anchor lookup: every (entity_id, anchor) across all mems whose anchor references artifact_path. This is the query the rebuilt check-realization hook consumes — given the file an agent just edited, which entities anchored to it. A span/file/tree anchor references the path when its base path (locator suffix @commit / #span stripped) equals the path, or — for a tree grain — when the path lies under the tree. Path-shaped grains only; url / entity anchors are matched by exact base equality.

Source

pub fn mem_anchors_resolved(&self, mem: &str) -> Vec<(EntityId, ResolvedAnchor)>

Every (entity_id, resolved anchor) in mem, read from its anchors sidecar once and each paired with its live resolution state (the same observation Self::entity_anchors_resolved produces per entity, computed here mem-wide in a single sidecar read). Empty for an unknown mem, a backend that persists no anchors, or a mem with none.

Additive read surface: the durable data is unchanged; state is the crate::anchor::resolve_anchor outcome against an observation the engine produces for a single path-namespace medium, or None when unobserved (never fabricated). The verify pipeline consumes it to adjudicate a mem’s anchors against the source; audit/health can reuse it.

Source

pub fn verify_mem_anchors( &self, mem: &str, ) -> Result<MemAnchorVerification, EngineError>

Standalone anchor verification — “do my sources still say what I recorded?” for one mem, regardless of how it was built. Walks the mem’s anchor sidecar through the shared per-anchor mechanism (Self::observe_anchor via Self::mem_anchors_resolved) and classifies every anchor into the report vocabulary: resolved (source present, hash matches or non-hash class), drifted (present, hash differs, stability stable), recheck (hash differs under unstable, or a hash is missing on either side), unresolvable (source absent, or a grain/medium the mechanism does not reach — never fabricated into drift). Read-only on mem content: pure sidecar read + filesystem observation, no commit on any backend. A mem with no anchors returns an empty report.

Source

pub fn mem_names(&self) -> Vec<&str>

Mem names the engine knows about, in declaration order. Cheap; useful for callers that need to enumerate before dispatching by mem.

Source

pub fn derivation_report( &self, mem: &str, ) -> Result<Vec<DerivationFinding>, EngineError>

Derivation-staleness report for one mem (agent-trust plan 12): every EXPLICIT edge whose rel-type the mem’s schema declares derivation: true, compared against its recorded baseline. Baseline differs from the target’s current hash → stale; no baseline recorded (edge predates the declaration, or was load-derived) → unbaselined, distinctly — never fabricated as fresh or stale. Fresh edges are not reported. A mem whose schema declares no derivation rel-types returns the empty report; an unreadable sidecar reads as empty (every edge unbaselined) rather than an error.

Source

pub fn mount(&self, mem: &str) -> Option<&Mount>

Public-shape mount record for mem, or None for an unknown mem.

Surfaces the operator-facing crate::workspace::Mount (mem name, schema pin, storage reference, capability, lifecycle, cross_linkable) so MCP / CLI handlers can branch on backend-specific shapes via crate::workspace::MountStorage when they need accessors that don’t make sense on every backend (e.g. gitdir / branch for memstead_health { include_config: true }’s git-class payload). Backends that want the equivalent of full’s engine.gitdir_for(mem) match engine.mount(mem).map(|m| &m.storage) against MountStorage::GitBranch { gitdir, branch } and walk directly — keeps the engine surface backend-neutral.

Counterpart to Self::mem_names which lists every mount.

Source

pub fn orphans_by_schema( &self, orphan_ids: &[EntityId], ) -> BTreeMap<String, usize>

Orphan count attributed to each mem’s pinned schema, over the given orphan_ids (the caller pre-filters them by any mem scope). Lets a health surface show that ingest-mem isolates (orphans by design) and code-mem debt land in different schema buckets rather than one blended, misleading total. Mems with no settled pin bucket under the empty string.

Source

pub fn communities_by_schema(&self, mems: &[String]) -> BTreeMap<String, usize>

Community count attributed to each schema across mems: a cluster counts toward every schema whose mems it touches, so these figures can sum above the global community count — the same “touches” semantic as the mem-scoped count. Per-schema dedup keeps a cluster touching two mems of one schema from being counted twice.

Source

pub fn mounts(&self) -> Vec<&Mount>

All mounts the engine knows about, in declaration order. Counterpart to Self::mem_names when the caller needs the full mount shape (e.g. to enumerate by storage variant).

Source

pub fn writable_mem_names(&self) -> Vec<&str>

Names of mems whose mount declares crate::workspace::MountCapability::Write, in declaration order. Convenience over mounts().iter().filter(...).map(...) for handlers that gate by writable status (memstead_health, memstead_overview‘s mem roster, the lifecycle tools’ candidate list). Read-only mounts (archive backends) are excluded.

Source

pub fn default_writable_mem(&self) -> Option<&str>

The default writable mem — the target a mutation lands in when it omits mem. None when no writable mem is mounted.

Defined as the first writable mount in declaration order, i.e. the seed / earliest-created writable mem. This is a stable designation, not a function of the current name set: new mems register via register_writable_mem, which pushes onto the end of the mount list (and mounts.json preserves that order across reboots), so creating an additional mem never moves the default — even one whose name sorts ahead alphabetically. Deleting the current default promotes the next-earliest writable mem; that is the only thing that shifts it. Both the MCP resolve_mem and the CLI’s omitted---mem path resolve through here so the two surfaces always agree (the pre-fix MCP path read writable_mems().iter().next() off an unordered HashSet, which silently retargeted writes when a second mem appeared).

Source

pub fn folder_path_for_mem(&self, mem: &str) -> Option<&Path>

On-disk folder path for a folder-backed mount, or None for any other backend (git-branch, archive) or unknown mem. Convenience over engine.mount(mem).map(|m| &m.storage) + matching on MountStorage::Folder { path }. Used by handlers that need a filesystem path for a folder mem (e.g. memstead_health { include_config: true }’s mems[].vcs.worktree field for folder mounts).

Source

pub fn mem_router(&self) -> &MemRouterSnapshot

Runtime snapshot of writable / visible mems. Handlers that need the writable roster (memstead_health’s writable_mems / read_mems), per-mem origin tag (include_config: true’s mems[].origin), or visibility check (memstead_overview‘s mem list, the lifecycle tools’ collision guard) consume the router here. Returned by reference — the Arc is held on the engine; callers that need a clonable handle can Arc::clone the engine’s field directly when that surface arrives.

Source

pub fn gitdir_for(&self, mem_name: &str) -> Result<PathBuf, EngineError>

Resolve the gitdir for a writable mem. Used by memstead_health { include_config: true } to surface per-mem vcs.gitdir so outer-repo bookkeeping clients can git -C <gitdir> per mem without hardcoding the layout.

  • EngineError::UnknownMem when the name does not resolve.
  • EngineError::Mem when the mount’s storage is not git-branch-backed (folder, archive — they have no gitdir).
Source

pub fn worktree_for(&self, mem_name: &str) -> Result<PathBuf, EngineError>

Resolve the worktree for a writable mem. Used by memstead_health { include_config: true } to surface per-mem vcs.worktree.

  • EngineError::UnknownMem when the name does not resolve.
  • EngineError::Mem when the mount’s backend has no worktree concept (git-branch with no working tree, archive).

Folder mounts surface their on-disk path. Git-branch mounts follow the dir: Some(...) composition pattern: when the workspace root contains a folder named after the mem with a .memstead/config.json marker, that folder is the worktree (disk-shape composition). Otherwise — pure mem-repo-backed — return Err.

Source

pub fn mem_config_for(&self, mem: &str) -> Option<&MemConfig>

Per-mem .memstead/config.json payload, when available. Used by memstead_health { include_config: true } to surface the opaque write_guidance map and the catch-all extra fields per mem.

Folder-backed mounts return Some(&MemConfig) when <path>/.memstead/config.json parsed cleanly at construction. Git-branch and archive backends return None until the read-from-storage-backend path lifts (the V1 unified engine loads configs only from folder layouts; the file lives inside the gitdir / archive for the other backends and needs a backend-level read primitive).

Unknown mem names return None (no error variant — the accessor is intentionally lenient because memstead_health emits an empty detail block per missing config rather than aborting the call).

Source

pub fn archive_provenance_for(&self, mem: &str) -> Option<&ArchiveProvenance>

The authoring-provenance payload an installed mem carries, read from the archive’s .memstead/provenance.json at construction. None when the mem carries none (a pre-provenance archive, a runtime-created mem, or a backend that does not surface one) — the read path reports provenance as absent. Unknown mem names return None.

Source

pub fn mem_configs_named(&self) -> impl Iterator<Item = (&str, &MemConfig)>

Iterate (mem_name, &MemConfig) for every mount whose mem-config payload loaded at construction. Used by callers that walk every writable mount’s config (memstead health’s per-mem dump, the workspace-dump CLI). The yielded &str is the authoritative mem leaf from the mount record.

Folder-backed mounts yield when their .memstead/config.json parsed cleanly. Git-branch and archive backends are silent in V1 (the same deferred-read-from-storage gap that Self::mem_config_for documents).

Source

pub fn schema_for(&self, mem: &str) -> Option<Arc<Schema>>

Resolved Arc<Schema> for a writable mem by name. None when the name is not a registered mount.

Cheap — Arc::clone over the per-mem schema map. Resolved schemas are stored in HashMap<String, Arc<Schema>> so the lookup is a single hash hit + clone.

Source

pub fn mem_head_sha( &self, mem_name: &str, ) -> Result<Option<String>, EngineError>

Cached current branch-tip cursor (typically a 40-char hex SHA for git-branch backends; None for fresh mems or backends that don’t track a head — folder / archive).

The value is the per-mount last_known_head, seeded at construction by backend.current_head() and refreshed by Self::reload_if_stale / mutation paths after a successful commit.

  • EngineError::UnknownMem when the name does not resolve.
Source

pub fn mem_drifted(&self, mem_name: &str) -> Result<bool, EngineError>

Whether a sibling writer has advanced this mem’s backend past the engine’s cached last_known_head — a read-only drift probe that does not reload (unlike Self::reload_if_stale). One backend.current_head() read compared against the cached cursor; the comparison clears once the engine re-reads (a reload / reload_if_stale refreshes last_known_head to the live tip).

Only git-branch backends track a head, so folder / archive / in-memory mounts always report false. A backend that errors on the probe (transient refdb hiccup) reports false rather than surfacing the error — drift is advisory, and the next real operation’s reload path is the authoritative sync.

  • EngineError::UnknownMem when the name does not resolve.
Source

pub fn workspace_root(&self) -> Option<&Path>

Workspace root the engine booted from, when one is known. None for engines built directly from a mount list (tests, ad-hoc consumers). Set by Self::from_workspace_root and the full counterpart.

Source

pub fn load_warnings(&self) -> &[WarningHint]

Typed warnings surfaced during mem load — drift findings the loader pipeline collects per entity. Empty for V1; the accessor surfaces them so handlers can merge into health summaries uniformly.

Source

pub fn quarantined_mems(&self) -> &[QuarantinedMem]

The quarantine roster: mems that failed their mem-level boot step and serve nothing until repaired + reloaded. Empty on a fully healthy workspace. Surfaced on overview and health.

Source

pub fn quarantine_reason(&self, mem: &str) -> Option<&QuarantinedMem>

The quarantine entry for mem, when it is quarantined.

Source

pub fn unknown_mem_error(&self, mem: &str) -> EngineError

The typed error for a mem name that did not resolve to a serving mount: MEM_QUARANTINED (carrying the underlying boot failure and its repair command) when the mem is on the quarantine roster, UNKNOWN_MEM otherwise. Every lookup site that fails to find a mem routes here so a quarantined mem is never misreported as unknown — honest absence, with the reason.

Source

pub fn boot_diagnosis(&self) -> Option<(&str, &str)>

The workspace-level boot diagnosis a diagnostic-shell engine carries (None on ordinarily booted engines).

Source

pub fn diagnostic_shell(reason_code: String, reason_message: String) -> Engine

Build a mem-less diagnostic-shell engine for a workspace whose boot failed at the WORKSPACE level (nothing loadable — e.g. an unparseable store). It serves no mems and no entities; its one job is answering overview/health with the typed boot diagnosis so a session can always ask WHY the graph is gone — the MCP server serves this instead of exiting into -32000 Connection closed (degrade, never disappear).

Source

pub fn extend_quarantine(&mut self, entries: Vec<QuarantinedMem>)

Append boot-path quarantine entries recorded outside from_mounts_inner (backend-instantiation failures happen before the mount list reaches the engine constructor). Boot paths only — quarantine is a boot judgment, never a runtime mutation.

Source

pub fn communities(&self) -> &LouvainOutput

Lazy community-detection cache. First call runs Louvain against the current store using one pinned schema for community.{resolution, seed} and the per-rel weights. Subsequent calls return the cached result. Mutations invalidate the cache via Self::invalidate_communities.

One detection run per engine. The partition is workspace-global, so it needs a single source for the Louvain parameters; that source is the schema of the lexicographically-first mem name — a stable key, so the partition is deterministic across processes even when mounts pin heterogeneous schemas. For a single-schema workspace every mem’s schema is identical, so the choice of key is immaterial there.

Source

pub fn invalidate_communities(&mut self)

Drop the cached community detection result.

Source

pub fn orphans(&self) -> Vec<EntityId>

Real entities with no incoming or outgoing edges — leaf-declared types exempt (their edge-less entities are terminal by construction; see Self::leaf_population).

Source

pub fn leaf_population(&self) -> BTreeMap<String, usize>

Count of real entities per leaf-declared type, keyed <schema_ref>:<type> — the visible population the orphan exemption covers.

Source

pub fn stubs(&self) -> Vec<(EntityId, Vec<EntityId>)>

Stub entities with their referencer ids.

Source

pub fn most_connected(&self, limit: usize) -> Vec<Connectivity>

Top limit entities by total degree.

Source

pub fn missing_required_outgoing( &self, mem_filter: Option<&str>, ) -> Vec<MissingRequiredOutgoingReport>

Entities whose type’s required_outgoing blocks are not yet satisfied. mem_filter = None scans every mem; Some(v) scans only that mem.

Source

pub fn constraint_findings( &self, mem_filter: Option<&str>, ) -> Vec<ConstraintFindingReport>

Standing violations of declared constraints (the health constraints include) — every non-stub entity whose type declares constraints its current state violates, in deterministic (mem, id) order.

Source

pub fn schema_format_defects(&self) -> Vec<SchemaFormatDefect>

Defective section-format declarations the loaded schemas carry (lenient boot recorded them; install would have refused).

Source

pub fn conformance_findings( &self, mem: &str, target_schema: Option<&SchemaRef>, ) -> Result<Vec<IntegrityFinding>, EngineError>

Conformance-axis integrity findings for one mem — which entities a write would refuse under the effective schema, and why. target_schema = None lints against the mem’s current pin; Some(ref) lints against that schema instead (resolved among mem-pinned, workspace, and built-in schemas).

Source

pub fn schema_pin(&self, mem: &str) -> Option<SchemaRef>

The mem’s Mount.schema expectation assertion, when set. None for unknown mems and for mems whose mount carries no assertion (the authoritative pin then lives in the backend config; the resolved active schema, not this, is the effective pin).

Source

pub fn migration_target(&self, mem: &str) -> Option<SchemaRef>

The mem’s in-flight migration target, when dual-pin state is active. None for settled or unknown mems.

Source

pub fn consistency_findings( &self, mem: &str, ) -> Result<Vec<IntegrityFinding>, EngineError>

Consistency-axis integrity findings for one mem — the pre-existing graph-coherence categories (dangling links, stubs) projected into the { id, axis, code, detail } finding shape.

Source

pub fn health(&self) -> HealthSummary

Engine-wide health summary across every mount.

Source

pub fn status(&self) -> Status

Engine-wide crate::ops::Status across every mount — the graph counts behind memstead status (renamed from stats with the command, D11; fields unchanged).

Source

pub fn context(&self, id: &EntityId) -> Option<ContextResult>

Build a ContextResult for id: the community cluster id (or None when the entity is a stub or not present), plus the outgoing + incoming neighbour lists.

Source

pub fn search_indexes(&self) -> &HashMap<String, MemIndex>

Lazily-built per-mem search index map. The map carries one entry per writable mem. Build cost scales with entity count; expect hundreds-of-ms for thousand-entity workspaces. Not available on wasm32 targets — search lives behind the bridge (see Self::search for the typed refuse).

Source

pub fn invalidate_search_indexes(&mut self)

Drop the cached per-mem search index map. No-op on wasm32 where no index exists; the method stays present so mutation hooks can call it unconditionally.

Source

pub fn list(&self, scope: &SearchScope) -> ListResult

Filter the in-memory store by metadata only (no text match).

Source

pub fn search(&self, scope: &SearchScope) -> Result<SearchResult, EngineError>

Run a search against the lazily-built index map. Returns EngineError::SearchUnavailable on wasm32 targets — browser consumers route search to the bridge; the local engine never builds a tantivy index in WASM. Native targets get the same shape as before, wrapped in Ok.

Source

pub fn list_entities(&self, mem: &str) -> Result<Vec<PathBuf>, EngineError>

All mem-relative entity paths under mem. Delegates to the backend’s list_entities. Order is backend-defined.

Source

pub fn read_entity( &self, mem: &str, rel_path: &Path, ) -> Result<Option<Vec<u8>>, EngineError>

Raw bytes for a single entity (Ok(None) if absent).

Source

pub fn read_provenance( &self, mem: &str, cursor: Option<&str>, ) -> Result<Vec<Provenance>, EngineError>

Provenance entries for mem since cursor. Cursor shape is backend-specific (RFC-3339 timestamp for folder, commit SHA for git-branch); None means “from the beginning”.

Source

pub fn capability(&self, mem: &str) -> Result<MountCapability, EngineError>

Capability declared on the mount for mem. Surfaced for callers that need to gate before dispatching a write — the engine itself does not yet enforce capability (mutation paths land in a later session).

Source

pub fn edge_is_from_readonly(&self, from: &EntityId) -> bool

Returns true when from’s source mem is mounted with crate::workspace::MountCapability::ReadOnly. Returns false for Write-Mems and for mems whose mount is absent from the router (no mount → no ReadOnly assertion can be made; the absence is treated as not-ReadOnly so consumers don’t trip on transient lookup misses).

Plan body §“Single edge source in the store” specifies this helper as the derived-on-demand alternative to adding a new field on crate::store::Edge. Strict-invariant validators and surfaces that want to highlight cross-mount references call this rather than pattern-matching on a per-edge marker. The information is fully derivable from the current mount roster, so no new state needs to live on the edge itself.

Whether a cross-mem edge from from_mem to to_mem is permitted under the current crate::WorkspaceSettings cross-mem link policy.

Resolution rules (matches full’s mem_router semantics):

  1. Same-mem edge (from_mem == to_mem) → always allowed; the policy gates cross-mem edges only.
  2. Explicit cross_mem_links[from_mem]:
    • "*" (wildcard) → allowed regardless of target.
    • ["a", ...] (allowlist) → allowed iff to_mem is in the list.
  3. Per-create-rule default_cross_links synthesis — if rule (1) didn’t grant permission and from_mem matches a [[mem_management.create]] rule whose default_cross_links is set, the synthesised value contributes:
    • "*" → allowed regardless of target.
    • ["a", ...] → allowed iff to_mem is in the list.
  4. Otherwise → denied (default-deny posture).

The synthesis layer compiles a crate::mem_management::CreateRuleSet lazily on first call and caches it; Self::set_settings invalidates the cache. Compilation failure (malformed glob in a rule) logs a warning and the synthesis layer is silently skipped — the resolver still returns true from explicit policy alone, so a half-broken config doesn’t lock out edges the operator did intend to allow. Operators who want hard validation pre-compile via crate::mem_management::CreateRuleSet::new before calling Self::set_settings.

The MCP memstead_relate handler’s cross-mem gate consumes this method directly.

Source§

impl Engine

Source

pub fn review_marks(&self) -> Vec<ReviewMarkStatus>

Every mem’s review mark (or its absence) with the current head. Markless mems are ordinary entries, never errors.

Source

pub fn set_review_mark( &mut self, mem_name: &str, target: Option<&str>, note: Option<&str>, ) -> Result<SetReviewMarkOutcome, EngineError>

Set (or clear, with target: None) a mem’s review mark to an explicitly named state. The target is validated against the backend’s cursor vocabulary before anything is written — git-branch cursors must resolve to a known commit, folder cursors must parse as the changelog’s RFC3339 timestamp shape — and an invalid target refuses with INVALID_CURSOR, leaving the mark untouched. Provenance (note gating, warn-and-commit) mirrors set_mem_sync_state; the config write commits with the caller’s note.

Source

pub fn review_mark_diff( &self, mem_name: &str, rename_similarity: Option<f32>, ) -> Result<ChangesReport, EngineError>

The accumulated per-entity delta from the mem’s review mark to its current head — exactly the envelopes changes_since reports for the mark’s cursor. A markless mem refuses with REVIEW_MARK_NOT_SET (marklessness is known from the roster; a silent empty answer would equate “no mark” with “no changes”).

Source§

impl Engine

Source

pub fn mem_topology(&self, mem: &str) -> Result<MemTopology, EngineError>

Project mem’s current topology from the live store — every entity in the mem, every relationship edge sourced in the mem (cross-mem targets marked), and the mem’s community roster from the global partition. Recomputed on every call, never incremental: deleted or renamed entities are simply absent from the next projection. Unknown mems refuse with crate::EngineError::UnknownMem.

Source§

impl Engine

Source

pub fn add_projection_json( &mut self, mem: &str, name: &str, projection_json: &str, note: Option<&str>, ) -> Result<(), PipelineEditError>

Create a binding from a JSON BindingPatch applied to the default scaffold: the caller may supply any author-editable field, including the inline sources and a full operations block; an absent block scaffolds the default build (discovery / loop / batch 20). See add_binding_json for the refusals (duplicate, missing destination_mem, in-record validation).

Source

pub fn update_projection_json( &mut self, mem: &str, name: &str, projection_json: &str, note: Option<&str>, ) -> Result<(), PipelineEditError>

Patch a binding from a JSON BindingPatch — absent fields are preserved (tail preservation, extended to every field); explicit null clears intent / rules / prune; a present sources or operations block replaces that whole block; version stays engine-managed. See update_binding_json for the refusals (not-found, in-record validation).

Source

pub fn delete_projection( &mut self, mem: &str, name: &str, note: Option<&str>, ) -> Result<(), PipelineEditError>

Delete a binding and refresh the snapshot. See delete_binding.

Source

pub fn rename_projection( &mut self, mem: &str, old: &str, new: &str, note: Option<&str>, ) -> Result<(), PipelineEditError>

Rename a binding and refresh the snapshot. See rename_binding.

Trait Implementations§

Source§

impl Debug for Engine

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

§

impl !Freeze for Engine

§

impl !RefUnwindSafe for Engine

§

impl !Sync for Engine

§

impl !UnwindSafe for Engine

§

impl Send for Engine

§

impl Unpin for Engine

§

impl UnsafeUnpin for Engine

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Converts Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>, which can then be downcast into Box<dyn ConcreteType> where ConcreteType implements Trait.
Source§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Converts Rc<Trait> (where Trait: Downcast) to Rc<Any>, which can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
Source§

fn as_any(&self) -> &(dyn Any + 'static)

Converts &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
Source§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Converts &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
Source§

impl<T> DowncastSend for T
where T: Any + Send,

Source§

fn into_any_send(self: Box<T>) -> Box<dyn Any + Send>

Converts Box<Trait> (where Trait: DowncastSend) to Box<dyn Any + Send>, which can then be downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Fruit for T
where T: Send + Downcast,

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more