Expand description
JSON ↔ engine-type conversions shared by TopoDB’s JSON-speaking front ends
(currently topodb-mcp; topodb-cli is next).
Most functions here are pure (no I/O, no Db access) and return Result<_, String> — callers are responsible for mapping the String into their own
error type (topodb-mcp’s server.rs maps it to an rmcp::ErrorData:
invalid_params for bad input, internal_error otherwise). Nothing here
ever panics: an unrepresentable value is always an Err, never an
unwrap/expect. Exception: the compose module reads from a Db to
plan writes, but never writes itself.
Structs§
- Lifecycle
Candidate - One decay candidate with its full evidence — everything the judge needs to decide keep|forget without another lookup.
- Lifecycle
Params - Tunables for one sweep.
Defaultis the spec’s policy. - Planned
Entity - Remember
Plan - Remember
Request
Enums§
- Compose
Error - Planning failure:
Invalidis a caller-fixable input problem (surface the message verbatim);Engineis a database failure unrelated to the input.
Constants§
- ALIAS_
EDGE_ TYPE - ALIAS_
LABEL - ALIAS_
NAME_ PROP - DEFAULT_
REMEMBER_ EDGE_ TYPE - Edge type
rememberuses when the caller doesn’t name one. - ENTITY_
LABEL - Label/prop name constants for the two built-in write shapes
(
create_memory/create-memory,create_entity/create-entity). Single source of truth shared by every front end (topodb-mcp’s defaultIndexSpecand write tools,topodb-cli’screate-entity/create-memorysubcommands) so writes land on exactly the(label, prop)pairs the default spec indexes — search and lookup work out of the box regardless of which front end wrote the data. - ENTITY_
NAME_ PROP - LIFECYCLE_
DEFAULT_ LIMIT - How many candidates a sweep reports by default.
- LIFECYCLE_
HALF_ LIFE_ EPISODIC_ DAYS - Per-kind staleness half-lives (spec-fixed defaults, tunable per call): an episodic observation goes stale in weeks, a standing fact in months, a how-to in a year.
- LIFECYCLE_
HALF_ LIFE_ PROCEDURAL_ DAYS - LIFECYCLE_
HALF_ LIFE_ SEMANTIC_ DAYS - MEMORY_
CONTENT_ HASH_ PROP - Equality-indexed hash of a memory’s normalized content, used to dedup a
re-stored fact to the existing node instead of minting a duplicate. Set by
the write front ends (
remember/create_memory), never by a caller. - MEMORY_
CONTENT_ PROP - MEMORY_
FORGOTTEN_ AT_ PROP - Millisecond timestamp at which a memory was forgotten (the
forgetverb). Distinct from supersession — a forgotten fact was not replaced, just judged not worth keeping. Same tombstone mechanics: recall drops a memory whose value here is<=the query’s now; the node is not deleted. - MEMORY_
KINDS - The closed kind vocabulary, in canonical order.
- MEMORY_
KIND_ DEFAULT - What an absent
kindprop reads as. - MEMORY_
KIND_ EPISODIC - MEMORY_
KIND_ PROCEDURAL - MEMORY_
KIND_ PROP - Memory taxonomy prop (
kind), aStron Memory nodes:episodic(a dated observation: “CI was red this morning”),semantic(a standing fact: “release tags are per-package”),procedural(a how-to: “publish crates in dependency order”). ABSENT MEANSsemantic— no migration; the read side maps a missing prop to the default before filtering. Kind never affects ranking; it exists for the lifecycle decay policy and explicit filtering. - MEMORY_
KIND_ SEMANTIC - MEMORY_
LABEL - MEMORY_
SUPERSEDED_ AT_ PROP - Millisecond timestamp at which a memory was superseded by a newer fact.
Set by
remember’ssupersedes; recall drops a memory whose value here is<=the query’snow(so anas_ofbefore it still sees the old fact). The node is not deleted — supersession dates a fact, keeping its history. - MEMORY_
TOMBSTONE_ PROPS - The canonical liveness set: a Memory is live iff NONE of these props tombstones it. Every read surface (CLI search, MCP search_memories, dedup, advisories, hygiene) filters on this same set so “live” cannot drift between surfaces.
- SYNONYM_
EXPANSION_ PROP - SYNONYM_
LABEL - SYNONYM_
TERM_ PROP - UNSUPPORTED
- Error string for both directions of an unrepresentable
PropValue:BytesandDateTimehave no JSON counterpart over MCP v0, and any JSON shape that isn’t a string/number/bool (array, object, null) has noPropValuecounterpart either.
Functions§
- content_
hash - Stable FNV-1a 64-bit hash of normalized content, hex-encoded. PERSISTED
(equality-indexed as
content_hash) — the algorithm must never change. Collisions are harmless: dedup always verifies exact normalized content. - default_
spec - The ONE canonical default
IndexSpecfor TopoDB’s built-in write shapes, shared by every front end (topodb-mcpwhen--specis omitted;topodb-cliwhen it creates a brand-new db file). Declares equality on(Entity, name),(Alias, name), and(Synonym, term), and text on(Memory, content),(Entity, name), and(Alias, name), using the shared label and property constants. - edge_
live_ at - Determine whether an edge is “live” (valid/present) at a given Unix-ms
timestamp. The validity window is inclusive on the lower bound
(
valid_from <= t) and exclusive on the upper bound (valid_to > t). Open edges (novalid_to) are treated as eternally open. - edge_
to_ json - An
EdgeRecord→ JSON:id/from/toas ULID strings,typeforty(JSON-friendlier than the Rust keyword-adjacent field name),scopeperscope_to_json,propsperprops_to_json, and the temporal boundsvalid_from/valid_to(valid_toisnullwhile the edge is open). - entity_
dedup_ key - In-call dedup key for
remember’s entity names: whitespace-collapsed, lowercased — mirroring the engine’s prop-index normalization (prop_index::normalize_str, which is pub(crate) and thus can’t be called from here). Drift between the two only weakens IN-CALL dedup ([“Drew”, “drew”] in one call); cross-call dedup always goes through the engine’s own normalized index via find_existing_entity. - existing_
memory - The id of a Memory in
write_scopewhose normalized content equalscontent. Hash-bucket lookup, then exact normalized-content verify on every candidate; oldest id wins. Superseded or forgotten memories … are excluded — re-learning a retired fact is a NEW fact, and the tombstone’sas_ofhistory stays intact. - find_
existing_ entity lookupis the caller’s collision surface (MCP: default read scopes + write scope + shared; CLI: write scope + shared).Ok(None)means “create it” — covering both no-visible-match and a custom spec without the (Entity, name) equality index (Rejected), which degrades to create-always rather than failing the write.- json_
to_ f32_ vec - A JSON array of finite numbers →
Vec<f32>, for raw embeddings (topodb::Op::SetEmbedding) and vector-search queries.Errifvisn’t a JSON array, or any element isn’t a finite number. (The host computes embeddings; TopoDB stores/searches the raw floats.) - json_
to_ prop_ changes - A JSON object of property changes for
topodb::Op::SetNodeProps. Anullvalue REMOVES the key (None); any other JSON scalar SETS it (Some(PropValue), viajson_to_prop_value).Errifvisn’t a JSON object, or a non-null value isn’t a representable scalar. Thenull-removes convention is what lets a caller delete a prop over the wire — plainjson_to_propshas no way to express removal. - json_
to_ prop_ value serde_json::Value→PropValue. Strings/bools map directly. A JSON integer maps toIntwhen it fitsi64, and is an error when it doesn’t ((i64::MAX, u64::MAX]— silently downgrading it to a lossyFloatwould corrupt the value); only a genuine non-integer number maps toFloat. This is the inverse ofprop_value_to_json’sInt/Floathandling. Every other JSON shape (array, object, null — and, structurally, anything that would have needed to round-trip throughBytes/DateTime) isUNSUPPORTED.- json_
to_ props - A JSON object →
Props, propagating the first unrepresentable value asErr.Errifvisn’t a JSON object at all. - lifecycle_
candidates - The Phase C sweep: rank live memories in
scopesby staleness and return the topparams.limitwith full evidence. Read-only and unbumped end to end.now_msis injectable for determinism. - memory_
props - Builds a new Memory node’s props: caller
extrais validated (the system-maintained keys below are rejected, andcontentcollides via merge_required_prop), then the whitespace-normalized content hash is stamped. The ONE constructor for every front end’s new-memory write (plan_remember, MCP create_memory, CLI create-memory) — so the reserved set cannot drift between surfaces. - merge_
required_ prop - Builds the
Propsmap for a write tool that has one required, caller-named field (create_memory’scontent,create_entity’sname) plus an optional JSONpropsobject of additional metadata.key/valueare the required field, already converted to aPropValue;extrais the tool call’s optionalpropsparam, converted viajson_to_props. - node_
to_ json - A
NodeRecord→ JSON:id/labelas strings (ULID viaDisplayforid),scopeperscope_to_json, andpropsperprops_to_json. Deliberately omits theembeddingfield — no MCP v0 tool surfaces vector data (that’s a later concern via dedicated embedding tools). - normalize_
content - Normalize memory content for dedup: trim and collapse internal whitespace. Deliberately NOT lowercased — casing can carry meaning in a stored fact.
- normalize_
edge_ type - Canonical form for edge types: Unicode-lowercased, with runs of
whitespace, hyphens, and underscores collapsed to a single underscore
(leading/trailing separators dropped).
"Works At","works-at", and"works_at"all normalize to"works_at"— one relation, one vocabulary entry, instead of three parallel edge types that silently fragment traversal filters. Every front-end write path (linktool, batchlinkcommand, CLI) passes edge types through here; read-side type filters should probe both the raw and normalized forms so edges written before normalization stay reachable.Erron a type that normalizes to empty. - open_
with_ busy_ retry - Calls
openuntil it stops returningBusyorbudget_mselapses. Backoff: 25ms doubling to a 500ms cap, with cheap deterministic-enough jitter (from the clock’s nanoseconds — no rand dependency) so two contending processes don’t retry in lockstep.budget_ms == 0means exactly one attempt (fail fast). Exhaustion returnsErr(Busy). - plan_
forget - Ops marking
idsforgotten (stampforgotten_at+ close open out-edges) plus the forgotten ids. The disconnect motion isplan_supersede’s, but the contract is STRICTER: supersede is a bulk mark that skips already-retired ids;forgetis an explicit judgment, so EVERY id must be a live Memory in the write scope and any violation rejects the whole call before ops build.now_msis a parameter so tests are deterministic. - plan_
purge - Phase E: plan the destructive purge. Selects every Memory node in
scopeswhose ANY tombstone prop holds anIntstrictly older thantombstoned_before_msand returns oneOp::RemoveNodeper hit plus the ascending-sorted id list (same order). The caller decides whether to submit — the CLI’s dry-run prints this plan without ever writing. Non-Inttombstone values are not marks (the engine’s liveness rule); live nodes are never selected. Purge is deliberately CLI-only and never part of the lifecycle graph: reclamation is an operator action. - plan_
remember - plan_
supersede - Ops marking
idssuperseded (stamp + close open out-edges) plus the ids actually marked. Moved from server.rssupersede_ops;now_msis a parameter so tests are deterministic. Error strings must stay identical. Public building block for layers (e.g. topodb-obsidian) that supersede without going throughplan_remember. - prop_
value_ to_ json PropValue→serde_json::Value.Str/Int/Boolmap directly;Floatmaps to a JSON number (Erronly for a non-finite float, which JSON has no representation for).Bytes/DateTimeareUNSUPPORTED.- props_
to_ json Props(aBTreeMap<String, PropValue>) → a JSON object, propagating the first unrepresentable value asErr.- resolve_
batch - resolve_
entities_ by_ name - Canonical entities for
name: direct (Entity, name) matches plus (Alias, name) matches followed through alias_of. Deduped by id, oldest first. - resolve_
scope - Resolves a tool’s optional
scopestring param to aScope:None→default(the server’s configured default scope);Some("shared")(case-insensitive) →Scope::Shared;Some(<ulid>)→Scope::Id; any other string → a clearErr. Mirrorstopodb-mcp’sconfig::parse_scope“shared” / ULID contract, generalized to theOption(tool-call) case. - scope_
label - Human/JSON-facing rendering of a
Scope:"shared"or the ULID string. Reused by every front end’sinfo/db_info-style output and scope round-tripping. (Distinct fromscope_to_json, which wraps the same rendering in aserde_json::Valuefor a JSON response body; this returns a bareStringfor contexts — like a struct field — that want the label without aValuewrapper.) - scope_
to_ json - A
Scope→ its JSON rendering:"shared"or the scope’s ULID string. Mirrors theshared/ULID label convention used across TopoDB’s JSON-facing front ends (e.g.topodb-mcp’sdb_infotool). - scope_
to_ scope_ set - A resolved
Scope→ the singletonScopeSeta read call needs:Sharedadmits only the shared scope,Id(id)admits only that one scope id. - scopes_
to_ scope_ set - Several resolved
Scopes → theScopeSeta multi-scope read runs against.Scope::Sharedsets the set’sinclude_sharedflag; eachScope::Idbecomes a member id. This is the only constructor that can produce a genuinely multi-memberScopeSet—scope_to_scope_setalways collapses to a singleton, which is why “this project plus shared” was previously unexpressible from any client. - staleness
- The spec’s staleness score:
(age / half_life) / ln(e + access_count). Negative ages (a node minted afternow— clock skew or a backdated sweep) clamp to 0. Pure and exported so tests and future policy layers can pin the formula itself. - subgraph_
to_ json - A
Subgraph→{"nodes": [...], "edges": [...]}, each element pernode_to_json/edge_to_json. - upgraded_
spec - Maps a db’s persisted spec forward when — and only when — it is exactly a
stock default this crate has shipped: any recognized stock generation
upgrades to the current
default_spec. Any other spec — a--speccustomization, however small — is returned unchanged: silently rewriting a declared spec would reindex data behind its owner’s back. Comparison is order-insensitive, matching how the engine’sensure_index_speccompares specs (it sorts both lists before persisting). - validate_
memory_ kind - Enum-validates a caller-supplied memory kind. Exact-match, lowercase only — a taxonomy is a vocabulary, not a suggestion box.