GNO Architecture Notes

Reviewed at commit: 5ea263f

Internal reference for maintainers. See spec/ for interface contracts.

1. Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                              CLI Layer                                   β”‚
β”‚   (index, search, vsearch, query, ask, embed, get, status, cleanup)     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                    β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Config Layer    β”‚      Pipeline Layer           β”‚    LLM Layer        β”‚
β”‚  (config/types)   β”‚  (hybrid, search, vsearch)    β”‚  (embed, rerank,    β”‚
β”‚                   β”‚  (fusion, rerank, expansion)  β”‚   gen, llama.cpp)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                    β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                          Ingestion Layer                                 β”‚
β”‚   walker β†’ converter β†’ canonicalize β†’ chunker β†’ store                   β”‚
β”‚   (bun:glob)  (registry)   (NFC+hash)  (800tok)   (sqlite)             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                    β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                          Storage Layer                                   β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
β”‚   β”‚  SQLite     β”‚  β”‚  FTS5        β”‚  β”‚  sqlite-vec  β”‚                   β”‚
β”‚   β”‚ (bun:sqlite)β”‚  β”‚  (bm25)      β”‚  β”‚  (optional)  β”‚                   β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
β”‚        documents, content, content_chunks, content_vectors              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Abstractions (Port/Adapter Pattern)

  • StorePort - SQLite adapter (migrations, collections, documents, chunks, FTS)
  • VectorIndexPort - sqlite-vec adapter (per-model vec tables, graceful degradation)
  • EmbeddingPort - llama.cpp GGUF embeddings
  • RerankPort - llama.cpp GGUF cross-encoder
  • ChunkerPort - Character-based markdown chunking
  • ConverterPort - MIMEβ†’Markdown conversion (registry, first-match wins)

2. Data Flow (Ingestion)

File on disk
    β”‚
    β–Ό walker (glob patterns, exclude list)
WalkEntry { absPath, relPath, size, mtime }
    β”‚
    β–Ό hash source content (SHA-256 β†’ sourceHash)
    β”‚
    β”œβ”€[ sourceHash unchanged ]─► skip (unchanged)
    β”‚
    β–Ό converter registry (MIME detection β†’ first matching converter)
ConvertOutput { markdown, title, languageHint, meta }
    β”‚
    β–Ό canonicalize (NFC, BOM strip, control chars, collapse blanks)
    β”‚
    β–Ό mirrorHash = SHA-256(canonical markdown)
    β”‚
    β”œβ”€[ mirrorHash exists ]─► reuse existing content (content-addressed)
    β”‚
    β–Ό chunker (800 tokens * 4 chars = 3200 max, 15% overlap)
ChunkOutput[] { seq, pos, text, startLine, endLine, language }
    β”‚
    β–Ό store (upsertDocument, upsertContent, upsertChunks, rebuildFts)

Content Addressing: Multiple documents can share the same mirror_hash if their canonical markdown is identical. This deduplicates storage and FTS indexing.

User query
    β”‚
    β–Ό detectQueryLanguage (franc, 30+ languages, β‰₯15 chars)
    β”‚
    β”œβ”€[ BM25-only mode (no sqlite-vec) ]─► searchBm25 only
    β”‚
    β–Ό searchBm25 (FTS5 + escapeFts5Query)
    β”‚
    β–Ό embedQuery (llama.cpp)
    β”‚
    β–Ό searchVector (sqlite-vec knn)
    β”‚
    β–Ό [optional] query expansion (LLM generates variants)
    β”‚   └─► additional BM25 + vector searches
    β”‚
    β–Ό RRF fusion (reciprocal rank, k=60)
    β”‚   └─► bm25Weight=1.0, vecWeight=1.0
    β”‚   └─► topRankBonus=0.1 if both top-5
    β”‚
    β–Ό [optional] rerank (cross-encoder)
    β”‚   └─► position-aware blending schedule
    β”‚
    β–Ό Final results (sorted by blendedScore)

4. Score Semantics

IMPORTANT: Scores are normalized per-query and NOT comparable across queries.

BM25 (src/pipeline/search.ts:29-52)

Raw BM25: smaller (more negative) = better match
Normalization: min-max scaling to [0,1]
  normalized = (worst - raw) / (worst - best)
  where worst = max(scores), best = min(scores)
  Edge case: if all scores equal (best === worst), all get 1.0
Result: 1.0 = best match in result set, 0.0 = worst

Vector (src/pipeline/vsearch.ts:26-28)

Cosine distance: 0 = identical, 2 = opposite
Similarity = 1 - (distance / 2)
Result: [0,1] where 1.0 = identical vectors

RRF Fusion (src/pipeline/fusion.ts)

RRF score = Ξ£ weight / (k + rank)
  k = 60 (default)
  bm25Weight = 1.0
  vecWeight = 1.0
  variant weights (fusion.ts:66-106): bm25_variant=0.5, vector_variant=0.5, hyde=0.7
  topRankBonus = 0.1 (added if result in top-5 of both BM25 and vector)

