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§
- EgoParams
- Parameters for ego snapshot building.
- Graph
Edge - Graph
Node - Graph
Snapshot - Graph
Truncation - Graph
View - 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 - Read
Scopes - A non-empty set of scopes a read filters by. The non-empty invariant is
structural: an empty
ScopeSetadmits nothing, so an empty read set would make every default read silently return empty. - Remember
Plan - Remember
Request - Temporal
Rewrite - A temporal phrase resolved against the injected reference
now_ms.
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 - ARTIFACT_
LABEL - Warehouse-derived nodes (crate
topodb-warehouse): anArtifactis one raw thing a session touched; aChunkis a text-indexed window of it. - CHUNK_
LABEL - CHUNK_
TEXT_ PROP - DEFAULT_
LOCK_ WAIT_ MS - Default busy-retry budget (ms) when
TOPODB_LOCK_WAIT_MSis unset or unparseable. Shared so the CLI, the MCP stdio server, and the daemon can never drift apart on the value. - 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 - GRAPH_
DEFAULT_ LIMIT - GRAPH_
MERMAID_ INLINE_ MAX_ NODES - GRAPH_
SNAPSHOT_ VERSION - GRAPH_
TITLE_ MAX_ CHARS - LIFECYCLE_
DEFAULT_ LIMIT - How many candidates a sweep reports by default.
- LIFECYCLE_
HALF_ LIFE_ DECISION_ DAYS - A deliberate tie to the semantic constant: decisions age like standing facts. Change independently if dogfood shows otherwise.
- 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_ DECISION - 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”),decision(a resolved choice plus its rationale: “ship as one lean PR”). 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.
- NEAR_
DUP_ K - How many near-duplicates to surface at most — enough to notice a redundancy without burying the caller.
- NEAR_
DUP_ REVIEW - Cosine floor below
NEAR_DUP_THRESHOLDat which a pair is still surfaced, but only as a weaker"possible"candidate. Measurement showed genuine reworded duplicates can sit as low as ~0.70 — and merely-related facts sit right there too (0.69), so there is NO floor that catches the former without the latter. Set just under that overlap (0.68) so borderline restatements are SURFACED for the caller (an LLM, a native entailment judge) to confirm, rather than silently dropped; the"possible"band is the warning that these need a look, not an automatic merge. Widening recall at the cost of precision is the right trade for an advisory tool where a human/agent makes the final call. - NEAR_
DUP_ THRESHOLD - Cosine-similarity floor for surfacing a semantic near-duplicate.
- SYNONYM_
EXPANSION_ PROP - SYNONYM_
LABEL - SYNONYM_
TERM_ PROP - TEXT_
BAND_ MIN_ TOKENS - Floor on the SMALLER token set’s size before a text-mode containment score may claim the “likely” band. Whitespace tokens include stopwords, so a short memory’s set is trivially contained in any longer memory that happens to mention the same words — containment 1.0 from 3 shared tokens is weak evidence, not strong. 6 keeps the calibrated canonical pair (“Vega stores its data in postgres”, 6 tokens, 0.833 → likely) exactly at its band.
- TEXT_
NEAR_ DUP_ CANDIDATES - BM25 candidate window for the write-time advisory; the scan is exhaustive.
- TEXT_
NEAR_ DUP_ CONTAINMENT - Text containment floor for text-based near-duplicate fallback when the embedder
isn’t Ready. CONTAINMENT = |∩| / min(|A|,|B|) scores candidates by how completely
one fact is contained in another — the canonical restatement shape. Floor 0.7
catches the canonical pair (“Vega stores data in postgres” / “Vega now stores data
in sqlite for embedded mode” = 5/6 ≈ 0.833) while leaving unrelated disjoint facts
near 0; a short fact fully contained in a longer restatement scores 1.0 exactly
(supersession/rewording). See
dup_bandfor confidence levels. - 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§
- apply_
upsert_ remap - Correct planned entities against an applier’s upsert remap.
- build_
ego - Build an ego-view snapshot from seeds and optional query.
- build_
scope - Build a scope-view snapshot: all entities and memories in the scope,
truncated to
limit(keeping newest-first by ULID, recording honest dropout counts). All nodes have hop: 0. View is marked “scope” with empty seeds and query. - containment_
of_ sets - Containment similarity of two precomputed token sets: |∩| / min(|A|,|B|). Returns 1.0 if both sets are empty. Returns 0.0 if exactly one set is empty (no overlap is possible, so containment is a deliberate 0.0 rather than NaN).
- 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. - dup_
band - Confidence band for a near-dup similarity:
"likely"at/above the strong floor (NEAR_DUP_THRESHOLD),"possible"in the review band below it. - dup_
relation "supersession"when the pair contradicts (seeis_supersession), else"duplicate".- edge_
believed_ at - Determine whether an edge was “believed” (recorded/present, belief axis)
at a given Unix-ms timestamp. Mirrors
edge_live_at’s shape exactly, but over the belief-axis fields: inclusive lower bound (recorded_at <= t), exclusive upper bound (superseded_at > t). An edge recorded aftertis invisible here regardless of its world-time validity — that’s the whole point of the recorded axis. Edges never superseded (None) are treated as still-believed indefinitely. - 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, the world-time boundsvalid_from/valid_to(valid_toisnullwhile the edge is open), and the belief-axis boundsrecorded_at/superseded_at(superseded_atisnullwhile the edge is open on that axis) — seeedge_live_atandedge_believed_at. - 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.- graph_
edge - graph_
node - is_
supersession - True when the two contents read as a CONTRADICTION rather than a restatement: one asserts a salient token the other negates. Cheap and deterministic — the hint that a high-similarity pair is a supersession (retire the stale one), not a duplicate (merge them). Calibrated in the module tests against a labeled battery.
- 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. - lock_
wait_ budget_ ms - Parse a
TOPODB_LOCK_WAIT_MSenv value into a busy-retry budget. Absent → the default, silently. Present-but-unparseable → the default plus a prefix-less warning (the caller prepends its own program name, since the CLI, the stdio server, and the daemon each identify themselves differently). This is the single source of truth for the parse-and-default policy the three front ends share. - memory_
kind_ half_ life - The kind→half-life map for SEARCH RANKING, built from the same constants the lifecycle decay sweep uses so the two can never drift. Semantic is the map’s default bucket: absent kind reads as semantic everywhere in the system, and non-Memory nodes (entities) deliberately decay on the semantic curve too.
- 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_
superseded - node_
title - Entity name, else
contentpreview (≤ GRAPH_TITLE_MAX_CHARS chars, char-boundary safe, ‘…’ suffix when cut), else the label itself. - 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). - parse_
iso_ instant - Resolve an explicit ISO bound to UTC ms. Accepts a date —
2026-08-01,2026-08, or2026, resolving to the start of its period — or a UTC datetimeYYYY-MM-DDTHH:MM[:SS]with an optional trailingZ, resolving to that exact instant (non-UTC offsets and fractional seconds are rejected: a bound silently shifted by timezone math would be worse than an error).Nonefor anything else. Shared by the MCPcreated_after/created_beforeparams and the CLI--created-*flags so explicit bounds and the rewriter resolve dates identically; the rewriter itself matches dates only — a datetime inside prose does not trigger a rewrite. - parse_
read_ scopes - Parses a comma-separated list of
shared(case-insensitive) or scope ULIDs. Whitespace around each entry is ignored. Order and duplicates are preserved. Rejects an empty list (""," , "). Each token is parsed viaresolve_scope— thedefaultpassed there is never used because every token is required. - parse_
temporal_ query - Extract one temporal phrase from
query, resolved againstnow_ms(unix ms, UTC).Nonemeans the caller searches its original query unmodified — see the module docs for the grammar and boundary rules. Pure and deterministic: same(query, now_ms)→ same result; no I/O, no clock reads. - 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. - text_
dup_ band - Band for a TEXT-mode (containment) score:
dup_band’s cosine-derived cutoffs, except capped at “possible” when the smaller token set is underTEXT_BAND_MIN_TOKENS— the score is real (the pair still surfaces), but small-set containment can’t justify “likely”. - to_
canonical_ json - to_dot
- to_html
- Self-contained interactive HTML: the canonical JSON is embedded verbatim
(with
<escaped as\u003cto make a literal</script>breakout impossible) insideassets/graph.html, a vanilla-JS force-layout viewer. - to_
mermaid - tokens
- Tokenize a string into a set of lowercase tokens (whitespace-split).
- 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.