./run llm-wiki-prompt-centomila-righe
LLM Wiki: when a one-page prompt becomes a hundred-thousand-line app
An exploration of nashsu/llm_wiki: how an abstract Karpathy pattern for LLM-maintained wikis becomes an 87-module Tauri desktop app, between graphs, ingest…
- status
- exploration
- project
- LLM-wiki
- updated
- 2026-09-20
- author
- third-party · exploration
- tags
I opened nashsu/llm_wiki expecting a prompt. What I found is a file, llm-wiki.md, that is exactly that: a page and a half of prose, the abstract description of an idea — Karpathy’s pattern for building personal knowledge bases with LLMs. The document says, verbatim, that it’s “intentionally abstract” and that “your agent will build the details.” Then I look next to it: package.json version 0.4.24, a Rust backend with Tauri, React 19, sigma.js for graphs, LanceDB, an MCP server, a Chrome extension. Eighty-seven TypeScript modules in src/lib/ alone, with ~95 test files keeping them company. ingest.ts on its own is 2,993 lines.
This is the hook that kept me here: the distance between an idea that fits on one page and its real instance. The pattern promises that “the LLM does all the dirty work, near-zero maintenance.” The code tells another story — the story of how much dirty work you have to write by hand for that promise not to collapse. It’s not mine, and it’s worth reading precisely for that.
The idea, in one line
Classic RAG redoes everything on every question: retrieve chunks, synthesize, forget. Here instead the LLM builds and maintains a persistent wiki — interlinked markdown files that accumulate. Add a source and the LLM doesn’t index it: it reads it, updates the existing entity pages, marks where it contradicts what was there. Knowledge is compiled once and kept alive. Obsidian as the IDE, the LLM as the programmer, the wiki as the codebase.
The architecture is three layers: raw sources (immutable, the truth), wiki (generated markdown, owned by the LLM), schema (the rules). Three operations: ingest, query, lint. Up to here it’s Karpathy’s document. The rest is where nashsu put in the work.
The piece that convinced me: the 4-signal relevance model
The pattern talks about [[wikilink]] but has no graph analysis at all. Here instead there’s a real relevance engine between pages. The calculateRelevance function combines four signals with explicit weights (graph-relevance.ts):
const WEIGHTS = {
directLink: 3.0, // pagine collegate via [[wikilink]]
sourceOverlap: 4.0, // pagine che condividono la stessa fonte grezza
commonNeighbor: 1.5, // Adamic-Adar sui vicini comuni
typeAffinity: 1.0, // bonus entity↔concept, ecc.
}
The detail I appreciate is that the heaviest signal isn’t the explicit links (3.0) but the source overlap (4.0): two pages born from the same raw source are considered more related than two pages that link to each other. It’s a precise domain choice — provenance counts more than citation. And the Adamic-Adar is implemented properly, weighting common neighbors by the inverse of the logarithm of their degree:
adamicAdar += 1 / Math.log(Math.max(degree, 2))
a hub connected to everything is worth less than a shared niche neighbor. On top of it run Louvain community detection (with cohesion scoring: clusters under 0.15 internal density get flagged as “sparse”) and graph insights — surprising cross-community connections and knowledge gaps. Stuff the abstract pattern doesn’t even hint at.
The thorn: “the LLM writes the files” is the hard part, not the easy one
The pattern says, with disarming lightness, that the LLM “touches 10-15 pages in one go.” In practice, the LLM spits out a single stream of text and someone has to cut it into real files. That someone is parseFileBlocks, and its header comment is the most honest thing I read in the whole repo. It lists the hazards, numbered, each with its test fixture:
- H1 — Windows CRLF: the regex anchored on
\nmissed every block. - H2 — truncated stream: the final
---END FILE---never arrived and the page silently disappeared. Not solvable (it’s a stream budget problem), but at least now it emits a warning. - H5 — the
---END FILE---delimiter inside a code fence, typical when the LLM writes a concept-page about the wiki’s own ingest format. The lazy match stopped there and truncated everything.
It’s the real lifecycle of LLM output treated as a fragile serialization protocol. And it explains why that file is enormous: half the complexity of “getting a model to write markdown” is defending against how the model actually writes it.
The same defensive spirit is in the Rust backend. The Cargo.toml is compiled with panic = "unwind" — not abort — on purpose, and there’s a panic_guard.rs that explains why:
Third-party parsers (pdf-extract/lopdf, docx-rs, calamine…) are known to panic on malformed input instead of returning Err. Under
panic = "abort"this kills the entire app.
So every Tauri command goes through a catch_unwind that converts the panic into an error you can show on screen. A corrupt PDF must not be able to bring down the window. These are the decisions you never see in a README but that separate a demo from an app.
The honest gotcha: README vs code
The README describes the query pipeline with a context-budget split of “60/20/5/15” (wiki / history / index / system). But if I open context-budget.ts, the real numbers are different:
const RESPONSE_RESERVE_FRAC = 0.15
const INDEX_BUDGET_FRAC = 0.05
const PAGE_BUDGET_FRAC = 0.5
50% goes to pages, 5% to the index, 15% is reserved empty to make room for the model’s response. History and system prompt have no imposed budget — the comment in the code says it plainly: “not enforced as a single budget, the leftover acts as headroom.” It’s a classic README/reality drift, the kind that makes a project credible rather than suspect: the code is more precise and more modest than the documentation.
Another number to take with a grain of salt: the README claims vector search raises recall “from 58.2% to 71.4%.” In the repo I found neither the benchmark nor the methodology — it’s a claim, not a reproducible result. I flag it because the guide asks for honesty, and this is exactly the kind of figure that needs verifying before you repeat it.
Where it stands
It’s shipped and alive: binaries for macOS (ARM+Intel), Windows and Linux via GitHub Actions, GPL-3.0, the latest commit fixes the PDFium fetch for Intel builds. There’s a local HTTP API on 127.0.0.1:19828, loopback only, protected by a token with constant-time comparison (constant_time_eq) against timing attacks and with a kill-switch. On top of it runs a bundled MCP server, so an agent like Claude Code can query your local wiki. Nine LLM providers in the code — the README announces five (OpenAI, Anthropic, Google, Ollama, custom), but the code adds Azure, MiniMax, Claude-CLI and Codex-CLI: perfect irony for a project whose thesis is read the code, not the README. Serial ingest queue with retry up to 3 times and an SHA256 cache to skip unchanged sources. It’s not a toy.
What I learned looking inside someone else’s house
Three things, concrete. First: an abstract pattern and its instance live in different universes of complexity. Karpathy writes a page that says “the LLM does the bookkeeping”; nashsu writes 87 modules to make sure that bookkeeping doesn’t produce corrupt files. The distance between the two is the whole project.
Second: the boundary with the LLM is a protocol, and protocols must be parsed with paranoia. parseFileBlocks with its numbered hazards is more instructive than any “structured output” tutorial — because it shows the real ways output breaks: CRLF, truncated streams, delimiters inside code fences.
Third, the most useful for me: read the code, not the README. The promised 60/20/5/15 split becomes 50/5/15 in reality, more honest and simpler. In projects that aren’t yours, that gap is where you learn something.
Updates
2026-09The cron that skipped versions
The fork I use to produce the unofficial Intel builds had a hole I hadn’t seen: the nightly scheduled job only looked at the latest upstream tag, so any version released and superseded between two cron runs never got built — it stayed a permanent gap in the release list. I rewrote the resolution job: now it enumerates all upstream tags above a minimum threshold, discards the ones that already have a <tag>-intel release on the fork, and emits the full list of missing ones as a JSON array. The actual build became a matrix with max-parallel: 1 and fail-fast: false, because behind it there’s a single self-hosted Intel Mac and one broken tag must not take the others down with it. I do the version comparison with sort -V instead of inventing a semver parser in bash, which is the kind of shortcut that hasn’t betrayed me yet. The manual trigger, on the other hand, stays targeted at a single tag and builds even if the release already exists, so I can redo a bad build without fighting the skip logic. This is fine: the next night two versions come out close together, the cron takes both instead of silently losing one.
Not mine: github.com/nashsu/llm_wiki — GPL-3.0, based on Karpathy’s LLM Wiki pattern.