Blending (src/pipeline/rerank.ts + types.ts:196-200)

Position-aware blending between fusion and rerank scores:

Position Fusion Weight Rerank Weight
1-3 0.75 0.25
4-10 0.60 0.40
11+ 0.40 0.60

minScore filter: Applied AFTER normalization (search.ts:357-361).

5. Model Presets

Presets define disk storage only. RAM varies by context length, batch size, GPU offload.

Preset Disk Embed Rerank Gen
slim ~1GB bge-m3-Q4 bge-reranker-v2-m3-Q4 Qwen3-1.7B-Q4
balanced ~2GB bge-m3-Q4 bge-reranker-v2-m3-Q4 SmolLM3-3B-Q4
quality ~2.5GB bge-m3-Q4 bge-reranker-v2-m3-Q4 Qwen3-4B-Q4

All use bge-m3 (multilingual, 1024 dims) for embedding.

RAM Note: Actual memory usage depends on:

  • Context window size (longer = more RAM)
  • GPU offload (VRAM vs system RAM)
  • Batch size during embedding

6. Design Decisions

Content-Addressed Storage

  • mirror_hash = SHA-256 of canonical markdown
  • Multiple source files can share same mirror (dedup)
  • Chunks keyed by (mirror_hash, seq)
  • Vectors keyed by (mirror_hash, seq, model)

FTS Tokenizer Immutability

  • Tokenizer stored in schema_meta.fts_tokenizer
  • Changing tokenizer requires --rebuild-fts (drops and recreates FTS table)
  • Options: unicode61 (default), porter (stemming), trigram (substring)

Per-Model Vector Tables

  • Table name: vec_<sha256(modelUri)[:8]>
  • Avoids dimension mismatches when switching models
  • Graceful degradation: storage works without sqlite-vec

Converter Priority (registry.ts)

  1. native/markdown (.md)
  2. native/plaintext (.txt)
  3. adapter/markitdown-ts (.pdf, .docx, .xlsx)
  4. adapter/officeparser (.pptx)

Canonicalization Contract (canonicalize.ts)

BREAKING CHANGE WARNING: Modifying these rules invalidates all mirrorHash values.

  • Strip BOM (U+FEFF)
  • Normalize \r\n β†’ \n
  • NFC Unicode normalization
  • Strip control chars except \n, \t
  • Trim trailing whitespace per line
  • Whitespace-only lines treated as blank (trim first, then count)
  • Collapse 2+ blank lines β†’ 1
  • Ensure single trailing \n

7. Graceful Degradation Paths

Component If Missing Behavior
sqlite-vec Not installed Vector storage works, search disabled
Embedding model Not downloaded gno embed fails, hybrid falls back to BM25-only
Reranker model Not downloaded Skip reranking, use fusion scores only
Gen model Not downloaded gno ask --answer fails
Collection path Not accessible Ingestion errors recorded per-file

sqlite-vec graceful degradation (sqlite-vec.ts):

  1. Try load extension
  2. If fail: searchAvailable = false, loadError set
  3. Vectors stored in content_vectors (always works)
  4. KNN search disabled, queries fall back to BM25

8. Interface Invariants

Exit Codes (spec/cli.md)

  • 0 = SUCCESS
  • 1 = VALIDATION (user error, bad input)
  • 2 = RUNTIME (system error, retryable)

Output Formats

  • --json - JSON to stdout (default for scripting)
  • --files - One path per line
  • --csv - CSV with header
  • --md - Markdown table
  • --xml - XML document

Error Shape (–json mode)

{ "error": { "code": "ERROR_CODE", "message": "...", "details": {} } }

Schema Contract

12 JSON schemas in spec/output-schemas/:

  • search-result.schema.json
  • status.schema.json
  • sync-result.schema.json
  • etc.

Contract tests: test/spec/schemas/ using Ajv validator.

MCP Interface (spec/mcp.md)

Tools: search, vsearch, query, get, multi_get, status Resource: gno://collection/path/to/file

9. Database Schema Summary

Table Purpose
schema_meta Migration version, fts_tokenizer
collections Collection definitions from config
contexts Context metadata from config
documents Source file tracking (sourceHash β†’ docid, mirrorHash)
content Canonical markdown by mirrorHash
content_chunks Chunked text (mirrorHash, seq, pos, startLine, endLine)
content_fts FTS5 virtual table (text)
content_vectors Embeddings (mirrorHash, seq, model, embedding)
vec_ sqlite-vec KNN index (per model)
ingest_errors Error log
llm_cache Response caching

Key relationships:

  • documents.mirror_hash β†’ content.mirror_hash (many-to-one)
  • content_chunks.mirror_hash β†’ content.mirror_hash
  • content_vectors (mirror_hash, seq) β†’ content_chunks (mirror_hash, seq)