**From Agent Workflows to a Deterministic Markdown Compiler**
In late 2025 and early 2026, Andrej Karpathy popularized the idea of using LLM agents to build and maintain a personal wiki: an agent loop that reads local markdown notes, makes recursive plan/decide/write calls, and produces a cross‑referenced knowledge base. The approach feels natural because it mirrors how we sometimes organize notes—iterative, LLM‑assisted refinement—but it carries real costs when the source material is already local and text‑based. Paying tokens for orchestration, wrestling with network latency, and accepting non‑deterministic outputs turned out to be unnecessary for my use case.
That realization motivated a different path: a pure‑Python markdown wiki compiler that transforms a folder of messy, locally stored notes into a linked, linted, deterministic knowledge base—no LLMs, no embeddings, no external APIs, and nothing beyond the standard library. The result is a pipeline that prioritizes correctness, reproducibility, and performance, and whose full source code, tests, and benchmark outputs are publicly available.
This article walks through the complete design of that compiler, the real bugs encountered during development, and the benchmarks that prove deterministic, deterministic outputs across platforms.
## The Compiler Mindset
The core shift is simple but powerful: an agent decides what your wiki might look like; a compiler guarantees what it must look like.
Instead of recursive LLM calls, the pipeline consists of deterministic stages:
1. Parse raw notes with a robust regex extractor.
2. Build a bidirectional linking graph using a word‑indexed phrase matcher.
3. Rewrite section‑by‑section, preserving hand‑written notes while regenerating compiler‑owned sections.
4. Lint the output with rule‑based checks that catch its own class of errors.
Because every stage is a pure function of its inputs, the pipeline always produces identical results for identical source files—across machines, across runs, and across time.
## Why Zero Dependencies Matters Here
Everything runs on the Python standard library: `re`, `os`, `pathlib`, `unittest`, and basic data structures. No `sentence‑transformers`, no vector databases, no HTTP clients, no third‑party packages.
This is not a purity argument—it is a practical one. When your source is local text, the remaining work is parsing, string manipulation, and graph traversal, which standard library tools handle perfectly. Adding heavy ML dependencies would introduce install friction, platform‑specific failures, and yet another class of bugs unrelated to the actual content of your notes.
## The Problems with Agent‑Driven Wikis
Agentic pipelines sound powerful, but they introduce three unavoidable costs when applied to restructuring existing text:
– Cost: Every re‑run repeats token‑spending reorganization work, even when only a few files changed.
– Latency: Network round‑trips and model compute add unavoidable delay for purely mechanical tasks.
– Non‑determinism: Small changes in model judgment can produce different graphs from the same input, which is unacceptable for a source of truth.
None of these issues arise in a parsing‑based compiler, which treats your notes as data rather than as a reasoning problem.
## Step 1: Robust Regex Metadata Extraction
Real note folders are messy. Headers may be `# Title`, `TITLE`, or missing entirely; metadata may appear anywhere. The extractor handles this by trying a series of fallback strategies and defaulting to the filename when necessary. This ensures the pipeline never crashes on imperfect input and instead works with whatever structure is present.
## Step 2: A Scalable Graph Builder (and the First Real Bug)
Early versions of the linker used a naïve `O(n²)` approach: for each entity name, run a regex over every file. With 100 files this was fine, but at 1,000 files the runtime hit 4.4 seconds, and at 5,000 files it exceeded 107 seconds. The absence of network calls did not imply efficiency.
The fix was to replace pairwise scanning with a word‑indexed phrase matcher. By tokenizing each file once and checking only candidate entity names starting with the current word, complexity dropped dramatically:
– 100 files: ~46 ms → ~2 ms
– 1,000 files: ~4,400 ms → ~33 ms
– 5,000 files: ~107,000 ms → ~492 ms
The middle row in this progression, based on an intermediate combined‑regex attempt, shows why cosmetic optimizations are not enough; only a change in algorithmic complexity delivers real scalability.
## Step 3: Section‑Aware Rewriting with Hand‑Edit Preservation
The rewriter works at the section level, not an abstract syntax tree. It keeps existing `## Notes` sections verbatim while regenerating compiler‑owned sections (Metadata, Related, Referenced By, Body). This guarantees that human annotations survive recompilation.
The process is simple on paper but powerful in practice:
– If a file already exists, extract its Notes block.
– Regenerate all compiler sections from the graph and metadata.
– Write back a fully updated page that preserves user content.
I validated this behavior by manually editing a generated page, recompiling, and confirming that only the expected sections changed, both on Linux and on Windows.
## Step 4: The Linter and a Subtle Second Bug
The linter is intentionally dumb: it scans for broken links and identifies files with zero incoming references. It is not an LLM judge, yet a bug in its counting logic once made the wiki appear perfectly linked when it was not.
The problem was that the linter counted links inside the `Referenced By` section, which contains the same link syntax but in reverse direction. The fix was to limit counting to the `Related` section only, isolating genuine outgoing edges.
A targeted regression test, named `test_linter_does_not_miscount_referenced_by`, ensures this class of bug can never silently reappear.
## The Full Test Suite: 17 Tests, Stdlib Only
The test suite covers each pipeline stage in isolation and includes full end‑to‑end checks:
– Unit tests with hand‑crafted `Entity` objects.
– Integration tests that verify idempotency and deterministic output.
– A regression test for the linter bug.
All 17 tests use `unittest` and produce consistent, predictable results with no external fixtures.
## Benchmarking on Linux and Windows
I ran the complete pipeline at three corpus sizes on both a Linux container and a Windows 10 machine, using the same seeded input.
### Aggregate Timing by Stage (5,000 files)
– Extract: ~1.4s (11%)
– Graph: ~0.6s (5%)
– Rewrite: ~3.4s (28%)
– Lint: ~7.0s (56%)
Lint dominates runtime not because of complex logic, but because it incurs per‑file disk I/O on two different operating systems. Windows is consistently slower, partly due to additional filesystem guardrails like real‑time scanning.
### Scaling Behavior
– 100 files: ~171 ms, 13 orphans
– 1,000 files: ~1.80s, 133 orphans
– 5,000 files: ~12.44s, 644 orphans
Scaling is sub‑linear but not perfectly linear, mainly due to the word‑indexed matcher’s interaction with larger vocabularies. Even at 5,000 files, a full recompile finishes in about twelve seconds on standard hardware—zero tokens, zero network.
### Determinism Across Platforms
Orphan counts matched exactly between Linux and Windows on every run. This is the core promise of a compiler: reproducible outputs, independent of execution environment.
## Known Limits
– **Unstructured input**: Highly chaotic or multi‑language notes may require richer extraction than regex can provide.
– **Semantic linking**: Exact‑string matching cannot infer that “gradient descent” and “the optimization step” refer to the same concept. This is a conscious boundary; extending the system with semantic analysis would require a clearly separated enhancement layer.
The framework is intentionally mechanical. Where judgment is needed, that is a place for human writing or future semantic extensions—not for an LLM to second‑guess your organization.
## Closing Thought
When your input is deterministic, your pipeline should be too. This compiler proves that a purely Pythonic, dependency‑free approach can replace agentic loops for the majority of personal knowledge management work, delivering speed, determinism, and transparency without paying token costs.
If your input is already text on disk, you do not need an agent. You need a parser, a graph, and a linter that tells you the truth about its own output.
—
**Full source code, benchmarks, and all 17 tests are available in the accompanying repository.**
—
**References**
[1] Andrej Karpathy, original post describing LLM‑driven personal knowledge bases, X, April 2026.
[2] Andrej Karpathy, “LLM Wiki” idea file (GitHub Gist), April 2026.



