v0.5: Semantic layer & second-brain enrichment: make the graph think like the engineer
| Field | Value |
|---|---|
| Date finalised | 2026-06-02 |
| Author | Rômulo Febasi <romulo@febasi.com.br> (analysis assisted by Claude Code; market scan from public 2026 sources, footprint numbers measured against fastembed-rs 5.x, ort 2.x, sqlite-vec, BGE/Nomic/mxbai model cards) |
| Scope | Roadmap brief for Synapse v0.5.0: the cycle that adds a semantic retrieval layer (embeddings + hybrid ranking + lightweight entity linking) and codifies the new business rule that turns Synapse from a typed Markdown store into an active knowledge graph that thinks at insertion time. Includes the architectural-debt cleanup left by v0.4 (mcp/server.rs re-split, cli/at + cli/audit yellow tier splits) and the network-freshness Phase 2 deferred from v0.2 (SYN-93b). |
| Status | fully decided 2026-06-02: owner answered all six §11 questions; ticket numbering locked in §9 (existing SYN-80..89 + SYN-93b stay, new tickets fill SYN-151..154 and SYN-160..167); ready to file issues in milestone #7 |
| Inputs | v0.4 closure read (2026-06-02-v0.4-closure-and-roadmap-review.md); owner’s 2026-06-02 directive: “local-first sem Ollama; processar dados no momento da inserção; relacionar temas como o cérebro humano relaciona; benchmarking forte”; concrete use case scenarios provided by the owner (Carina / Firebase Remote Config / flag name); current v0.5 milestone (#7, 11 issues SYN-80..SYN-89 + SYN-93b); v0.4 brief that cut syn agent from v0.4 to v0.6 explicitly; ADR-008 reserved slot for the embedding-backend decision; ADR-013 audit trail + BR-12 privacy bundle; CHANGELOG.md §0.4.0. |
| Out of scope | syn agent autonomous loop with batch approval: deferred to v0.6 (owner decision on 2026-05-25 carried forward). Full Microsoft-style GraphRAG with community summaries: too expensive for v0.5, deferred to v0.6+. On-device generative LLM (Llama / Phi / Gemma in-process via candle): out of scope; Synapse continues to call MCP clients for generation, not embed a generator itself. Web UI / TUI: backlog. Cross-bundle merge of identity bundles: v0.6+. Sigstore transparency log for identity: kept at v0.5+ per ADR-016. |
Executive summary
v0.4 closed every documented debt of v0.3 and shipped a cross-editor LSP companion. v0.5 has a different shape: it changes what Synapse is, not just what it can do. The product moves from “typed Markdown store with FTS5 search and a great audit trail” to “active knowledge graph that recognises the same entity across notes the moment you write each one”.
The owner’s framing is what makes this brief different from a pure
“add embeddings” cycle: do the work at insertion time, not query time.
The brief codifies this as a new business rule BR-13 and structures
v0.5 around it. Embeddings ship as one signal among three (FTS, vector,
entity graph) combined by Reciprocal Rank Fusion. The local-first
invariant is preserved: the chosen embedding backend is fastembed-rs
via ONNX Runtime: ~70 MB total footprint, ~14× smaller than the
equivalent Ollama setup, runs on any CPU without external services.
Twenty tickets (twelve core + four arch-debt + four stretch). Six weeks of focused execution. The benchmark suite (SYN-90..SYN-93 corpus generator + P@k / nDCG@10 / MRR / latency budget) is core, not stretch: without it the choice of every algorithm in v0.5 is folklore.
1. The premise: what Synapse is for, restated for v0.5
Synapse’s target user has been consistent since v0.1: the multi-context developer of 2026, the engineer who runs 3-7 client projects in parallel because Claude Code / Cursor / Cline let them. The product compresses context-switching cost. v0.1..v0.4 made the store correct, auditable, and scriptable. v0.5 is the cycle that makes it associative: when the user writes a note about a meeting with Carina, the workspace should already know that “Remote Config” mentioned there is the Firebase tool from the other note. The query “flag do Firebase do dia X” finds both notes because the ingestion step wired them together; the search engine is just looking up the edge that was already drawn.
1.1 The canonical query that v0.5 must answer
The owner’s concrete example, restated:
Note A:
notes/2026-04-20-reuniao-carina.md, kindmeeting: “Reunião com Carina sobre a flagenable_v2_checkoutdo Remote Config. Acordamos rebatizar paracheckout-v2-eligibilityantes do próximo deploy.”Note B:
notes/firebase-stack.md, kindreference: “Remote Config é a ferramenta de feature-flags do Firebase. Os valores são entregues em tempo real para os clientes mobile.”Query (via Claude Code MCP, or
syn search): “Qual o nome da flag do Firebase do dia da minha reunião com a Carina?”
What it takes to answer this:
| Signal | Why it’s needed |
|---|---|
Temporal anchor (dia da reunião com a Carina) | Filter notes by meeting kind + Carina participant + date in the right month |
Concept bridge (Remote Config ↔ Firebase) | Note A doesn’t say “Firebase”; note B doesn’t say “Carina”. Without a bridge the two notes never co-occur |
| Specific extraction (the flag name) | Once both notes are surfaced, the LLM / CLI returns checkout-v2-eligibility from note A |
FTS5 alone, today, scores note B (literal match for “Firebase”) and maybe finds “Carina” in the participants index, but the link Remote Config → Firebase is invisible until both notes are seen together by the generation step: that means the LLM has to do the work, which means latency, cost, and non-determinism. v0.5’s job is to draw that edge once, when the user writes note B, so every future query that touches Firebase or Remote Config sees the link without paying for it again.
1.2 The new business rule BR-13: insertion-time enrichment
BR-13 (proposed for
docs/business/BUSINESS_RULES.md). Synapse processes entries at insertion time, not query time. Every CLI write (syn project add,syn person add,syn note add,capture_factvia MCP, …) hooks an enrichment pipeline that runs synchronously before the workspace transaction commits:
- Embed the entity’s
title + body + tagswith the configured embedding model; write the vector to theentities_vectable.- Extract candidate mentions: slugs of existing entities that textually appear in the new content, dates, person names with
@-prefix syntax, URLs, tag tokens, plus NER-detected spans (see §1.3 below for the language-per-workspace contract that drives the model choice).- Resolve mentions: every candidate is matched against the live workspace index. A hit becomes a typed
linkrow withrel = "mentions"orrel = "concept:<lemma>"; a miss becomes apendingproposal of kindlink_entity(existing) orcreate_entity_from_ner(new, when the NER pass discovered a span whose mapped Synapse entity type does not yet exist).- Latency budget: median ≤ 500 ms, p95 ≤ 800 ms on a 10k-entity workspace (M1 Pro reference). The pipeline runs the embedding and the NER passes in parallel inside the same
Workspace::txso the wall-clock cost ismax(embed, ner) + heuristic + link_resolve, not the sum. Background / async enrichment is explicitly out of scope for v0.5 and tracked as the v0.6 follow-up SYN-194.- Fail loud: if the enrichment step errors, the entity write aborts (atomicity preserved). The user can disable enrichment per workspace via
[enrichment] mode = "off"for the workspace that wants raw v0.4 semantics back.The principle: the IA is the memory co-pilot, not the real-time reranker. The work is paid once, on write.
This is the load-bearing decision of v0.5. Every other choice: vector backend, ranking algorithm, model size, benchmark target: flows from it.
1.3 Product principle: best models by default, per-workspace opt-down
Owner decision 2026-06-02: Synapse ships the strongest model in each slot by default. Smaller / English-only / faster variants are reachable through workspace config but the user does not have to know they exist to get a good first experience.
The corollary that matters for architecture: language is a per-workspace
property, not a per-binary one. A single Synapse install may host one
workspace where [enrichment] language = "pt-BR" and another where it is
"en-US"; both must run with equivalent retrieval quality and write
latency. The embedding backend and the NER model are selected per
workspace from a shared ~/.cache/synapse/models/ registry: the binary
itself stays language-agnostic.
The matrix this produces:
| Slot | Default (best) | Opt-down |
|---|---|---|
| Embedding | BGE-M3 multilingual (1024-dim, ~570 MB) | BGE-small-en-v1.5 Q (~33 MB, English only, faster): for workspaces that explicitly value binary footprint over recall |
| NER | GLiNER-multi-base Q (~150 MB) | GLiNER-base-en Q (~70 MB, English only): same trade-off |
| Vector storage | sqlite-vec (no choice: see §2.2) | - |
| Hybrid ranking | RRF k=60 (no choice for v0.5: see §2.3; convex combination is v0.6 polish) | - |
The configuration knob lives in <workspace>/.synapse/config.toml:
[enrichment]
language = "pt-BR" # or "en-US"
embedding_model = "bge-m3" # or "bge-small-en-v1.5-q"
ner_model = "gliner-multi" # or "gliner-base-en"
mode = "heuristic+ner" # or "heuristic" (skip NER), "off" (raw v0.4)
Defaults match the “best by default” row. The cascade config (user-level
~/.config/synapse/config.toml → workspace) is the same as for other
blocks (per ADR-011 §config policy).
2. Market scan: the four technical decisions in plain language
The brief uses the rest of the section to make the tooling choices defensible. Each subsection ends with a single line: What this means for Synapse.
2.1 Embedding backend: does it have to be Ollama?
In mid-2026 the realistic options for generating sentence embeddings from a Rust application are:
| Option | What it is | Cost on disk | Cost on first use | External deps |
|---|---|---|---|---|
| Ollama as runner | Synapse calls an Ollama HTTP server running locally; Ollama loads the embedding model. | ~700 MB Ollama binary + ~280 MB nomic-embed-text:v1.5 (q4) → ~1 GB minimum | User must install Ollama. First model download is ~280 MB. | Ollama running on localhost:11434. Synapse breaks if the user stops the daemon. |
fastembed-rs (wraps ort / ONNX Runtime) | Pure Rust crate; loads quantised ONNX models on demand; runs inference in-process on CPU. The model is downloaded from Hugging Face the first time it is used and then cached. | ~30 MB of ONNX Runtime statically linked + the model. BGESmallENV15Q is ~33 MB on disk. Total ~60–70 MB. | One-time ~33 MB model download. Cached under ~/.cache/fastembed/. | None at runtime. |
candle (HuggingFace pure-Rust ML) | Reads HF model weights directly (safetensors); no ONNX dependency. Maximum flexibility but slower per-query than ort. | ~10 MB crate + ~140 MB model (fp16). | Model download. | None at runtime. |
| Hand-rolled flat / HNSW + bag-of-words | No embeddings: return to BM25 + Levenshtein expansion. | 0 MB. | None. | None. |
The 2026 industry baseline for fast on-device embedding in a CPU-only Rust
application is fastembed-rs: Qdrant’s port of their original Python
library, in production at multiple Rust shops, supports the standard
embedding catalogue (BGE, Nomic, mxbai, MiniLM) including their quantised
ONNX variants ([fastembed-rs on crates.io], [Qdrant article on fastembed]).
The model the brief recommends as the v0.5 default: superseding the
original draft’s BGE-small-EN-v1.5 pick after the owner’s 2026-06-02
“best by default + per-workspace language” answer (§1.3): is BGE-M3
multilingual quantised: 1024-dimensional, ~570 MB on disk, MTEB
multilingual top-quartile (covers en + pt-BR + es + 100 + other
languages at near-state-of-the-art quality), embed latency ~80–200 ms
on M1 Pro CPU. For workspaces whose [enrichment] language = "en-US"
and that explicitly value binary footprint over recall, the
BGE-small-EN-v1.5-Q variant (~33 MB) is the opt-down: same trait
implementation, different model identifier passed to fastembed.
What this means for Synapse: Default fastembed-rs with BGE-M3
quantised, per-workspace selectable to BGE-small-EN-v1.5-Q. Ollama is a
configurable alternative ([embeddings] provider = "ollama") but not
the default. The local-first invariant survives. The binary itself stays
small; the model lives in ~/.cache/synapse/models/ and is downloaded on
first use of each workspace.
2.2 Vector storage: is sqlite-vec ready?
sqlite-vec is Alex Garcia’s loadable extension for SQLite that adds vector
columns, vec0 virtual tables, and L2 / cosine / dot distance functions in
~1.5 MB of native code. It is post-1.0, used in production, and Alex
Garcia maintains explicit Rust integration docs ([Using sqlite-vec in Rust]).
Two gotchas to plan for, both flagged in upstream issues and the rusqlite discussion threads ([rusqlite discussion #1423]):
rusqlitebundled+load_extensioninteraction. Bundled SQLite does not enable run-time loadable extensions by default, the user has to callconn.load_extension_enable()beforeload_extension(…), and on older SQLite versions thevtabfeature has to be enabled at therusqliteCargo level. The crate exposes the flag; documenting it in ADR-008 prevents a wasted afternoon.- No HNSW yet: flat scan only.
sqlite-veccurrently does brute-force scan; an HNSW-style index is on the roadmap but not landed. For a personal workspace of < 10k entities the linear scan is fast enough (~5 ms on a modern CPU). The v0.5 ceiling is set by this: if a user workspace exceeds ~50k entities, the recommendation is to chunk by workspace via federation rather than wait forsqlite-vecHNSW.
The cheap alternative is flat scan in pure Rust (Vec<f32> columns +
SIMD cosine): same time complexity, fewer abstractions, but reinvents the
storage layer. The expensive alternative is lance (Apache Arrow-based
columnar vector DB): full HNSW + ANN, but ~50 MB of code and a different
mental model from “everything lives in .synapse/index.db”.
What this means for Synapse: Adopt sqlite-vec. It keeps the
single-database invariant from BR-1. Document the load_extension setup in
ADR-008. Accept the flat-scan ceiling; revisit in v0.7 if a real user hits
it.
2.3 Hybrid ranking: RRF or convex combination, didactically
Once Synapse has three retrieval signals (FTS5, embeddings, entity-graph neighbourhood), the search command needs to fuse their results into a single ranked list. Two algorithms dominate the 2026 literature:
Reciprocal Rank Fusion (RRF)
The intuition. Each retrieval system independently ranks the documents. RRF ignores the scores (which are on incompatible scales: BM25 returns 0..∞ while cosine similarity returns -1..1) and uses only the ranks:
RRF_score(doc) = Σ_i 1 / (k + rank_i(doc))
rank_i(doc) is doc’s position in retriever i (1 if it’s the top hit, 2
if second, …). k = 60 is the standard tuning constant. A document that is
top-3 in BM25 and top-3 in vector search gets a high combined score; a
document that is top-1 in only one retriever still scores well but loses
to documents that show up in multiple.
Worked example: the Carina/Firebase query.
| Doc | FTS5 rank | Vector rank | Graph rank | RRF score (k=60) |
|---|---|---|---|---|
| Note B (Firebase reference) | 1 | 5 | 1 | 1/(60+1) + 1/(60+5) + 1/(60+1) = 0.049 |
| Note A (Carina meeting) | (not in top-10) | 2 | 2 | 0 + 1/(60+2) + 1/(60+2) = 0.032 |
| Random Firebase note | 2 | 12 | 25 | 1/(60+2) + 1/(60+12) + 1/(60+25) = 0.041 |
Both target notes surface in the top-3, even though FTS5 never saw note A and the random Firebase note ranked higher in literal text. RRF is safe out of the box and is the default of every major hybrid system shipped in 2026 (Elasticsearch, OpenSearch, Weaviate, Qdrant, [Hybrid search reference 2026]).
Convex Combination (a.k.a. score blending)
The intuition. Normalise each retriever’s score to [0,1], then take a weighted average:
score(doc) = α · vector_sim_normalised(doc) + (1-α) · bm25_normalised(doc)
α = 0.5 is the cold-start guess. The 2026 papers
([An Analysis of Fusion Functions for Hybrid Retrieval]) report convex
combination outperforms RRF when you have at least ~40 labelled query →
relevant-document pairs to tune α; below that, RRF wins because
normalisation breaks across queries.
The trade-off, sharply. RRF is the right cold-start default. Convex
combination is a polish step that requires the user to grade some search
results. A personal workspace owner can generate those labels (syn search foo --grade), and v0.6 should ship that. v0.5 does not.
What this means for Synapse: RRF with k = 60 for v0.5. Convex
combination becomes a v0.6 ticket gated on syn search --grade being shipped
first.
2.4 Entity linking at ingestion: small models, big payoff
The 2026 emerging best practice in retrieval pipelines is adaptive RAG: classify the query first, route simple factual queries to vector RAG and relationship queries to a knowledge-graph traversal ([GraphRAG vs Vector RAG 2026]). The Synapse twist, per BR-13, is to do the graph-building at ingestion time rather than wait for the query.
The choice of how the graph gets built drives binary size and latency:
| Approach | What it does at ingestion | Cost |
|---|---|---|
| Heuristic only (no model) | Scan the new entity’s text for: existing slugs (regex [a-z0-9-]+), ISO dates, @username mentions, URL hostnames, tag tokens. Match each one against the live index; emit pending proposals for confirmed concepts that need a new entity. | Free. ~5 ms per write. Catches the easy cases (e.g. the meeting note mentioning the literal slug firebase or remote-config). |
Small NER model (GLiNER-multi ONNX quantised, ~150 MB) | Run a lightweight zero-shot NER pass over the configured entity type list. Catches “Carina” without an @carina syntax, recognises “Remote Config” as a proper-noun span even when it doesn’t match an existing slug. | ~150 MB cached model. ~80–250 ms per write (inference). Parallelised with embedding so wall-clock cost is max(embed, ner). |
| LLM-driven extraction (call out to the user’s MCP client via sampling) | Best recall + best relation typing. | Network call per write. Breaks local-first. Rejected. |
What this means for Synapse: updated 2026-06-02 after owner decision:
v0.5 ships heuristic + GLiNER together as core. The original brief had
GLiNER as stretch; the owner promoted it on the rationale that (a)
pt-BR-grammatical content embeds proper nouns inside articles
("reunião com a Carina") which the heuristic-only path systematically
misses, and (b) the per-workspace language contract from §1.3 makes
GLiNER’s multilingual coverage a first-class need, not an optional one.
The promotion comes with three design specs that lock the budget:
- Model is
GLiNER-multi-basequantised (~150 MB) by default;GLiNER-base-enquantised (~70 MB) as the workspace opt-down per §1.3. - Embedding and NER run on parallel threads inside the same
Workspace::tx. Wall-clock cost ≈max(embed, ner). This is what keeps the BR-13 500 ms median budget reachable. - NER entity types are hard-coded by Synapse, not configured by
the user:
["person", "organization", "product", "feature_flag", "api_endpoint", "framework", "tool", "decision_topic"]. The mapping from NER type → Synapse entity type lives in the linker (SYN-91):person → person,api_endpoint → api,product | tool | framework | feature_flag | organization | decision_topic → entity. A surprise NER type out of this set is dropped; a new type requires a minor brief amendment, not a workspace config change.
The Carina case is now solved without any user discipline: even if the
user wrote "reunião com a Carina sobre a flag enable_v2_checkout do Remote Config" in raw pt-BR, NER tags Carina as person, Remote Config as product, enable_v2_checkout as feature_flag, and the
linker either creates the typed links or emits create_entity_from_ner
proposals to the pending queue.
The full-fledged GraphRAG with community summaries ([GraphRAG vs Vector RAG 2026]): 3–5× ingestion cost, requires LLM calls: remains out of scope. v0.5’s NER pass is the lightweight substitute; v0.6+ can evaluate whether the cost of community summaries pays off against the ingestion latency tax.
3. Cross-check vs the pre-research v0.5 backlog
The eleven issues already filed in milestone #7 are evaluated against the
brief’s recommendation:
| Open issue | Pre-research title | Status after brief | Reason |
|---|---|---|---|
| SYN-80 | feature flag vector + separate migration directory | Keep. | The migration story (migrations_vec/) is the right shape; vector becomes off-by-default for v0.5, default-on in v0.6. |
| SYN-81 | sqlite-vec integration: load_extension + entities_vec table | Keep, retitle to clarify the bundled+vtab setup. | Defaulted by §2.2. |
| SYN-82 | trait EmbeddingBackend (interface only) | Keep. | Right shape; the implementations come behind it. |
| SYN-83 | EmbeddingBackend implementations: Ollama, OpenAI, Voyage, configurable | Rewrite as “EmbeddingBackend implementations: fastembed-rs with BGE-M3 default + BGE-small-EN-v1.5-Q opt-down (per workspace); Ollama configurable”. OpenAI/Voyage move to stretch (SYN-95). | Local-first invariant per owner; per-workspace language contract from §1.3. |
| SYN-84 | EmbeddingBackend stub: deterministic, no network | Keep. | Needed for unit tests; deterministic SipHash-based fake vector. |
| SYN-85 | syn embed --all and syn embed <slug> | Keep, expand to also expose syn embed --check for verifying index integrity. | |
| SYN-86 | hybrid ranking: FTS5 + vector via RRF | Keep, finalise k=60. | Defaulted by §2.3. |
| SYN-87 | syn search --semantic (vector-only mode) | Reframe. v0.5 default = hybrid. --semantic becomes an explicit “vector-only, no FTS” debugging mode. --no-vector is the inverse. | The two existing tickets were inconsistent; this resolves the §4 open question of the closure doc. |
| SYN-88 | MCP tool semantic_search mirrors syn search --semantic | Reframe. The MCP tool stays search_knowledge (already exists); the hybrid mode is internal. semantic_search becomes a separate tool only if a client benefits from the vector-only mode; provisional kept as stretch. | |
| SYN-89 | ADR-008: chosen default embedding backend after benchmark | Keep. Brief mandates ADR-008 lands before SYN-83 code. | Governance. |
| SYN-93b | workspace freshness Phase 2 (network ls-refs via gix-protocol) | Keep. Independent of the semantic stack; lands as a parallel track. | Originally deferred from v0.2. |
4. Architectural debt cleanup: non-negotiable, lands first
These four tickets close the debts the v0.4 closure doc surfaced. They are core, not stretch. They lock the architecture before the semantic layer piles new modules on top:
- SYN-151:
mcp/server.rsdispatcher refactor. Move every#[tool]handler to a thin shim (3–8 lines) that delegates to a use-case module undersrc/core/mcp/(orsrc/mcp/handlers/). Acceptance: file drops below 500 lines and stays there; a CI lint rejects regrowth above 600 lines. ADR-015 §Playbook updated with the dispatcher pattern. - SYN-152:
cli/at.rssplit. Break the 620-line file intocli/at/{mod,search,show,list,info,graph}.rs. Pure code move, no behaviour change. - SYN-153:
cli/audit.rssplit. Same pattern:cli/audit/{listing,verify,replay,stale,prune}.rs. - SYN-154: file-size CI lint. A 30-line script in
.githooks/pre-pushor a GitHub Actions step: walksrc/, fail when any file exceeds 800 lines, warn at 600. ADR-015 enforcement.
5. The semantic layer: execution plan
The semantic layer ships as a sequenced track. Order matters because each ticket depends on a primitive established by the previous one.
SYN-89 (ADR-008)
↓
SYN-80 (feature flag vector + migrations_vec/)
↓
SYN-81 (sqlite-vec wired; entities_vec table; bundled+vtab+load_extension)
↓
SYN-82 (trait EmbeddingBackend)
↓
SYN-84 (deterministic stub) ← unblocks unit tests
↓
SYN-83 (fastembed-rs impl; Ollama impl behind config)
↓
SYN-85 (syn embed --all / --check / <slug>)
↓ ↓
SYN-86 (hybrid RRF) SYN-90 (BR-13 enrichment pipeline) ← writes hook this
↓ ↓
SYN-87 (syn search SYN-91 (heuristic entity-linker:
modes & flags) slugs + dates + @mentions + URLs)
↓ ↓
SYN-92 (benchmark suite: corpus generator + queries + metrics)
↓
SYN-93 (results report + thresholds for nDCG/P@k/MRR/latency)
Six new tickets join the eleven already-filed:
- SYN-90: pipeline:
core::enrichment::run_on_write(ws, entity)called by every write helper beforetx.commit(). Honours[enrichment] mode = "off" | "heuristic" | "ner". BR-13 codified. - SYN-91: heuristic linker module. No model dep. Documented detection
patterns. Emits
pendingproposals when a candidate is ambiguous (two slugs share a basename). - SYN-92: benchmark corpus + query bank. Generator produces a
synthetic workspace of 1k / 5k / 10k entities with planted cross-note
relations (the Carina/Firebase template generalised). Query bank with
ground-truth
relevant_slugsper query. - SYN-93: benchmark runner + results doc. CI artifact:
bench/results-<date>.json.
Stretch (lands if window allows, otherwise v0.6):
- SYN-94:
GLiNERNER backend behind--features ner. Comparable to the heuristic on the bench corpus. - SYN-95:
EmbeddingBackend::OpenAIandVoyageimpls (network-based, behind explicit opt-in). - SYN-96:
syn search --gradeto label query→relevant pairs for the v0.6 convex-combination polish. - SYN-97: MCP
semantic_searchtool, only if SYN-88 finds a client use case the unifiedsearch_knowledgecannot serve.
6. The benchmark suite: what “strong” means here, concretely
A benchmark suite for v0.5 has to do three things at once: produce a corpus that is representative of a Synapse user’s data, encode queries that are hard in the way the Carina example is hard, and report metrics that force a decision when algorithms compete.
6.1 Corpus generator (SYN-92)
The generator outputs a workspace as .synapse/index.db + notes/,
projects/, people/, apis/, entities/, tasks/. Three sizes (1k / 5k
/ 10k entities). Each generated workspace has a planted entity backbone:
- 5 fake “stacks” (e.g.
firebase-stack,aws-stack,vercel-stack,notion-stack,langchain-stack): each represented by anotes/<stack>.mdreference note that names 3–5 sub-tools (Firebase: Remote Config, Firestore, Auth, Functions, Storage). - 30–60 fake “meetings” with participants drawn from a pool of 12 fake people. Each meeting note mentions a sub-tool by name (without naming the parent stack) and proposes a flag rename / config change / API call / decision.
- 20–40 fake “decisions” (note kind
decision) referring back to a meeting and a stack sub-tool.
The cross-reference pattern is the Carina/Firebase pattern at scale. A query like “flag do Firebase do dia da reunião com Maria-Eduarda” has exactly one correct answer in the planted corpus; the benchmark measures whether the retrieval system finds it.
6.2 Query bank
50 queries per corpus, three difficulty tiers:
- T1: literal. “Carina” with no concept bridging: should be P@1 = 1.0 for everything (sanity check the FTS5 fallback).
- T2: single-hop concept (the Carina/Firebase case). The query names one concept (Firebase) and one anchor (a meeting participant or date); the correct answer is in a note that does not contain the concept word literally, only the sub-tool word. This is where RRF + embeddings + heuristic linking must beat plain FTS.
- T3: multi-hop / temporal. “What did we decide about retention in any AWS service in Q2?”: combines a temporal anchor with a parent-stack bridge with a decision-kind filter. RRF + embedding should surface the AWS-stack note + the relevant decision note; full GraphRAG would do better but is out of scope.
6.3 Metrics & thresholds
| Metric | Definition | v0.5 ship threshold |
|---|---|---|
| P@1 | Top-1 hit is in the ground-truth set | T1 = 1.0, T2 ≥ 0.6, T3 ≥ 0.3 |
| P@5 | Any of top-5 is correct | T1 = 1.0, T2 ≥ 0.85, T3 ≥ 0.6 |
| MRR | Mean reciprocal rank over the 50 queries | overall ≥ 0.55 |
| nDCG@10 | Normalised discounted cumulative gain | overall ≥ 0.6 |
| Write latency | Time for syn project add (median, 10k corpus, enrichment + NER on, default models) | ≤ 500 ms median, ≤ 800 ms p95 (revised 2026-06-02 from the original 200 ms target after the owner accepted the GLiNER core promotion + BGE-M3 default; the parallel embed+NER pipeline is what makes 500 ms reachable) |
| Search latency | Time for hybrid search (median, 10k corpus) | ≤ 150 ms |
| Binary size delta | target/release/syn size vs v0.4 baseline (binary only, models cached separately under ~/.cache/synapse/models/) | ≤ +60 MB |
| Cold-start model download | Total bytes fetched on first syn invocation in a fresh workspace with default models | ≤ 750 MB (BGE-M3 ~570 MB + GLiNER-multi-Q ~150 MB; visible in syn init output) |
Thresholds are codified in SYN-93’s acceptance: if a metric is missed, the v0.5 brief gets reopened.
7. Footprint, honestly: what the user installs
Sizes measured on aarch64-apple-darwin static release builds, refreshed
2026-06-02 after the BGE-M3 + GLiNER-multi defaults were locked.
| Build | Binary on disk | Models cached on first use | When |
|---|---|---|---|
syn (v0.4) | ~14 MB | none | today |
syn --features "mcp lsp vector" (v0.5 default, default models) | ~60 MB | ~570 MB BGE-M3 + ~150 MB GLiNER-multi = ~720 MB in ~/.cache/synapse/models/ | v0.5 ship |
Same binary, opt-down to English-only (workspace config: bge-small-en-v1.5-q + gliner-base-en) | ~60 MB | ~33 MB BGE-small + ~70 MB GLiNER-en = ~103 MB | per-workspace choice |
syn with Ollama instead of fastembed | ~14 MB binary + ~1 GB external (Ollama daemon + model) | requires Ollama daemon | configured opt-in |
The deal the user signs up for by accepting the default install:
- Binary itself stays ≤ 60 MB (versus 14 MB today, +46 MB for the ONNX Runtime statically linked plus the fastembed glue plus GLiNER plumbing).
- First
syninvocation in a fresh workspace fetches ~720 MB of models to~/.cache/synapse/models/. The cache is shared across workspaces of the same language config, so a user with three pt-BR workspaces pays the 720 MB once, not three times. - The English-only opt-down (target: low-resource laptops, CI workers) drops the cache to ~100 MB at the cost of multilingual coverage.
The Ollama-equivalent still costs 14× more on disk and adds a daemon dependency the user has to manage. The local-first invariant remains.
8. Risk register
| Risk | Severity | Mitigation |
|---|---|---|
sqlite-vec load_extension setup proves harder than expected on Windows MSVC | Medium | Spike before SYN-81; if blocking, fall back to a pure-Rust flat scan (slower path, same SQL surface) so the rest of the stack is unblocked |
| Quantised BGE-small accuracy is too low on Portuguese content (T2 threshold missed) | Medium | The brief reserves BGE-M3 as the multilingual swap behind [embeddings] model = "bge-m3"; the bench corpus generator includes pt-BR variant prompts |
Insertion-time enrichment introduces a write-latency regression that hurts syn capture UX | High | BR-13 already mandates the enrichment runs inside the workspace transaction; the 200 ms write threshold is the brief’s ship gate. If exceeded, enrichment moves async with a 5-second eventual-consistency window: explicitly out of scope for v0.5 ship |
| Benchmark numbers reveal RRF underperforms convex combination even at α=0.5 | Low | The brief documents this explicitly; SYN-96 (convex combination + --grade) is the v0.6 polish path |
Re-split of mcp/server.rs (SYN-151) breaks an rmcp macro contract | Medium | The dispatcher pattern keeps every #[tool] shim inside the macro’s impl block; the move is the body of each shim into a free function. Smoke tests against Claude Code stay the verification gate |
| Owner runs out of context-window mid-v0.5; the cycle drags into v0.6 | Medium | The brief breaks v0.5 into the 4 arch-debt tickets (SYN-151..154) shippable independently and the 17 semantic-layer tickets sequenceable; the architecturally hardest decisions (ADR-008 spike) front-loaded |
9. Ticket sketch: provisional numbering
This is the proposed full v0.5 milestone after the brief lands. Provisional
numbers in SYN-150 range expand into the unused SYN-151..199 block;
existing SYN-80..89 + SYN-93b are kept with the retitles from §3. The
new tickets fill the v0.2-adjacent free slots SYN-94..98 plus the
expansion block.
9.1 Architecture-debt cleanup (lands first)
| ID | Layer | Size | Title |
|---|---|---|---|
| SYN-151 | mcp | M | refactor mcp/server.rs to dispatcher pattern (thin #[tool] shims, logic moves to core::mcp::*) |
| SYN-152 | cli | S | split cli/at.rs into cli/at/{mod,search,show,list,info,graph}.rs |
| SYN-153 | cli | S | split cli/audit.rs into cli/audit/{listing,verify,replay,stale,prune}.rs |
| SYN-154 | ci | XS | CI lint rejecting src/** files above 800 lines (red), warning at 600 (yellow): ADR-015 enforcement |
9.2 Semantic layer: core (must land)
| ID | Layer | Size | Title |
|---|---|---|---|
| SYN-89 | docs | M | ADR-008: chosen default embedding backend after benchmark spike (default: fastembed-rs + BGE-M3 multilingual quantised; opt-down: BGE-small-EN-v1.5-Q; both per-workspace per §1.3) |
| SYN-80 | index/build | M | feature flag vector + separate migrations_vec/ migration dir |
| SYN-81 | index | L | sqlite-vec integration via bundled+vtab+load_extension; entities_vec virtual table; ADR-008 §gotchas |
| SYN-82 | core | M | trait EmbeddingBackend { fn embed(text: &str) -> Result<Vec<f32>> } + trait NerBackend { fn extract(text: &str, types: &[&str]) -> Result<Vec<NerSpan>> } (the parallel-pipeline contract is one trait per slot) |
| SYN-84 | core / tests | S | deterministic EmbeddingBackend + NerBackend stubs (SipHash-based fake vector, regex-based fake NER) for unit tests |
| SYN-83 | core | M | EmbeddingBackend impls: fastembed-rs with BGE-M3 default + BGE-small-EN-v1.5-Q opt-down per [enrichment] embedding_model; Ollama configurable |
| SYN-164 | core | M | NerBackend impl: GLiNER-multi-base-Q default + GLiNER-base-en-Q opt-down per [enrichment] ner_model. Promoted from stretch to core 2026-06-02. Hard-coded entity types per §2.4 spec. |
| SYN-85 | cli | M | syn embed --all / syn embed <slug> / syn embed --check / syn embed --reset-cache |
| SYN-160 | core | L | enrichment pipeline core::enrichment::run_on_write invoked by every write helper; runs embedding + NER in parallel threads inside the same Workspace::tx; honours [enrichment] mode; BR-13 codified. Owns the median-500 ms / p95-800 ms latency budget. |
| SYN-161 | core | M | heuristic entity-linker + NER-span resolver: slug / date / @mention / URL / tag patterns plus the GLiNER-typed spans from SYN-164 → matched against the live index → either typed link rows or pending proposals (link_entity + new create_entity_from_ner) |
| SYN-86 | core / index | L | hybrid ranking via RRF with k = 60 over FTS + vector + graph-neighbourhood signals |
| SYN-87 | cli | S | syn search default = hybrid; --semantic = vector-only; --no-vector = FTS+graph only |
| SYN-162 | core / tests | L | benchmark corpus generator (1k / 5k / 10k) + query bank (50 queries × 3 difficulty tiers): pt-BR + en-US variants, since the language contract from §1.3 mandates equivalent quality. |
| SYN-163 | core / tests / docs | M | benchmark runner + results report; CI artefact; thresholds enforce v0.5 ship gate (T2 P@5 ≥ 0.85, MRR ≥ 0.55, write median ≤ 500 ms, search median ≤ 150 ms) |
| SYN-167 | docs / cli | S | README §Editor integration section promoting syn lsp near the top + cross-link to docs/EDITOR-INTEGRATION.md. Owner decision 2026-06-02: lands inside v0.5 cycle (not as a parallel v0.4.1). Homebrew tap explicitly out of v0.5; asciinema demo registered for v0.5.1. |
9.3 Stretch (lands if window allows; otherwise rolls to v0.6)
| ID | Layer | Size | Title |
|---|---|---|---|
| SYN-165 | core | M | EmbeddingBackend impls for OpenAI + Voyage (network-based, behind explicit opt-in) |
| SYN-166 | cli / core | S | syn search --grade UX for capturing query → relevant labels; foundation for v0.6 convex-combination tuning |
| SYN-88 | mcp | S | MCP semantic_search tool: only if a client benefits over search_knowledge’s hybrid mode |
| SYN-194 | core | L | (v0.6 follow-up: registered explicitly so it does not get lost) async / background enrichment with eventual-consistency window for users who want to remove the 500 ms write penalty entirely |
9.4 Network freshness Phase 2: parallel track
| ID | Layer | Size | Title |
|---|---|---|---|
| SYN-93b | store / mcp | L | workspace freshness Phase 2: network ls-refs via gix-protocol, ADR-011 §Phase 2 closure |
9.5 New BR + ADR
- BR-13: insertion-time enrichment principle. Landing in
docs/business/BUSINESS_RULES.mdas part of SYN-90. - ADR-008: embedding backend choice + sqlite-vec setup notes. Landing as part of SYN-89 before SYN-81 code starts.
10. Cuts assumed (registered explicitly)
syn agentautonomous loop with batch approval: kept at v0.6, owner decision from 2026-05-25 carried.- Microsoft-style GraphRAG with community summaries: too expensive (3–5× ingestion cost); v0.6+ or v0.7. Lightweight heuristic + optional GLiNER is the v0.5 substitute.
syn cv export --as pdf: already cut from v0.4; still v0.5+1 stretch or later.- Web UI / TUI: backlog.
- Cross-bundle merge of identity bundles: kept at v0.6+, ADR-014/016 cuts carried.
- Sigstore transparency log for identity: kept at v0.5+ per ADR-016 §Alternatives considered, lands when there is a public exchange to protect.
11. Decisions log
Decided 2026-06-02
- ✅ Default embedding model:
BGE-M3multilingual. Owner answered the §2.1 question explicitly: ship the strongest model by default;BGE-small-EN-v1.5-Qis the per-workspace opt-down. Combined with the new product principle from §1.3, the binary itself stays language-agnostic; the model is selected per workspace. - ✅ GLiNER promoted from stretch to core. Owner answered the §2.4
question with option B:
GLiNER-multi-base-Qdefault,GLiNER-base-en-Qopt-down per workspace. Hard-coded entity types, parallel embed+NER pipeline insideWorkspace::tx, three mitigations from §2.4 locked as design specs. SYN-94 moved from §9.3 to §9.2; was retitled accordingly.
- ✅ BR-13 latency budget: write median ≤ 500 ms, p95 ≤ 800 ms. Owner raised the original 200 ms ceiling explicitly to make room for the GLiNER pass; async / background enrichment registered as the v0.6 follow-up (SYN-194) so the trade-off is not lost.
- ✅ Per-workspace language contract: owner declared the product
is not pt-BR-primary; each workspace’s
.synapse/config.tomlpicks itslanguage = "pt-BR" | "en-US", and both must perform equivalently. Codified as §1.3 product principle; benchmark mandates pt-BR + en-US query variants in SYN-92.
Decided 2026-06-02 (second pass)
- ✅
syn searchsyntax confirmed. Default = hybrid (FTS + vector + graph via RRF).--semantic= vector-only mode.--no-vector= FTS + graph only. Codified in §2.3, §3 (SYN-87 retitle), §9.2. - ✅ Ticket numbering: SYN-160..167 expansion block. Owner picked
the safer remapping over reusing the v0.2-adjacent slots. Final
allocation:
SYN-160enrichment pipeline (BR-13)SYN-161heuristic + NER linkerSYN-162benchmark corpus + queriesSYN-163benchmark runner + reportSYN-164NerBackend impl (GLiNER multi default + en-only opt-down)SYN-165(stretch) OpenAI + Voyage EmbeddingBackend implsSYN-166(stretch)syn search --gradeSYN-167README §Editor integration adoption section- Plus the architecture-debt block already at
SYN-151..154.
- ✅ README §Editor integration lands inside v0.5 as SYN-167.
Owner explicitly rejected the v0.4.1 parallel-cycle idea. Homebrew
tap is out of v0.5 entirely (decision carried to v1.0 backlog
SYN-102). Asciinema demo of
syn lspregistered for v0.5.1, not in this cycle but mapped. - ✅ Six-week window accepted as the upper bound, owner expects earlier delivery.
12. References
- fastembed-rs on crates.io: Rust embedding generation, ONNX-backed.
- Qdrant’s FastEmbed article: design rationale and CPU-first benchmarks.
- Using sqlite-vec in Rust: Alex Garcia: the integration walkthrough.
- Hybrid Search: BM25, Vector & Reranking Reference 2026: 2026 industry baseline.
- An Analysis of Fusion Functions for Hybrid Retrieval, the original convex-combination vs RRF analysis carried forward in 2026 work.
- GraphRAG vs Vector RAG: Knowledge Graph AI Guide 2026, the adaptive RAG framing the brief borrows.
- Best Embedding Model for RAG 2026: Milvus blog: model size + MTEB comparison feeding §2.1.
- Entity Linking and Relationship Extraction With Relik in LlamaIndex: Neo4j Developer blog: small-model NER at ingestion baseline.
- rusqlite discussion #1423:
modern_sqlite+vtabwith loadable extensions: the sqlite-vec gotcha called out in §2.2. - v0.4 closure read:
docs/researchs/2026-06-02-v0.4-closure-and-roadmap-review.md. - v0.4 brief:
docs/researchs/2026-05-25-v0.4-refinement-and-extensibility.md: pattern this brief follows. - ADR-013: audit trail (still binds the insertion-time pipeline).
- ADR-015: modular files policy (enforced by SYN-154 lint).
- ADR-018:
tower-lsp-serverchoice (precedent for picking active forks over abandoned upstreams).