Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

v0.5: Semantic layer & second-brain enrichment: make the graph think like the engineer

FieldValue
Date finalised2026-06-02
AuthorRô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)
ScopeRoadmap 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).
Statusfully 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
Inputsv0.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 scopesyn 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, kind meeting: “Reunião com Carina sobre a flag enable_v2_checkout do Remote Config. Acordamos rebatizar para checkout-v2-eligibility antes do próximo deploy.”

Note B: notes/firebase-stack.md, kind reference: “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:

SignalWhy 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_fact via MCP, …) hooks an enrichment pipeline that runs synchronously before the workspace transaction commits:

  1. Embed the entity’s title + body + tags with the configured embedding model; write the vector to the entities_vec table.
  2. 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).
  3. Resolve mentions: every candidate is matched against the live workspace index. A hit becomes a typed link row with rel = "mentions" or rel = "concept:<lemma>"; a miss becomes a pending proposal of kind link_entity (existing) or create_entity_from_ner (new, when the NER pass discovered a span whose mapped Synapse entity type does not yet exist).
  4. 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::tx so the wall-clock cost is max(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.
  5. 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:

SlotDefault (best)Opt-down
EmbeddingBGE-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
NERGLiNER-multi-base Q (~150 MB)GLiNER-base-en Q (~70 MB, English only): same trade-off
Vector storagesqlite-vec (no choice: see §2.2)-
Hybrid rankingRRF 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:

OptionWhat it isCost on diskCost on first useExternal deps
Ollama as runnerSynapse 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 minimumUser 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-wordsNo 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]):

  1. rusqlite bundled + load_extension interaction. Bundled SQLite does not enable run-time loadable extensions by default, the user has to call conn.load_extension_enable() before load_extension(…), and on older SQLite versions the vtab feature has to be enabled at the rusqlite Cargo level. The crate exposes the flag; documenting it in ADR-008 prevents a wasted afternoon.
  2. No HNSW yet: flat scan only. sqlite-vec currently 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 for sqlite-vec HNSW.

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.

DocFTS5 rankVector rankGraph rankRRF score (k=60)
Note B (Firebase reference)1511/(60+1) + 1/(60+5) + 1/(60+1) = 0.049
Note A (Carina meeting)(not in top-10)220 + 1/(60+2) + 1/(60+2) = 0.032
Random Firebase note212251/(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:

ApproachWhat it does at ingestionCost
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:

  1. Model is GLiNER-multi-base quantised (~150 MB) by default; GLiNER-base-en quantised (~70 MB) as the workspace opt-down per §1.3.
  2. 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.
  3. 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 issuePre-research titleStatus after briefReason
SYN-80feature flag vector + separate migration directoryKeep.The migration story (migrations_vec/) is the right shape; vector becomes off-by-default for v0.5, default-on in v0.6.
SYN-81sqlite-vec integration: load_extension + entities_vec tableKeep, retitle to clarify the bundled+vtab setup.Defaulted by §2.2.
SYN-82trait EmbeddingBackend (interface only)Keep.Right shape; the implementations come behind it.
SYN-83EmbeddingBackend implementations: Ollama, OpenAI, Voyage, configurableRewrite 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-84EmbeddingBackend stub: deterministic, no networkKeep.Needed for unit tests; deterministic SipHash-based fake vector.
SYN-85syn embed --all and syn embed <slug>Keep, expand to also expose syn embed --check for verifying index integrity.
SYN-86hybrid ranking: FTS5 + vector via RRFKeep, finalise k=60.Defaulted by §2.3.
SYN-87syn 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-88MCP tool semantic_search mirrors syn search --semanticReframe. 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-89ADR-008: chosen default embedding backend after benchmarkKeep. Brief mandates ADR-008 lands before SYN-83 code.Governance.
SYN-93bworkspace 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.rs dispatcher refactor. Move every #[tool] handler to a thin shim (3–8 lines) that delegates to a use-case module under src/core/mcp/ (or src/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.rs split. Break the 620-line file into cli/at/{mod,search,show,list,info,graph}.rs. Pure code move, no behaviour change.
  • SYN-153: cli/audit.rs split. Same pattern: cli/audit/{listing,verify,replay,stale,prune}.rs.
  • SYN-154: file-size CI lint. A 30-line script in .githooks/pre-push or a GitHub Actions step: walk src/, 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 before tx.commit(). Honours [enrichment] mode = "off" | "heuristic" | "ner". BR-13 codified.
  • SYN-91: heuristic linker module. No model dep. Documented detection patterns. Emits pending proposals 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_slugs per query.
  • SYN-93: benchmark runner + results doc. CI artifact: bench/results-<date>.json.

Stretch (lands if window allows, otherwise v0.6):

  • SYN-94: GLiNER NER backend behind --features ner. Comparable to the heuristic on the bench corpus.
  • SYN-95: EmbeddingBackend::OpenAI and Voyage impls (network-based, behind explicit opt-in).
  • SYN-96: syn search --grade to label query→relevant pairs for the v0.6 convex-combination polish.
  • SYN-97: MCP semantic_search tool, only if SYN-88 finds a client use case the unified search_knowledge cannot 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 a notes/<stack>.md reference 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

MetricDefinitionv0.5 ship threshold
P@1Top-1 hit is in the ground-truth setT1 = 1.0, T2 ≥ 0.6, T3 ≥ 0.3
P@5Any of top-5 is correctT1 = 1.0, T2 ≥ 0.85, T3 ≥ 0.6
MRRMean reciprocal rank over the 50 queriesoverall ≥ 0.55
nDCG@10Normalised discounted cumulative gainoverall ≥ 0.6
Write latencyTime 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 latencyTime for hybrid search (median, 10k corpus)≤ 150 ms
Binary size deltatarget/release/syn size vs v0.4 baseline (binary only, models cached separately under ~/.cache/synapse/models/)≤ +60 MB
Cold-start model downloadTotal 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.

BuildBinary on diskModels cached on first useWhen
syn (v0.4)~14 MBnonetoday
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 MBper-workspace choice
syn with Ollama instead of fastembed~14 MB binary + ~1 GB external (Ollama daemon + model)requires Ollama daemonconfigured 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 syn invocation 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

RiskSeverityMitigation
sqlite-vec load_extension setup proves harder than expected on Windows MSVCMediumSpike 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)MediumThe 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 UXHighBR-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.5LowThe 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 contractMediumThe 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.6MediumThe 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)

