PRD: GNO - Local Knowledge Index and Retrieval (CLI + MCP)
Defines a local-first knowledge indexing and retrieval system with CLI and MCP interfaces, using SQLite FTS5, vector search, and local LLM inference.
What this file does
Defines a local-first knowledge indexing and retrieval system with CLI and MCP interfaces, using SQLite FTS5, vector search, and local LLM inference.
When to use it
- Building a local RAG tool for personal or team document archives
- Creating a CLI-first knowledge base that works offline without external services
- Implementing an MCP server for agent runtimes to query local documents
- Designing a multilingual retrieval pipeline with deterministic conversion and chunking
Assumes this stack
PRD: GNO - Local Knowledge Index and Retrieval (CLI + MCP)
Document status: Draft (spec-driven, implementation-driving)
Last updated: 2025-12-23
Working product name: GNO (from “gnosis”)
Binary / command (default): gno
Virtual URI scheme (default): gno://
Primary interfaces: CLI + MCP server (stdio)
Implementation stack: Bun + TypeScript (ESM), SQLite (FTS5), sqlite-vec, node-llama-cpp
Hard constraint: No Python dependency anywhere in the toolchain (conversion and inference included)
1. Summary
GNO is a local-first knowledge indexing and retrieval system that:
-
Ingests Markdown and common document formats (DOCX, PPTX, XLSX, PDF, TXT, and more over time).
-
Converts non-Markdown files into deterministic Markdown mirrors for indexing, search, and retrieval.
-
Provides high retrieval quality via:
- BM25 keyword search (SQLite FTS5)
- vector semantic search (sqlite-vec)
- hybrid search with structured query expansion, parallel retrieval, fusion, reranking, and explainability
-
Runs fully on-device:
- No external services required
- No separate LLM runtime required (no Ollama)
- Local inference via node-llama-cpp with GGUF models resolved and cached automatically
-
Exposes capabilities through:
- a world-class CLI for humans and scripting
- an MCP server so agent runtimes can query and read local knowledge via tools and
gno://resources
-
Optimizes for first-run success (MVP):
gno initcreates config and index DB (migrations) and can register content rootsgno indexperforms ingestion plus embedding in one command (wrapper overupdateplusembed)gno askis the human-default query UX (citations-first; optional short grounded synthesis)- target: first useful result in 3 commands or fewer (
init->index->ask)
2. Naming and configurability
2.1 Defaults (MVP)
- Tool name: GNO
- CLI:
gno - URI scheme:
gno:// - Directories (defaults, OS-idiomatic; overridable via env vars and
--config/--index):-
Linux (XDG):
- Config dir:
${XDG_CONFIG_HOME:-~/.config}/gno/ - Data dir (SQLite DB):
${XDG_DATA_HOME:-~/.local/share}/gno/ - Cache dir (models, temp artifacts):
${XDG_CACHE_HOME:-~/.cache}/gno/
- Config dir:
-
macOS:
- Config dir:
~/Library/Application Support/gno/config/ - Data dir (SQLite DB):
~/Library/Application Support/gno/data/ - Cache dir (models, temp artifacts):
~/Library/Caches/gno/
- Config dir:
-
Windows:
- Config dir:
%APPDATA%\\gno\\config\\ - Data dir (SQLite DB):
%LOCALAPPDATA%\\gno\\data\\ - Cache dir (models, temp artifacts):
%LOCALAPPDATA%\\gno\\cache\\
- Config dir:
-
2.2 Renaming is cheap by design
All user-visible identifiers must be centrally configurable in src/app/constants.ts (or equivalent), including:
- CLI name
- URI scheme
- config/data/cache directory names
- MCP server name
- MCP tool namespace prefix
2.3 Directory resolution rules (MVP)
Resolution precedence:
- Environment overrides (recommended for tests/CI):
GNO_CONFIG_DIRGNO_DATA_DIRGNO_CACHE_DIR
- Platform defaults from §2.1 (XDG vs Library vs AppData)
Index DB path rule:
- DB file is stored under the resolved data dir as:
<dataDir>/index-<indexName>.sqlite
--index <indexName>selects the file suffix; default index name isdefault.
3. Goals and non-goals
3.1 Goals (MVP)
-
Local-only indexing and search over multiple collections (directories + glob rules).
-
Deterministic conversion to Markdown mirrors suitable for:
- stable indexing
- stable embeddings (given same converter version + model)
- golden conversion fixtures
-
Hybrid retrieval pipeline:
- structured query expansion (lexical + semantic + optional HyDE)
- parallel BM25 and vector retrieval
- fusion via RRF (+ optional top-rank bonus)
- cross-encoder reranking
- position-aware blending to keep results stable
-
Zero extra runtime for inference:
- local embeddings, generation, reranking via node-llama-cpp
- GGUF models auto-resolved and cached
-
Agent-first interfaces:
- CLI structured outputs (JSON, files line protocol, CSV, MD, XML)
- MCP tools/resources with explicit schemas and predictable behavior
-
World-class engineering:
- spec-driven development
- layered architecture (ports/adapters)
- high unit and integration coverage
- deterministic test fixtures
- eval harness and regression gates for stochastic or ranking-sensitive behavior (Evalite v1)
-
World-class first-run UX:
- a user can go from zero to a useful result in 3 commands or fewer (
init->index->ask) - defaults work without manual configuration edits
- missing optional components (models, vectors) degrade gracefully with actionable diagnostics
- scripted onboarding is supported via
--yes(no prompts; safe defaults only)
- a user can go from zero to a useful result in 3 commands or fewer (
-
Multilingual-first retrieval:
- storage and retrieval preserve language signals end-to-end (converter -> chunks -> ranking -> outputs)
- default model preset is multilingual and robust across DE/FR/IT/EN
- lexical search remains deterministic, with tokenizer configured per index (DB), and opt-in stemming only where appropriate
3.2 Non-goals (MVP)
- GUI application
- Background watch daemon (explicit
gno updateonly) - Cloud indexing, hosted mode, or remote retrieval endpoints (other than MCP over stdio)
- Multi-user collaborative indices
- Editing documents through the tool (read-only retrieval)
- OCR/audio transcription/image understanding (best-effort only if a converter happens to extract it; not required)
4. Key concepts and invariants
4.1 Collection
A named set of files defined by:
- root directory path
- glob pattern(s)
- optional include and exclude rules
- optional update command to run before indexing
- optional contexts (global, per-collection, per-path prefix)
4.2 Source file vs Markdown mirror
- Source file: original artifact on disk (example:
contracts/nda.docx) - Markdown mirror: deterministic canonical Markdown derived from the source (example:
nda.docxconverted to Markdown)
Invariant:
- Search and retrieval operate on the mirror text.
- Identity, references, and URIs always refer to the source file.
4.3 Virtual URI scheme
Stable, transport-friendly identifier:
gno://<collection>/<relativePath>
Examples:
gno://work/contracts/nda.docxgno://notes/meetings/2025-11-12.md
4.4 docid
A short handle for quick reference:
- Format:
#<6-8 hex> - Derived from
source_hash(sha256 of source bytes), truncated - Not stable across edits (edits change source_hash), but stable across converter upgrades if the source bytes are unchanged
Example:
#a1b2c3
4.5 Context
Optional human-authored metadata associated with:
- global scope (
/) - a collection root (
collection:) - a path prefix within a collection (
gno://collection/prefix)
Context can be used to:
- display richer search results
- optionally improve embeddings/reranking by prepending context to LLM inputs (configurable)
5. Users and primary workflows
5.1 Personas
- Knowledge workers with mixed-format archives (MD + Office + PDFs)
- Engineers building local RAG for personal or team docs
- Agent workflows needing safe, local retrieval (MCP integration)
5.2 Golden workflows
Index (fast path, recommended):
gno init ~/notes --name notes --pattern "**/*.md"gno init ~/work/docs --name work --pattern "**/*.{md,pdf,docx,pptx,xlsx,txt}"gno index
Index (explicit steps):
gno collection add ~/notes --name notes --pattern "**/*.md"gno collection add ~/work/docs --name work --pattern "**/*.{md,pdf,docx,pptx,xlsx,txt}"gno updategno embed
Ask (human-default):
gno ask "termination clause" --collection workgno ask "wie deployen wir nach staging" --collection work
Search (lower-level):
gno search "termination clause"gno vsearch "how do we deploy to staging"gno query "quarterly planning process" --explain
Retrieve:
gno get gno://work/runbooks/oncall.pdfgno get "#a1b2c3" --line-numbersgno multi-get "work/runbooks/*" --json
Agent integration:
gno mcpand toolsgno_search,gno_query,gno_get, etc.
6. Product principles
- Local truth: source files are authoritative; mirrors are derived.
- Deterministic interfaces: CLI and MCP outputs are stable, versioned, schema-driven.
- Explainable retrieval: return docid, URI, path, snippet, scores, and optional line ranges.
- Fail-soft ingestion: conversion failures do not block indexing; they produce structured errors and actionable diagnostics.
- Minimal friction for inference: no separate server; models resolved and cached automatically.
- No Python: all conversion and inference are Node native (Bun/TS) with well-defined adapters.
- Human-first commands:
init,index, andaskprovide the simplest on-ramp while preserving lower-level primitives (collection,update,embed,query). - Multilingual by default: store language hints/tags and use language-aware defaults for embedding, ranking, and explainability.
7. Functional scope (MVP)
7.1 Collections and configuration
Features:
- create/list/remove/rename collections
- per-collection:
path(absolute)pattern(glob)- optional
includeextensions allowlist - optional
excludepatterns (defaults include.git,node_modules,.venv,.idea,dist,build) - optional
updateshell command executed duringgno update - per-path contexts
- optional
languageHint(BCP-47 likede,fr,it,en, orund; hint only, never required)
Index-level settings (per index DB, not per collection):
ftsTokenizer:- default:
unicode61(multilingual-safe) - optional:
porter(English stemming only; opt-in) - optional:
trigram(if available in the SQLite build; higher index size; validated bygno doctor)
- default:
Config:
- config file path:
<configDir>/index.yml(resolved per §2.1–§2.3) - override via
--config <path>and env vars (for tests/CI)
Onboarding convenience (MVP):
gno initis a thin, ergonomic wrapper over:- config creation (if missing)
- index DB creation and migrations (if missing)
collection add(optional, if a path is provided)- printing next steps and environment info (resolved config/data/cache paths and index DB path)
gno initmust be safe to run repeatedly (idempotent; no destructive actions without explicit flags)
7.2 Ingestion and sync
On gno update, per collection:
-
Enumerate files matching rules.
-
For each file:
- stat (mtime, size)
- if size >
maxByteslimit, recordTOO_LARGEerror and skip (do not read bytes) - read bytes
- compute
source_hash = sha256(bytes) - detect MIME/ext (layered detection)
- select converter adapter
- convert to Markdown mirror
- canonicalize mirror markdown deterministically
- compute
mirror_hash = sha256(canonicalMarkdown)
-
Upsert document record keyed by
(collection, relativePath). -
Store mirror markdown in
contentkeyed bymirror_hash(dedupe). -
Chunk mirror content into
content_chunks.- assign
chunk_language(BCP-47 orund) deterministically (see §10.2)
- assign
-
Update FTS index over chunks.
-
Mark missing files as inactive (soft delete).
-
Record conversion warnings/errors for diagnostics and status.
Deletion handling:
- If previously indexed file no longer exists, mark document inactive.
gno cleanupremoves orphaned content/chunks/vectors not referenced by any active document.
Converter upgrade handling:
- Store
converter_idandconverter_versionper document. - If converter version changes (or routing changes), mirror must be regenerated even if source bytes are unchanged.
Human command alias (MVP):
gno indexis the recommended human command to build/update an index end-to-end:- default behavior: run
gno updatethengno embed --no-embedruns ingestion only--collection <name>scopes ingestion/embedding to a single collection (optional)--models-pullallows model download (network) before embedding (prompted unless--yes)
- default behavior: run
7.3 Supported file types (MVP)
Always supported:
.md(passthrough + canonicalization).txt(plaintext to Markdown + canonicalization)
Converter-backed (MVP):
.pdf,.docx,.xlsxvia a Node MarkItDown port adapter (primary choice:markitdown-ts).pptxviaofficeparser(239 stars, 119K weekly downloads, in-memory extraction)
Future (explicitly supported by architecture, not MVP):
- pdf.js based PDF extraction
- mammoth DOCX conversion
- PPTX parsing
- SheetJS for XLSX
- additional formats via adapter registry
8. Converter subsystem (Node-only, deterministic)
This section is implementation-driving.
8.1 Converter goals
- Single stable conversion API independent of any vendor library
- Deterministic Markdown output (canonicalization rules locked)
- Structured errors and warnings
- Clear routing via MIME detection and registry
- Golden conversion fixtures in the repo
8.2 Converter interfaces
src/converters/types.ts:
export type ConverterId =
| "native/markdown"
| "native/plaintext"
| "adapter/markitdown-ts"
| string;
export type ConvertInput = {
sourcePath: string; // absolute
relativePath: string; // within collection
collection: string;
bytes: Uint8Array;
mime: string;
ext: string; // ".pdf"
limits: {
maxBytes: number;
timeoutMs: number;
};
};
export type ConvertWarning = {
code:
| "LOSSY"
| "TRUNCATED"
| "PARTIAL"
| "UNSUPPORTED_FEATURE"
| "LOW_CONFIDENCE";
message: string;
details?: Record<string, unknown>;
};
export type ConvertOutput = {
markdown: string; // canonical markdown (see 8.4)
title?: string;
languageHint?: string; // optional BCP-47 or "und"
meta: {
converterId: ConverterId;
converterVersion: string;
sourceMime: string;
warnings?: ConvertWarning[];
};
};
export type ConvertResult =
| { ok: true; value: ConvertOutput }
| { ok: false; error: ConvertError };
export interface Converter {
readonly id: ConverterId;
readonly version: string;
canHandle(mime: string, ext: string): boolean;
convert(input: ConvertInput): Promise<ConvertResult>;
}
8.3 Error model
src/converters/errors.ts:
export type ConvertErrorCode =
| "UNSUPPORTED"
| "TOO_LARGE"
| "TIMEOUT"
| "CORRUPT"
| "PERMISSION"
| "IO"
| "ADAPTER_FAILURE"
| "INTERNAL";
export type ConvertError = {
code: ConvertErrorCode;
message: string;
retryable: boolean;
fatal: boolean; // reserved for unrecoverable store corruption, not conversion
converterId: string;
sourcePath: string;
mime: string;
ext: string;
cause?: unknown;
details?: Record<string, unknown>;
};
Indexing policy (MVP):
UNSUPPORTED,TOO_LARGE,CORRUPT: document is indexed as metadata-only and marked non-searchable (no chunks), with a recorded error entry.TIMEOUT,ADAPTER_FAILURE: same as above, plus a warning ingno status.IO,PERMISSION: warn, continue.- Conversion errors never crash the full update unless the DB layer fails.
8.4 Canonical Markdown conventions
Canonicalization must be deterministic and independent of machine/time.
Rules:
- Use
\nnewlines. - Strip
\u0000and other non-printable control chars except\nand\t. - Trim trailing whitespace per line.
- Collapse 3+ blank lines to exactly 2.
- Ensure exactly one final newline at end of document.
Important:
- Do not inject run-specific timestamps into the canonical markdown.
- Do not inject absolute paths into the canonical markdown.
- Source references are surfaced via document metadata and CLI/MCP outputs, not embedded into mirror content used for hashing/indexing.
Language hint propagation (MVP):
- If a converter provides
languageHint, persist it on the document record. - If a converter does not provide
languageHint, determine language at chunking time (see §10.2) and store per chunk.
Optional (display-only) header:
gno getand MCP resource reads may optionally prepend a short comment header for agent friendliness, but that header must be generated at read time and must not be used to computemirror_hash.
8.5 MIME detection strategy
Layered detection:
-
Extension map (MVP):
.md->text/markdown.txt->text/plain.pdf->application/pdf.docx->application/vnd.openxmlformats-officedocument.wordprocessingml.document.pptx->application/vnd.openxmlformats-officedocument.presentationml.presentation.xlsx->application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
-
Lightweight sniffing (MVP+):
- bytes start with
%PDF--> PDF - bytes start with
PK\x03\x04and ext in {docx,pptx,xlsx} -> OOXML MIME - fallback to ext map
- bytes start with
API:
export type MimeDetection = {
mime: string;
ext: string;
confidence: "high" | "medium" | "low";
via: "sniff" | "ext" | "fallback";
};
export interface MimeDetector {
detect(path: string, bytes: Uint8Array): MimeDetection;
}
8.6 Converter registry and routing
Priority order (MVP):
native/markdownnative/plaintextadapter/markitdown-ts(PDF, DOCX, XLSX)adapter/officeparser(PPTX)
Selection:
- choose first converter where
canHandle(mime, ext)is true - else return
UNSUPPORTED
8.7 MVP converter adapter: markitdown-ts
Responsibilities:
- enforce
maxBytesandtimeoutMs - prefer path-based conversion if supported by the library
- map library exceptions into
ConvertError - emit warnings for suspicious output:
- empty output for non-empty input
- truncation or partial extraction signals
Hard constraint:
- No Python-based MarkItDown. Node adapter only.
8.7b MVP converter adapter: officeparser (PPTX)
Responsibilities:
- handle
.pptxfiles (markitdown-ts has incomplete PPTX support) - extract slide text and speaker notes
- format extracted text as Markdown with filename-derived title
- enforce
maxBytes(defense in depth; EPIC 5 does pre-read stat check) - map library exceptions into
ConvertError
Library choice rationale:
- 239 GitHub stars, 119K weekly npm downloads
- In-memory extraction (no disk writes)
- Full TypeScript support
- Active maintenance (Nov 2024 updates)
8.8 Converter golden fixture plan
Fixtures:
test/fixtures/conversion/
pdf/
simple.pdf
simple.expected.md
docx/
headings.docx
headings.expected.md
xlsx/
sheet1.xlsx
sheet1.expected.md
pptx/
deck1.pptx
deck1.expected.md
md/
sample.md
sample.expected.md
Golden tests:
- converter output must match
*.expected.mdexactly - converter version is locked in
package.json - if output changes, update fixtures deliberately
9. Storage model and schema (SQLite)
9.1 Design intent
- Source identity lives in
documents - Mirror content is content-addressed in
contentbymirror_hash - Chunking is mirror-derived and stored in
content_chunks - FTS indexes chunk text
- Vectors index chunk embeddings by model
- Contexts are separate and applied at query/runtime
9.2 Conceptual tables (MVP)
-
collectionsname,path,pattern,include,exclude,update_cmd, timestamps
-
contextsscope_type(global, collection, prefix)scope_key(e.g.,/,collection:work,gno://work/prefix)text- timestamps
-
documentsidcollectionrel_pathtitlesource_abs_pathsource_mime,source_extsource_mtime,source_sizesource_hash(sha256)docid(derived fromsource_hash, 8 hex chars)mirror_hash(soft reference tocontent, NOT an FK - documents are tracked before content exists;cleanupOrphans()handles integrity)converter_id,converter_versionlanguage_hint(optional; BCP-47 orund)activelast_error_code,last_error_message(optional denormalization)- timestamps
-
contentmirror_hash(PK)markdown- timestamps
-
content_chunks(mirror_hash, seq)(PK)pos(char offset)textstart_line,end_line(recommended to support stable line-number output)language(BCP-47 orund, derived deterministically)token_count(optional; if tokenizer is available, for debugging and evals)
-
content_fts(FTS5 virtual table over chunk text plus doc metadata columns) -
content_vectors(mirror_hash, seq, model)(PK)embeddingstored via sqlite-vec backingembedded_at
-
llm_cachekey(PK)value- timestamps
-
ingest_errors(optional but recommended)(collection, rel_path, occurred_at)code,message,details_json
Schema source of truth:
spec/db/schema.sqlplus migrations
10. Chunking and indexing
10.1 Chunking requirements
- Token-aware chunking preferred when embedding model tokenizer is available
- Deterministic chunk boundaries for a given (model, text) pair
- Default parameters (configurable):
maxTokens = 800overlap = 15%(120 tokens)
Fallback (MVP bootstrapping):
- char-based chunking if tokenizer is not available yet (must be deterministic)
10.2 Language detection and tagging (multilingual MVP)
Goals:
- stable per-chunk language tags to improve explainability, ranking prompt selection, and optional language filtering
- deterministic results for the same input text and the same detector version
Policy (MVP):
- Each chunk stores
languageas BCP-47 orund. - Language tag derivation order:
- document-level
language_hintfrom converter, if present and notund - deterministic heuristic detector over chunk text (no network; no stochastic model)
- optional collection-level
languageHint(if configured and notund) - else
und
- document-level
Implementation guidance (MVP):
- Use a lightweight, deterministic detector with a pinned implementation version in code.
- Detector must not depend on system locale, current time, or non-deterministic APIs.
10.3 Indexing surfaces
- FTS indexes chunk text, not entire documents, to align with vector chunk retrieval
- Snippets returned from the best matching chunk(s), with optional line numbers
10.4 FTS tokenizer configuration (per index DB, multilingual MVP)
- FTS5 tokenizer is configured at FTS virtual table creation time.
- Therefore tokenizer selection is per index (DB), not per document or per row.
- Default tokenizer is
unicode61. - Optional tokenizers (if available) are validated by
gno doctor. - Tokenizer selection is stored in index metadata and reported in
gno status.
11. Local inference and model strategy (node-llama-cpp)
11.1 Runtime requirements
Local inference uses node-llama-cpp with GGUF models, supporting:
- embeddings
- generation (query expansion and optional HyDE)
- reranking (cross-encoder)
Constraints:
- No separate server process
- Safe lifecycle management:
- keep models loaded for repeated calls when practical
- dispose contexts/sequences promptly
- avoid leaking memory across long MCP sessions
11.2 Determinism settings (MVP)
For query expansion / HyDE:
- temperature = 0
- bounded max tokens
- versioned prompt templates
- cache results by
(prompt_version, model_uri, input_hash)
11.3 Model management UX
Cache:
<cacheDir>/models(resolved per §2.1–§2.3)
Commands:
gno models listgno models pull [--all|--embed|--rerank|--gen]gno models cleargno models path
First-run fallback behavior (MVP):
- If embeddings are not available yet (no vectors present and/or embed model missing):
gno vsearchreturns a structured error indicating vectors are unavailable and suggestsgno indexorgno embedgno queryandgno askdegrade gracefully to BM25-only retrieval by default and annotate this in--explainand JSON metadata
- If generation/rerank models are missing:
- expansion/HyDE and reranking are skipped (with explainability), but retrieval still returns citations/snippets
11.4 Default model presets (config-driven)
Defaults must live in config presets, not in business logic.
Provide at least two presets (MVP):
-
Preset A (multilingual, widely used):
- Embedding: bge-m3 GGUF
- Reranker: bge-reranker-v2-m3 (cross-encoder) GGUF where available
- Generation: small Qwen Instruct GGUF for structured expansion/HyDE
-
Preset B (Qwen family):
- Embedding: Qwen3 embedding GGUF
- Reranker: Qwen3 reranker GGUF
- Generation: small Qwen3 Instruct GGUF
gno doctor must validate:
- model URIs resolvable
- models cached or downloadable
- embedding and rerank dimensions/config match expectations
Default preset rule (MVP):
- Preset A is the default unless the user explicitly selects another preset.
- Rationale: consistent multilingual behavior across DE/FR/IT/EN in a single default path.
12. Search modes and pipelines
12.1 Search commands
gno search: BM25/FTS only (fast, deterministic)gno vsearch: vector only (semantic)gno query: hybrid (best quality)gno ask: human-default wrapper overgno query(citations-first; optional grounded short answer)
12.2 Structured query expansion (hybrid)
Expansion returns a structured object:
- lexical variants for BM25
- semantic variants for embedding queries
- optional HyDE synthetic snippet (only if valid)
Example shape:
{
"lexicalQueries": ["...", "..."],
"vectorQueries": ["...", "..."],
"hyde": "optional synthetic paragraph",
"notes": "optional debug notes"
}
Rules:
- Expansion must be JSON-schema constrained to avoid malformed outputs.
- Expansion is skipped when BM25 is already strong (configurable threshold).
12.3 Parallel retrieval
Hybrid retrieval runs in parallel:
-
BM25 over:
- original query (weighted)
- lexicalQueries variants
-
Vector over:
- original query
- vectorQueries variants
- HyDE if present
12.4 Fusion via RRF (+ top-rank bonus)
- Reciprocal Rank Fusion with parameter
k = 60(configurable) - Weight original query results higher than expansions
- Optional top-rank bonus to reward agreement across retrieval modes
- Candidate cap for reranking (example: top 50-100 chunks)
12.5 Reranking
- Cross-encoder reranker scores candidate chunks (0..1 normalized)
- Rerank inputs include:
- query
- chunk text
- optional title and applicable contexts (configurable)
12.6 Position-aware blending
Goal: avoid destabilizing top results due to reranker noise.
Example blending schedule (configurable):
- ranks 1-3: 0.75 retrieval score, 0.25 rerank score
- ranks 4-10: 0.60 retrieval score, 0.40 rerank score
- ranks 11+: 0.40 retrieval score, 0.60 rerank score
12.7 Explainability
gno query --explain prints to stderr:
- whether expansion was used
- expansion payload (redacted if needed)
- BM25 and vector contribution summary
- fusion and reranking parameters
- final score components per top result
13. Retrieval
13.1 get
Retrieves a single document mirror by:
gno://...collection/path#docid- fuzzy filename match (optional, must be deterministic and explainable)
Supports:
:linesuffix:gno get gno://work/contracts/nda.docx:120--from <line>-l <maxLines>--line-numbers--source(include abs source metadata in output; does not change mirror content)
Default:
- returns Markdown mirror plus metadata (CLI format)
- in JSON format returns structured object with mirror and source refs
13.2 multi-get
Retrieves multiple documents by:
- glob pattern
- comma-separated list of refs
- list of docids
Supports:
--max-bytes <n>default 10240 for safety--line-numbers- structured “skipped” records when limits exceeded
14. CLI specification
14.1 Global conventions
Exit codes:
- 0 success
- 1 validation / usage error
- 2 runtime failure (IO, DB, conversion, model, etc.)
Global flags:
--index <name>use alternate DB name (multiple indices per machine)--config <path>override config path--no-color--verbose--yesnon-interactive mode: accept safe defaults, never prompt (required for CI and scripted onboarding)
Output format flags (where applicable):
--json--files(line protocol)--csv--md--xml
14.2 Command catalog (MVP)
gno statusgno init [<path>] [--name <name>] [--pattern <glob>] [--include <csv-ext>] [--exclude <csv>] [--update <cmd>] [--yes]gno collection add <path> --name <name> [--pattern <glob>] [--include <csv-ext>] [--exclude <csv>] [--update <cmd>]gno collection listgno collection remove <name>gno collection rename <old> <new>gno update [--git-pull]gno index [--collection <name>] [--no-embed] [--models-pull] [--git-pull] [--yes]gno embed [--force] [--model <embedModelUri>] [--batch-size <n>]gno search <query> [options]gno vsearch <query> [options]gno query <query> [options]gno ask <query> [options]gno get <ref> [options]gno multi-get <pattern-or-list> [options]gno ls [collection|gno://collection/prefix]gno context add <path|gno://...|/> "text"gno context listgno context checkgno context rm <path|gno://...|/>gno models list|pull|clear|pathgno cleanupgno doctorgno mcp
14.3 Search options (search, vsearch, query)
-n <num>limit (default 5; default 20 for--json/--files)--min-score <num>-c, --collection <name>--fullinclude full mirror (instead of snippet)--line-numbers--lang <bcp47>optional filter or hint:- for
search: best-effort filter by chunk language where available - for
vsearch/query/ask: hint for language detection and prompt selection - default: auto
- for
- hybrid-only:
--no-expand--no-rerank--explain
14.4 Ask options (MVP)
gno ask is a wrapper over gno query with citations-first output.
Additional options:
--answerenable a short grounded answer (requires generation model)--no-answerforce retrieval-only output (citations/snippets only)--max-answer-tokens <n>hard cap for answer generation (default small, config-driven)
15. Output contracts (schema-driven)
15.1 Search result JSON shape
All search-like commands output:
{
"docid": "#a1b2c3",
"score": 0.78,
"uri": "gno://work/contracts/nda.docx",
"title": "Extracted Title",
"snippetLanguage": "de",
"context": "Optional folder context",
"snippet": "Markdown snippet (mirror)",
"snippetRange": { "startLine": 120, "endLine": 145 },
"source": {
"absPath": "/abs/path/to/source.docx",
"relPath": "contracts/nda.docx",
"mime": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"ext": ".docx",
"modifiedAt": "2025-11-12T10:30:00Z",
"sizeBytes": 123456,
"sourceHash": "sha256-hex"
},
"conversion": {
"converterId": "adapter/markitdown-ts",
"converterVersion": "x.y.z",
"mirrorHash": "sha256-hex",
"warnings": []
}
}
Contract rules:
urialways refers to the source identity.snippetcomes from mirror content.snippetLanguageis the best-effort language tag for the snippet range (BCP-47 orund).source.absPathis included when:--sourceis set, or- output is from MCP tools (always include to support agent actions).
15.2 --files line protocol
One line per result:
#docid,<score>,gno://collection/path
Optional extra fields may be appended only in a versioned manner.
15.3 Schema artifacts
Repo must include JSON schemas for:
- search result item
- status payload
- get payload
- multi-get payload
- MCP tool structured outputs
- ask payload
15.4 ask JSON shape (MVP)
gno ask --json returns:
{
"query": "string",
"mode": "hybrid",
"queryLanguage": "auto",
"answer": "optional string",
"citations": [
{
"docid": "#a1b2c3",
"uri": "gno://work/path",
"startLine": 120,
"endLine": 145
}
],
"results": [],
"meta": {
"expanded": true,
"reranked": true,
"vectorsUsed": true
}
}
Rules:
results[]items are identical togno query --jsonresult item shape.answermust be grounded inresultsand cite line ranges viacitations.- If generation is unavailable or disabled, omit
answer(do not emit an empty string). - If vectors are unavailable, set
modetobm25_onlyand setmeta.vectorsUsed=false.
16. MCP specification
16.1 Server
- Command:
gno mcp - Transport: stdio
- Must keep DB open for server lifetime for performance
16.2 Resources
gno://{collection}/{path}-
returns
text/markdownmirror content -
may optionally prepend a display-only comment header with:
- uri
- source abs path
- mime
- docid
- language metadata when available (document hint and/or chunk language for returned ranges)
-
line numbers default ON for agent friendliness
-
16.3 Tools (stable namespace)
Tool names are stable and versioned under gno.*:
gno_search(BM25)gno_vsearch(vector)gno_query(hybrid)gno_getgno_multi_getgno_status
Each tool returns:
content[]: human-readable summary stringsstructuredContent: machine-readable payloads matching schemasisError: trueon failures (for example vector index missing)
Multilingual MCP requirement (MVP):
- Tool structured outputs include
snippetLanguagewhere applicable (mirrors CLI schema).
16.4 MCP correctness requirements
- Strictly follow MCP tool response conventions
- Ensure
gno://URIs are URL-encoded for special characters while preserving path slashes - Maintain backward compatibility of tool schemas once published (version fields required)
17. Architecture
17.1 High-level design
- Ports and Adapters (hexagonal)
- Functional core / imperative shell
- Result-based error handling (no throws across boundaries)
Core domain responsibilities:
- document identity and source refs
- mirror content and hashing
- chunking
- retrieval orchestration and scoring
Ports:
ConverterPortStorePortEmbeddingPortGenerationPort(for expansion/HyDE)RerankPortFsPort,ClockPort(testability)MimeDetectorPortFileWalkerPort
Adapters:
- SQLite store + migrations
- FTS5 adapter
- sqlite-vec adapter
- node-llama-cpp adapter
- markitdown-ts adapter
- CLI delivery
- MCP delivery
17.2 Proposed module layout
(Reference layout, not a constraint, but should remain layered.)
src/
app/
config/
domain/
converters/
adapters/markitdownJs/
llm/
nodeLlamaCpp/
store/
migrations/
sqlite/
indexing/
pipeline/
cli/
mcp/
test/
fixtures/
spec/
eval/
spec/
cli.md
mcp.md
db/schema.sql
converters.md
models.md
evals.md
output-schemas/
18. Engineering quality requirements
18.1 Tests
Unit tests:
- path normalization
- MIME detection
- canonical Markdown normalization
- chunking boundaries
- RRF and blending math
- query expansion schema validation
- language detector determinism and tagging rules
Integration tests:
- create temp index DB, run migrations
gno updateover fixture corpusgno search/vsearch/query/get/multi-getoutputs match schemas- converter golden fixtures
- MCP server tool contract tests (golden calls)
- multilingual fixture corpus:
- at least one DE and one EN document
- confirm
snippetLanguagetagging is stable and surfaced in CLI and MCP outputs
18.2 Evals (Evalite v1)
Scope:
- ranking quality gates for
vsearchandquery - stability checks for structured expansion outputs
- multilingual ranking sanity checks (DE/FR/IT/EN mixed corpus)
18.2.1 Evalite Setup
File structure:
test/
eval/
vsearch.eval.ts # vector search ranking evals
query.eval.ts # hybrid query pipeline evals
expansion.eval.ts # structured expansion stability
multilingual.eval.ts # cross-language ranking
fixtures/
corpus/ # DE/EN/FR/IT test documents
queries.json # query-judgment pairs
evalite.config.ts # global eval configuration
Configuration (evalite.config.ts):
import { defineConfig } from "evalite/config";
import { createSqliteStorage } from "evalite/sqlite-storage";
export default defineConfig({
storage: () => createSqliteStorage("./evalite.db"),
testTimeout: 120000, // 2 min for slow LLM calls
maxConcurrency: 10, // parallel test cases
scoreThreshold: 70, // MVP: 70%, tighten over time
cache: true, // cache LLM responses in dev
});
18.2.2 Custom Scorers (IR Metrics)
Create reusable scorers for retrieval metrics (not built into Evalite):
// test/eval/scorers/ir-metrics.ts
import { createScorer } from "evalite";
export const recallAtK = (k: number) =>
createScorer<
{ query: string },
string[], // output: docids
string[] // expected: relevant docids
>({
name: `Recall@${k}`,
description: `Fraction of relevant docs in top ${k} results`,
scorer: ({ output, expected }) => {
const topK = output.slice(0, k);
const hits = expected.filter((id) => topK.includes(id)).length;
return {
score: expected.length > 0 ? hits / expected.length : 1,
metadata: { k, hits, total: expected.length },
};
},
});
export const ndcgAtK = (k: number) =>
createScorer<
{ query: string },
string[],
{ docid: string; relevance: number }[]
>({
name: `nDCG@${k}`,
description: `Normalized DCG at rank ${k}`,
scorer: ({ output, expected }) => {
const relevanceMap = new Map(expected.map((e) => [e.docid, e.relevance]));
const dcg = output.slice(0, k).reduce((sum, docid, i) => {
const rel = relevanceMap.get(docid) ?? 0;
return sum + (Math.pow(2, rel) - 1) / Math.log2(i + 2);
}, 0);
const ideal = [...expected]
.sort((a, b) => b.relevance - a.relevance)
.slice(0, k)
.reduce(
(sum, e, i) =>
sum + (Math.pow(2, e.relevance) - 1) / Math.log2(i + 2),
0
);
return {
score: ideal > 0 ? dcg / ideal : 1,
metadata: { k, dcg, idcg: ideal },
};
},
});
18.2.3 Example Eval File
// test/eval/vsearch.eval.ts
import { evalite } from "evalite";
import { recallAtK, ndcgAtK } from "./scorers/ir-metrics";
import { vsearch } from "../../src/pipeline/vsearch";
evalite("Vector Search Ranking", {
data: async () => {
const queries = await Bun.file("test/eval/fixtures/queries.json").json();
return queries.map((q) => ({
input: { query: q.query, collection: q.collection },
expected: q.relevantDocs,
}));
},
task: async (input) => {
const results = await vsearch(input.query, {
collection: input.collection,
limit: 10,
});
return results.map((r) => r.docid);
},
scorers: [
{ scorer: (args) => recallAtK(5).scorer(args) },
{ scorer: (args) => recallAtK(10).scorer(args) },
{ scorer: (args) => ndcgAtK(10).scorer(args) },
],
trialCount: 1, // deterministic for same embeddings
});
18.2.4 Metrics
- recall@k (k=5,10) via custom scorer
- nDCG@k via custom scorer
- latency budgets (soft gate initially, tracked via custom column)
18.2.5 Rules
- golden tests must not depend on exact expanded queries
- eval thresholds must tolerate minor model drift while catching major regressions
- use
trialCount > 1for non-deterministic tasks (e.g., LLM expansion) to measure variance - cache LLM responses in dev (
cache: true) for fast iteration
18.2.6 CLI Usage
# Dev: watch mode with UI at localhost:3006
bun run evalite watch
# CI: run once, fail if threshold not met
bun run evalite --threshold=70 --outputPath=./eval-results.json
# Export static UI for CI artifacts
bun run evalite export --output=./eval-ui
18.2.7 Multilingual Eval Notes (MVP)
- include language-mismatched queries (e.g., DE query over EN doc) to validate vector + rerank behavior
- do not gate on exact expansion text, only on ranking metrics and schema validity
- use
columnsto showsnippetLanguagefor debugging cross-language behavior
See spec/evals.md for detailed implementation specification.
19. Packaging and distribution
Target: npm registry, Bun-only
# Requires Bun runtime
curl -fsSL https://bun.sh/install | bash
# Then run
bunx @gmickel/gno
# Or install globally
bun add -g @gmickel/gno
Note: npm install / yarn / pnpm will download the package but execution requires Bun runtime (code uses bun:sqlite, bin is .ts).
Prebuilt standalone binaries not viable due to native deps (sqlite-vec needs sidecar, node-llama-cpp can't bundle). See notes/spike-bun-compile.md.
First-run UX:
gno initguides initial setup and prints what was created and where it lives.gno doctorguides:- converter readiness
- sqlite-vec availability
- model resolution and downloads
- embedding backlog
20. Risks and mitigations
-
Native dependency friction (sqlite-vec, node-llama-cpp) Mitigation:
doctor, clear error messages, platform matrix CI, documented rebuild guidance. -
Converter output drift across library versions Mitigation: lock converter versions, store converter version per doc, golden fixtures updated deliberately.
-
Reranking latency on CPU Mitigation: cap candidates, cache rerank results, position-aware blending.
-
Multilingual lexical search limitations Mitigation:
- default
unicode61tokenizer for multilingual safety - opt-in stemming only where appropriate (
porterfor English) - rely on vector + rerank for cross-language and morphology-heavy cases
- store per-chunk language tags to improve explainability and prompt selection
- default
21. Spec-driven development workflow
Rule: No implementation merges without spec updates and executable contract tests.
Repo must include:
spec/cli.md✓spec/mcp.md✓spec/db/schema.sql(placeholder exists)spec/converters.mdspec/models.mdspec/evals.md(Evalite v1 implementation spec)spec/output-schemas/*.json✓
Contract tests: test/spec/schemas/ (94 tests via Ajv)
Definition of done (per epic):
- specs updated and reviewed
- contract tests and golden fixtures added/updated
- unit and integration tests pass
- eval gates pass where applicable
- CLI help updated
22. Implementation plan (ordered epics and tasks)
EPIC 0 - Repo scaffold and naming constants
- T0.1 Bun + TS ESM scaffold, lint/typecheck, test runner baseline
- T0.2 Central constants module for CLI name, URI scheme, dirs, MCP namespace
- T0.3 CI pipeline: typecheck + tests
Acceptance:
bun testpasses- renaming GNO elements is a single-module change
EPIC 1 - Specs and contract tests (freeze interfaces early)
- T1.1 Write
spec/cli.md(commands, flags, exit codes, output formats) - T1.2 Write
spec/mcp.md(tools/resources, schemas, versioning rules) - T1.3 Write
spec/output-schemas/*.json - T1.4 Add contract tests validating JSON outputs against schemas
Acceptance:
- schema contract tests exist before feature implementation expands
EPIC 2 - Config, collections, contexts
- T2.1 Config schema + loader/saver (YAML), XDG defaults, overrides
- T2.2
collection add/list/remove/rename - T2.3
context add/list/check/rmincluding global (/) and prefix contexts - T2.4
initcommand (idempotent):- create config + DB if missing
- optionally add a collection (same flags as
collection add) - support
--yes
- T2.5 Multilingual config:
- collection-level
languageHintparsing and validation - index-level
ftsTokenizerparsing and validation (per index DB)
- collection-level
Acceptance:
- deterministic config edits, validated by unit tests
EPIC 3 - Store layer (SQLite + migrations)
- T3.1 Implement migrations runner
- T3.2 Implement core tables and queries (collections, contexts, documents, content, chunks, errors)
- T3.3 Status/health queries
Acceptance:
- integration tests migrate and run CRUD correctly
EPIC 4 - Converter subsystem (Node-only)
- T4.1 MIME detector + tests
- T4.2 Canonical Markdown normalizer + tests
- T4.3 Converter interfaces, registry, error mapping + tests
- T4.4 Native markdown/plaintext converters
- T4.5 markitdown-ts adapter + golden fixtures for pdf/docx/xlsx
- T4.5b officeparser adapter + golden fixtures for pptx
Acceptance:
- conversion fixtures match expected markdown exactly
- no Python dependency
EPIC 5 - Indexing sync (gno update) and FTS
- T5.1 File walker + include/exclude logic + deterministic path normalization
- T5.2 Sync algorithm: hash, convert, upsert, soft-delete missing
- T5.3 Chunking (deterministic, tokenizer-aware when available)
- T5.4 FTS5 indexing over chunks + snippet extraction (with optional line ranges)
- T5.5
gno statusandgno cleanup - T5.6
indexcommand wrapper:- runs
updatethenembed(unless--no-embed) - supports
--collectionscoping - supports
--models-pull(model resolution/download; prompted unless--yes) - supports
--git-pull(best-effort git pull in git repos) - supports
--yes
- runs
- T5.7 Language tagging:
- persist document
language_hint - deterministic per-chunk language detection and storage
- surface
snippetLanguagein CLI and MCP outputs
- persist document
Acceptance:
gno updateon fixture corpus yields correct docs/chunks/fts- conversion errors recorded but do not block overall update
EPIC 6 - LLM subsystem (node-llama-cpp) and model UX
- T6.1 LLM adapter lifecycle management
- T6.2 Model presets and config overrides
- T6.3 Model cache resolver (hf: URIs) and
gno modelscommands - T6.4
gno doctorchecks for models, vec availability, conversion readiness
Acceptance:
- local models resolvable and cached
- doctor produces actionable diagnostics
EPIC 7 - Vector index and embeddings workflow (gno embed)
- T7.1 sqlite-vec integration (optional deps handled cleanly)
- T7.2 Embedding backlog detection
- T7.3 Batch embed chunks and store vectors per model
- T7.4
--forcere-embed support
Acceptance:
- vectors populated and status shows backlog decreasing to zero
EPIC 8 - Search pipelines
-
T8.1
gno search(FTS) -
T8.2
gno vsearch(vector) -
T8.3
gno queryhybrid:- strong BM25 skip expansion
- structured expansion with schema constraint
- parallel retrieval
- RRF fusion + top-rank bonus
- rerank + blended scoring
--explain
-
T8.4
askcommand:- wrapper over
querywith citations-first output - optional grounded short answer when generation is configured (gated by
--answer)
- wrapper over
-
T8.5 Multilingual awareness:
- deterministic query language detection (heuristics)
- language-aware prompt template selection for expansion and rerank
- cache keys include language tags and template versions
Acceptance:
- deterministic golden tests for
search - evalite gates for hybrid behavior and ranking
EPIC 9 - Retrieval and output polish
- T9.1 Output formatters: cli/json/files/csv/md/xml
- T9.2
getandmulti-getincluding limits and skipped records - T9.3
ls(collections and per-prefix listing)
Acceptance:
- output schemas enforced by tests across formats where applicable
EPIC 10 - MCP server (stdio)
- T10.1 MCP server skeleton
- T10.2 Implement tools:
gno_search,gno_vsearch,gno_query,gno_get,gno_multi_get,gno_status - T10.3 Implement resource reads for
gno://... - T10.4 MCP contract tests (golden tool calls)
Acceptance:
- MCP inspector can call tools and read resources consistently
EPIC 11 - Evals and regression gates (Evalite v1)
- T11.1 Curate corpus, queries, judgments
- T11.2 Implement eval harness and metrics
- T11.3 CI gating (soft fail then hard fail after baseline)
Acceptance:
- evals run locally and in CI with documented thresholds
EPIC 12 - Packaging, release, docs
- T12.1 npm packaging and install docs
- T12.1a Quickstart docs:
gno init->gno index->gno ask- include OS-specific paths and what gets created where
- include multilingual note: default preset is multilingual; language tagging is best effort
- T12.2 release automation, versioning, changelog
- T12.3 troubleshooting: native deps, caches, model downloads
Acceptance:
- clean install path;
gno doctormakes first-run successful
23. MVP acceptance criteria
-
User can index:
- a Markdown folder
- a folder containing at least PDF and DOCX (and ideally PPTX/XLSX)
-
gno search,gno vsearch,gno queryreturn results with:- docid
gno://URI- source path + MIME (in structured output; always for MCP)
- snippet and optional line range
-
gno getreturns mirror markdown and can include source metadata. -
gno mcpexposes tools with stable schemas and supportsgno://reads. -
Test suite includes:
- converter golden fixtures
- CLI integration tests on a fixture corpus
- evalite harness for ranking quality and expansion stability
-
No Python dependency anywhere.
-
First-run success:
- a new user can run
gno init->gno index->gno askand get usable results without editing config files manually
- a new user can run
-
Multilingual behavior:
- per-chunk language tags are stored deterministically and surfaced in CLI and MCP outputs (
snippetLanguage) - default model preset supports DE/FR/IT/EN, and hybrid retrieval remains usable without manual tuning
- per-chunk language tags are stored deterministically and surfaced in CLI and MCP outputs (
24. Future extensions (explicitly supported by architecture)
- Watch mode:
gno watchincremental updates (non-MVP) - Cross-device sync protocol (content-addressed manifests)
- Rich metadata extraction (authors, dates, slide titles, sheet names)
- OS integration:
gno open/ reveal (non-MVP) - Additional native converters (pdf.js, mammoth, pptx parsing, SheetJS)
What's inside
11 sections covering goals, architecture, storage schema, converter subsystem, chunking, inference, and CLI/MCP workflows
Change this for your project
- Replace
gmickel/gnowith your own repository name - Replace
gnoCLI name andgno://URI scheme with your own identifiers insrc/app/constants.ts - Replace default directory paths like
~/.config/gno/with your own OS-idiomatic paths - Replace model presets and GGUF model URIs with your chosen models
Where it goes
Keep it in your repository where the agent or team that needs it will read it.
Worth borrowing
- Deterministic Markdown mirroring with golden test fixtures for stable indexing and embeddings
- Layered converter registry with structured error model that never crashes the full update
- Per-index FTS tokenizer configuration with multilingual defaults and graceful degradation when models are missing
Related Documents
SourceAtlas PRD v2.9.6
Defines the product requirements, architecture, and command interface for an AI-powered codebase understanding assistant integrated into Claude Code.
AGENTS.md — ShakkaShell v2.0
Guides AI coding agents through building a CLI that translates natural language into offensive security commands, with a defined tech stack, structure, and implementation order.
Fleet Management System - Product Requirements Document (PRD)
Defines functional, non-functional, and technical requirements for a fleet management system with compressed GPS tracking and predictive maintenance.
TracePerf - Advanced Console Logging & Performance Tracking
Defines a Node.js logging library with execution flow tracing, performance bottleneck detection, and conditional log modes for dev/staging/prod.