IDLayerSizeTitle
SYN-151mcpMrefactor mcp/server.rs to dispatcher pattern (thin #[tool] shims, logic moves to core::mcp::*)
SYN-152cliSsplit cli/at.rs into cli/at/{mod,search,show,list,info,graph}.rs
SYN-153cliSsplit cli/audit.rs into cli/audit/{listing,verify,replay,stale,prune}.rs
SYN-154ciXSCI lint rejecting src/** files above 800 lines (red), warning at 600 (yellow): ADR-015 enforcement

9.2 Semantic layer: core (must land)

IDLayerSizeTitle
SYN-89docsMADR-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-80index/buildMfeature flag vector + separate migrations_vec/ migration dir
SYN-81indexLsqlite-vec integration via bundled+vtab+load_extension; entities_vec virtual table; ADR-008 §gotchas
SYN-82coreMtrait 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-84core / testsSdeterministic EmbeddingBackend + NerBackend stubs (SipHash-based fake vector, regex-based fake NER) for unit tests
SYN-83coreMEmbeddingBackend impls: fastembed-rs with BGE-M3 default + BGE-small-EN-v1.5-Q opt-down per [enrichment] embedding_model; Ollama configurable
SYN-164coreMNerBackend 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-85cliMsyn embed --all / syn embed <slug> / syn embed --check / syn embed --reset-cache
SYN-160coreLenrichment 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-161coreMheuristic 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-86core / indexLhybrid ranking via RRF with k = 60 over FTS + vector + graph-neighbourhood signals
SYN-87cliSsyn search default = hybrid; --semantic = vector-only; --no-vector = FTS+graph only
SYN-162core / testsLbenchmark 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-163core / tests / docsMbenchmark 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-167docs / cliSREADME §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)

IDLayerSizeTitle
SYN-165coreMEmbeddingBackend impls for OpenAI + Voyage (network-based, behind explicit opt-in)
SYN-166cli / coreSsyn search --grade UX for capturing query → relevant labels; foundation for v0.6 convex-combination tuning
SYN-88mcpSMCP semantic_search tool: only if a client benefits over search_knowledge’s hybrid mode
SYN-194coreL(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

IDLayerSizeTitle
SYN-93bstore / mcpLworkspace 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.md as 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 agent autonomous 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

  1. ✅ Default embedding model: BGE-M3 multilingual. Owner answered the §2.1 question explicitly: ship the strongest model by default; BGE-small-EN-v1.5-Q is 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.
  2. ✅ GLiNER promoted from stretch to core. Owner answered the §2.4 question with option B: GLiNER-multi-base-Q default, GLiNER-base-en-Q opt-down per workspace. Hard-coded entity types, parallel embed+NER pipeline inside Workspace::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.toml picks its language = "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)

  1. ✅ syn search syntax 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.
  2. ✅ Ticket numbering: SYN-160..167 expansion block. Owner picked the safer remapping over reusing the v0.2-adjacent slots. Final allocation:
    • SYN-160 enrichment pipeline (BR-13)
    • SYN-161 heuristic + NER linker
    • SYN-162 benchmark corpus + queries
    • SYN-163 benchmark runner + report
    • SYN-164 NerBackend impl (GLiNER multi default + en-only opt-down)
    • SYN-165 (stretch) OpenAI + Voyage EmbeddingBackend impls
    • SYN-166 (stretch) syn search --grade
    • SYN-167 README §Editor integration adoption section
    • Plus the architecture-debt block already at SYN-151..154.
  3. ✅ 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 lsp registered for v0.5.1, not in this cycle but mapped.
  4. ✅ Six-week window accepted as the upper bound, owner expects earlier delivery.

12. References