How It Works — Written Guide

📄 Written guide · 13 chapters · read at your own pace — every chapter paired with the real source files it describes.

How this site works, chapter by chapter — each one paired with the real source files it describes. An audio guide → narrating the same walkthrough is being re-recorded.

01. What this site is

The site is a self-paced AI-engineering curriculum delivered as a single Next.js App Router application deployed to Vercel. Every lesson, from "Phase 1" fundamentals to the production‑ship phase, is a plain Markdown file under content/ — for example, content/phase-2-prompting.md. There is no database of lesson text at rest; the filesystem is the authoritive store.

The architecture’s backbone is one ordered list: LESSON_SLUGS in lib/articles.ts. Each slug’s position in that array (1‑indexed) defines its lesson number. The same array also drives the audiobook auto‑advance sequence — the “flow order” that the persistent player follows. Because the array is used for both numbering and sequencing, the two are always in sync: lesson 4 follows lesson 3 in the player because the slug at index 4 follows the slug at index 3. This single source of truth eliminates drift between a separate numbering list and a separate play‑order list. LESSON_NUMBER is a computed lookup object derived from the same array, so every getLessonBySlug call can retrieve the correct number without a second config.

The lesson read path lives in lib/data.ts and implements a three‑tier fallback designed for a Vercel‑deployed app that may or may not have a D1 database attached. On each read (getLessonBySlug), the first tier attempts to fetch from a D1 content‑cache — a writable database that lets authors push updated lesson content without redeploying the entire site. If D1 is unavailable or empty, the second tier loads from a JSON bundle (export-content) that was generated at build time from the same Markdown files. This bundle is fast and cacheable but static. The final fallback is the filesystem parser in lib/articles.ts itself, which reads the .md files directly at runtime — useful during local development or when no build output exists.

Why not just rely on one tier? Each tier buys a different operational property. D1 gives hot‑content updates without a build — ideal for corrections or additions between deployments. The JSON bundle makes cold starts fast and survives D1 outages. The filesystem parser keeps the app runnable out of the box on a fresh clone without any infrastructure setup. The fallback chain is expressed with a simple pattern: try the fast path, catch silently and move to the next. The same three‑tier pattern is used for getAllLessons, getCategoryMeta, and getGroupedLessons. This layered approach means the site can operate in environments ranging from a fully‑provisioned production Vercel deployment, through preview branches with D1 replicas, down to a local npm run dev with nothing but a content/ directory.

The Lesson interface itself is lean: slug, number, title, category, excerpt, difficulty, wordCount, readingTimeMin, url. The difficulty field is never stored — it is derived positionally by getDifficulty in lib/articles.ts, which slices each category’s lesson range into beginner, intermediate, and advanced bands. This keeps the data model immutable and avoids stale difficulty metadata. The entire curriculum — numbering, ordering, difficulty classification, and audiobook spine — flows from that single LESSON_SLUGS array, a design that trades flexibility for coherence and makes the player’s auto‑advance both predictable and trivially debuggable.

ELI5 — explain it simply
1
Gist

It's a website that teaches you how to become an AI engineer. It's one app, and every lesson is just a text file — there's no lesson database sitting underneath it.

2Morego a level deeper

The lessons are markdown files in a folder, and one ordered list decides both what number each lesson gets and what plays next in the audiobook, so the two can never drift apart. When a page needs a lesson it tries the live database first, then a copy baked in at build time, then the raw file on disk — whichever answers first wins.

3Deepthe full mechanics

It's a Next.js App Router app deployed to Vercel. LESSON_SLUGS in lib/articles.ts is the single spine: array position (1-indexed) is the lesson number, LESSON_NUMBER is derived from it, and the same order drives the player's auto-advance. getLessonBySlug in lib/data.ts is a three-tier try/catch fallback — D1 content cache, then the build-time export-content JSON bundle, then the filesystem parser — and difficulty is never stored, just sliced positionally by getDifficulty. One array for everything trades flexibility for coherence.

Code references

Where this chapter's machinery lives in the repo:

lib/articles.ts:8-19

The typed Lesson metadata record the taxonomy grid and lesson pages are built from.

typescript
export interface Lesson {
  slug: string;
  fileSlug: string;
  number: number;
  title: string;
  category: string;
  excerpt: string;
  difficulty: DifficultyLevel;
  wordCount: number;
  readingTimeMin: number;
  url: string;
}

lib/articles.ts:43-53

LESSON_SLUGS — the ordered spine: a slug's position sets its lesson number and its audiobook play order.

typescript
// `*` slugs are new Cloudflare-specific pilot chapters.
const LESSON_SLUGS = [
  // Phase 1 · Foundations & Model Inference (1-9)
  "ai-on-cloudflare-workers", // *
  "roadmap",
  "workers-ai-models", // *
  "transformer-architecture",
  "tokenization",
  "model-architectures",
  "scaling-laws",
  "inference-optimization",

lib/data.ts:25-29

The lesson read path the [slug] route actually uses: D1 content_cache first, bundled JSON export second, markdown parser only as the dev-time fallback (see getLessonBySlug below this comment).

ts
// Lesson content resolves D1 first (dynamic, written by the `kg.sync_d1`
// publish step → new/edited content shows on refresh, no redeploy), then the
// build-bundled JSON exported by `export-content`, then the markdown parser in
// ./articles as the last-resort fallback. Each layer is independent, so an
// unconfigured/empty D1 silently falls through.
STUDY AIDSevidence-backed memory techniques
Recall check

What single array in lib/articles.ts drives lesson numbering, ordering, difficulty classification, and audiobook auto-advance?

Show answer

LESSON_SLUGS

02. Build and deploy

The build pipeline begins in package.json, where the prebuild script runs before build. It invokes tsx scripts/prebuild.ts, a gate that executes a battery of content‑contract tests—among them test:audio:all and test:linkedin—so that any broken assertion aborts the process before an artifact is produced. This is the central principle: gate the build, not the deploy. If a content error slips past local development, it is caught here, not after publication.

The build command itself is next build --webpack. The --webpack flag is mandatory because the production bundle must embed markdown registries as raw strings. The next.config.ts file defines a webpack rule using asset/source that matches paths under data/app-prep, specs/case-study/deep-dives, and other directories. Deployed serverless functions cannot fs.readFile repository files at runtime—the filesystem is read‑only—so these sources must be statically inlined during compilation. Without that rule, the production build would silently produce empty pages.

Deployment is not a simple vercel deploy --prod. The package.json defines deploy as bash scripts/deploy.sh, and this script is the only mechanism used to ship to Vercel. The reason is structural: the app is a git submodule inside the v9ai/ai-apps monorepo. A standard Vercel upload performs a git‑aware transfer that skips submodule contents, yielding a broken deployment. The script circumvents this by building locally (files exist on disk) and then shipping a single .tgz archive with vercel deploy --prebuilt --prod --archive=tgz. That archive also bypasses the 5000‑file upload cap of the free tier (api-upload-free).

The script must run from the monorepo root, not from the app directory. This is driven by next.config.ts, which sets outputFileTracingRoot to path.join(__dirname, "../..")—the monorepo root. The Next.js builder resolves traced dependencies relative to that root; running from apps/ai-engineer-roadmap would cause it to look for paths twice (e.g., apps/ai-engineer-roadmap/apps/…), triggering a failure after a successful build. The script therefore changes directory to the monorepo root at the start of its execution.

Several guardrails are embedded in scripts/deploy.sh. Before any build step, it reads the buildCommand key from the app’s vercel.json and verifies it is exactly "pnpm run build". A background content loop sometimes rewrites that value to a broken turbo filter; the guard fails fast with a clear message, preventing a confusing mid‑build crash.

After the deployment, the script verifies the apex domain ai-engineer-roadmap.xyz. It does not trust a bare HTTP 200—any server can return that. Instead, it sends a cache‑busted request and checks for the x-vercel-id response header, which only a Vercel edge sets. If the header is missing, the script prints the exact remediation: vercel alias set <deploy_url> ai-engineer-roadmap.xyz and exits with a failure. Only when the header is present and the status code is 200 does the script succeed.

The entire pipeline—from prebuild through the deploy script’s own checks—ensures that a broken artifact never reaches production. The build is gated by tests, the build itself embeds required assets, and the deploy script validates both the build command and the final alias. Every failure surfaces before any user sees a stale or empty page.

ELI5 — explain it simply
1
Gist

Run the tests first, then build, then ship the finished build as one bundle — and afterwards actually ask the live site whether it's really there.

2Morego a level deeper

A gate runs the content tests before anything is built, so a broken artifact never gets made in the first place. Shipping isn't the ordinary one-line publish command: this app is a sub-repo inside a bigger one, and the normal upload would quietly leave its files out, so a script builds everything locally and uploads a single packed archive instead. Then it checks the real domain answers, not just that something answered.

3Deepthe full mechanics

prebuild chains test:audio:all, test:linkedin and test:llamaindex-primer before next build --webpack — the webpack flag is required so markdown registries get inlined with asset/source, since serverless functions can't fs.readFile at runtime. npm run deploy is bash scripts/deploy.sh, never vercel deploy --prod: it must run from the monorepo root (outputFileTracingRoot), asserts vercel.json's buildCommand is still "pnpm run build" in case a background loop rewrote it, and ships vercel deploy --prebuilt --prod --archive=tgz (which also dodges the free tier's 5000-file cap). Verification demands the x-vercel-id header on a cache-busted apex request — a bare 200 proves nothing — and fails with the exact vercel alias set remedy. The theme: gate the build, not the deploy.

Code references

Where this chapter's machinery lives in the repo:

package.json:33-36

The script chain the deploy actually runs: prebuild gates the build behind the audio, LinkedIn and primer test suites, build pins the webpack bundler, and deploy delegates to scripts/deploy.sh.

json
    "prebuild": "npm run test:audio:all && npm run test:linkedin && npm run test:llamaindex-primer",
    "build": "NODE_OPTIONS=--disable-warning=DEP0040 next build --webpack",
    "start": "next start",
    "deploy": "bash scripts/deploy.sh",

vercel.json:2-8

The project contract: a plain pnpm run build build command (no turbo filter), turbo-ignore deciding whether a push is even worth building, and git.deploymentEnabled.main = false — pushing to main never ships; the deploy script does.

json
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "buildCommand": "pnpm run build",
  "devCommand": "pnpm run dev",
  "installCommand": "pnpm install --no-frozen-lockfile",
  "framework": "nextjs",
  "ignoreCommand": "npx turbo-ignore",
  "git": { "deploymentEnabled": { "main": false } },

next.config.ts:42-55

The asset/source webpack rule that inlines the markdown seeds and deep-dive references as raw strings — the deployed functions cannot fs-read repo files at request time, which once made pages render empty in prod while every local build passed.

typescript
  // Markdown seeds (data/app-prep/*.md) and the deep-dive references
  // (specs/case-study/deep-dives/*.md) import as raw strings so their
  // registries can bundle them prod-safe — the deployed serverless functions
  // cannot fs-read repo files at request time (fs succeeds at build time,
  // which made the deep-dive page silently render EMPTY in prod while
  // passing every local build). Webpack covers the production build
  // (`next build --webpack`); the turbopack rules cover `next dev`.
  webpack(config) {
    config.module.rules.push({
      test: /(data[\\/]app-prep|specs[\\/]case-study[\\/]deep-dives|specs[\\/]cadence|specs[\\/]daily-routine)[\\/].*\.md$/,
      type: "asset/source",
    });
    return config;
  },

scripts/deploy.sh:4-11

Why this is a prebuilt deploy and not a plain vercel deploy --prod: this app is a git submodule, and a git-aware upload skips submodule contents. Building locally and shipping one --archive=tgz sidesteps that and the file-count upload cap.

bash
#

#   This app is a git SUBMODULE (v9ai/ai-engineer-roadmap) inside the
#   v9ai/ai-apps monorepo. A plain `vercel deploy --prod` from the monorepo
#   root does a git-aware upload that SKIPS submodule contents -> broken
#   deploy. The prebuilt flow below builds locally (files exist on disk) and
#   ships a single tgz archive, which sidesteps BOTH the submodule-upload gap
#   and the >5000-file free-tier upload cap (`api-upload-free`).
STUDY AIDSevidence-backed memory techniques
Cloze

Instead, it sends a cache‑busted request and checks for the  ____  response header, which only a Vercel edge sets.

Show answer

x-vercel-id

03. The content grounding pipeline

Most guide pages are generated not by writing from memory but by retrieving relevant fragments from the actual source code. This content‑grounding pipeline lives in roadmap‑kg/kg/memory_common.py. At its core is build_cached_index, the drop‑in replacement for an ad‑hoc VectorStoreIndex.from_documents(…). Instead of embedding everything from scratch each time, it builds a persistent, searchable index over the central store — the same Qdrant collection that the “Explain” service uses, with incremental sync by ref_doc_id so unchanged documents skip split/embed/insert entirely.

Before documents enter the index they are chopped into manageable segments. The pipeline uses a _default_splitter – a SentenceSplitter from LlamaIndex – that cuts each document into chunks of tokens. Every chunk overlaps with its neighbours, so a fact that straddles a chunk boundary (e.g. a sentence that ends one chunk and continues in the next) is still captured in full by at least one chunk. Without that overlap, the retrieval step would lose the second half of the thought, and the generated explanation would be missing the conclusion. The chunking behaviour is configurable via the splitter parameter that build_cached_index accepts; callers can supply a custom splitter, and the defaults are what every lane in the app uses by omission.

Once chunked, the documents are handed to sync_documents, which computes a content‑keyed synthetic ref_doc_id for each. An unchanged document produces the same id, so the sync layer skips split/embed/insert entirely — only new or edited documents are processed, and edited documents are replaced in place. The sync writes the resulting vectors and payloads into the namespace’s STABLE Qdrant collection. Because the index is scope‑filtered (_attach_scope), each lane retrieves over exactly the documents it passed, even though the physical collection is shared.

If Qdrant is unavailable (another process holds the lock, or VECTOR_STORE=memory is set), build_cached_index falls back to an in‑memory VectorStoreIndex. The fallback is loud when Qdrant was intended; it hard‑fails under REQUIRE_QDRANT=1. But the deliberate opt‑out for hermetic tests or benchmark lanes stays quiet. In either case, the result is a VectorStoreIndex whose retrieval is grounded in real source text, not author memory.

Chunking plus overlap is the precondition for that grounded generation. A single long‑form explanation often weaves several facts together; if a key sentence fell on a chunk boundary, the retriever would never see its conclusion, and the generated text would invent a gap. The overlap guarantees that every sentence belongs to at least two chunks, so the full context survives into the vector index. The entire pipeline — _default_splitter, build_cached_index, sync_documents, the Qdrant store — is designed so that every guide page is a retrieval‑augmented generation over the real codebase, not a hand‑written fiction.

ELI5 — explain it simply
1
Gist

Chop the source material into index cards, and remember the cards so you never have to redo the work on the next run.

2Morego a level deeper

A splitter cuts each file into overlapping ~512-token chunks; every chunk is turned into a vector; those vectors go into a searchable index. Identical runs reuse the saved vectors instead of recomputing them.

3Deepthe full mechanics

build_cached_index splits docs with SentenceSplitter(512/64), attaches cached vectors via embed_nodes_cached, then builds a VectorStoreIndex from the pre-embedded nodes so LlamaIndex never re-embeds; an empty namespace shares one SQLite vector store across all lanes and the explain service.

Code references

Where this chapter's machinery lives in the repo:

roadmap-kg/kg/memory_common.py:807-833

build_cached_index: split → cached embed → index built from pre-embedded nodes (never re-embeds).

python
    )


def _attach_scope(index, doc_ids) -> None:
    """Scope every retriever built off ``index`` to the lane's own docs. On the
    STABLE shared collections a lane's index is a view over kg-corpus / kg-ns-*,
    so an unfiltered ``as_retriever()`` / ``as_query_engine()`` would surface
    every other lane's nodes. Wrapping the instance's ``as_retriever`` (which
    ``as_query_engine`` calls internally) injects the lane's ``doc_id`` filter as
    the DEFAULT — behavioral parity with the old one-collection-per-corpus
    isolation. A caller-passed ``filters=`` REFINES the scope (ANDed with it)
    rather than replacing it — a lane filtering on ``file == x.py`` must match
    its own x.py chunks, never another lane's file of the same name.
    ``store_nodes`` reads the same ``_kg_scope_filters`` attribute to hydrate
    only the slice."""
    filt = scope_filters(doc_ids)
    index._kg_scope_filters = filt
    orig_as_retriever = index.as_retriever

    def scoped_as_retriever(*args, **kwargs):
        user = kwargs.get("filters")
        if user is None:
            kwargs["filters"] = filt
        else:
            from llama_index.core.vector_stores.types import (
                FilterCondition,
                MetadataFilters,

roadmap-kg/kg/memory_common.py:796-804

_default_splitter: the shared SentenceSplitter with env-tunable 512/64 chunking.

python
        n.embedding = vec


def _default_splitter():
    """The same SentenceSplitter the grounding lanes built inline — honours the shared
    CHUNK_SIZE / CHUNK_OVERLAP env knobs so swapping the helper in changes nothing."""
    from llama_index.core.node_parser import SentenceSplitter

    return SentenceSplitter(
STUDY AIDSevidence-backed memory techniques
Quiz

What does the section identify as the precondition for grounded generation in the content grounding pipeline?

Options: Chunking plus overlap is the precondition for that grounded generation. · The in-memory fallback is the precondition for that grounded generation. · Scope filtering via _attach_scope is the precondition for that grounded generation. · Persistent Qdrant collection is the precondition for that grounded generation.

Show answer

Chunking plus overlap is the precondition for that grounded generation.

04. Local-first embeddings

The embedding strategy powering the guide’s memory lanes and the “Explain this” service is entirely local — no embedding API key exists anywhere in the stack. The model is BAAI/bge-small-en-v1.5 (384 dimensions) from the FastEmbed family, downloaded once as ONNX weights and then run in‑process. The function kg.llm.make_embed constructs this model; because the weights live on disk and the inference engine runs locally, every embedding call is a local computation with zero network cost. The constructed model is cached for the life of the process keyed by the EMBED_MODEL environment variable (defaulting to that BAAI identifier), so repeated calls to make_embed inside a single run return the same object — a pattern called a “process‑cached FastEmbed model” that makes per‑text embedding nearly free after the first call.

Even though the model itself is fast, a pipeline that re‑embeds an entire corpus on every grounding run would still waste CPU. The persistent cache in roadmap‑kg/kg/memory_common.py solves that. Every text that gets embedded passes through embed_nodes_cached (the shared helper for memory lanes) or the lower‑level cached_text_embeddings. Each text is hashed together with the active model name using embed_cache_key (a deterministic SHA‑256 of model + "\x1f" + text), and the resulting vector is stored in a local SQLite database at .embed‑cache/embeddings.db (configurable via EMBED_CACHE_DB or MEMORY_EMBED_CACHE_PATH). The cache is incremental: editing one chapter re‑embeds only that chapter’s text, leaving every other cached vector untouched. De‑duplication within a single call also prevents identical texts (e.g. repeated prompts) from being embedded more than once.

This design buys two critical properties. First, the pipeline runs with the network unplugged: no API calls, no keys to rotate, no rate limits. Second, re‑running the same corpus (or a largely unchanged one) costs near zero — the cache satisfies almost every lookup, and only newly introduced or edited texts trigger an actual embedding. The environment variable EMBED_CACHE=0 can disable the persistent cache for benchmarks, but in normal operation the combination of an in‑process model and a one‑time‑per‑text SQLite store keeps the whole system local and fast.

ELI5 — explain it simply
1
Gist

The site turns text into numbers on its own machine, for free, and remembers the numbers so it never repeats the work.

2Morego a level deeper

A small local model makes the vectors — no cloud API — and each vector is filed under a fingerprint of the model plus the text, so identical text is looked up instead of re-computed.

3Deepthe full mechanics

make_embed builds an in-process FastEmbed bge-small (384-dim ONNX); cached_text_embeddings stores packed float32 in SQLite keyed by sha256(model ‖ text); a float32 round-trip guarantees miss/hit parity, and the key matches the explain service so they share one database.

Code references

Where this chapter's machinery lives in the repo:

roadmap-kg/kg/llm.py:183-200

make_embed: in-process FastEmbed bge-small, instance cached per EMBED_MODEL.

python
                pass
        _repeat_penalty = os.environ.get("LLM_REPEAT_PENALTY", "").strip()
        if _repeat_penalty:
            try:
                _extra_body["repeat_penalty"] = float(_repeat_penalty)
            except ValueError:
                pass
        if _extra_body:
            # Merge into any pre-existing additional_kwargs/extra_body rather than
            # clobber it, so these knobs compose with anything set upstream.
            _addl = dict(local_kwargs.get("additional_kwargs") or {})
            _addl["extra_body"] = {**(_addl.get("extra_body") or {}), **_extra_body}
            local_kwargs["additional_kwargs"] = _addl
        return OpenAILike(**local_kwargs)

    # DeepSeek-via-CF-gateway fallback. _gateway_model() adds the `deepseek/` prefix
    # the compat surface needs (DeepSeekLLM does not prefix on its own).
    return _build_deepseek(

roadmap-kg/kg/memory_common.py:643-655

embed_cache_key: sha256(model ‖ text) — model-namespaced so a model swap is a clean miss.

python
    return os.environ.get("EMBED_CACHE", "").strip().lower() not in ("0", "false", "no", "off")


def embed_cache_key(text: str, *, model: str | None = None, namespace: str = "") -> str:
    """Cache key for one text's embedding — namespaced by model (and optional corpus
    ``namespace``). With an EMPTY namespace the key is byte-identical to the explain
    service's ``indexing.py:_embed_key`` (``sha256(model ‖ text)``), so the two share
    rows in the one unified DB; a non-empty namespace inserts the corpus partition. A
    model swap is always a clean miss (model folded into the key)."""
    h = hashlib.sha256()
    if namespace:
        payload = f"{model or _embed_model_name()}\x1f{namespace}\x1f{text}"
    else:

roadmap-kg/kg/memory_common.py:734-759

hit/miss path: embed only the misses, float32 round-trip before caching for miss/hit parity.

python
                if k not in cached and k not in miss_first_idx:
                    miss_first_idx[k] = idx

            if miss_first_idx:
                if verbose:
                    print(
                        f"[mem-embed-cache] {len(cached)} hit, "
                        f"{len(miss_first_idx)} to embed",
                        flush=True,
                    )
                miss_keys = list(miss_first_idx)
                miss_texts = [texts[miss_first_idx[k]] for k in miss_keys]
                new_vecs = embed_model.get_text_embedding_batch(
                    miss_texts, show_progress=verbose
                )
                rows = []
                for k, vec in zip(miss_keys, new_vecs):
                    # Round-trip through float32 BEFORE caching in-memory so a freshly
                    # embedded (MISS) text returns the SAME value a later HIT will read
                    # back from the float32 BLOB — no miss/hit precision asymmetry.
                    blob = array.array("f", vec).tobytes()
                    a = array.array("f")
                    a.frombytes(blob)
                    cached[k] = a.tolist()
                    rows.append((k, blob))
                con.executemany(
STUDY AIDSevidence-backed memory techniques
Cloze

The combination of an  ____  and a one‑time‑per‑text  ____  keeps the whole system local and fast.

Show answer

in‑process model, SQLite store

05. The LLM egress tiers

The language model is reached through make_llm in roadmap-kg/kg/llm.py, which resolves exactly one of three egress tiers in strict first‑available‑wins order. The function never tries multiple tiers in sequence; instead it returns as soon as it finds a configured path, and the choice is locked at startup so every request in the process uses the same tier. This fixed‑precedence design prevents a subtle class of bugs that “try whatever is around” heuristics suffer from—for example, a developer who sets DEEPSEEK_API_KEY for production but inadvertently leaves a LLM_BASE_URL lying around in a .env file would silently send traffic to a local server rather than the intended DeepSeek API. By checking the direct key first and returning immediately, the code guarantees that a stray LLM_BASE_URL cannot quietly pull a request onto a local model.

Tier 1 – DeepSeek’s own API (highest precedence).
When the environment variable DEEPSEEK_API_KEY is set, make_llm builds a DeepSeekLLM (an OpenAILike subclass defined in kg/deepseek_llm.py) pointed at api.deepseek.com. The model id is read from DEEPSEEK_MODEL_DIRECT (default: deepseek-v4-flash), and if the legacy model deepseek-chat is still set, it is transparently mapped to its successor deepseek-v4-flash. The key is sourced out‑of‑band (one‑shot env or an out‑of‑repo env file) and must never be persisted in the public repo’s .env, because the auto‑commit bot could stage it. This tier enables native function calling (DeepSeekLLM defaults is_function_calling_model=True), which means agents can drive tools natively without falling back to ReAct. The context window defaults to one million tokens (adjustable via DEEPSEEK_CONTEXT_WINDOW), and the completion length can be capped with max_tokens or the LLM_MAX_TOKENS env override.

Tier 2 – Local OpenAI‑compatible server.
If no DEEPSEEK_API_KEY is present but LLM_BASE_URL is set (stripped of trailing slashes), the code instantiates plain OpenAILike against that local server—typically llama.cpp’s llama-server or LM Studio. Because quantized GGUF models tool‑call unreliably, this tier keeps is_function_calling_model=False; agents fall back to ReAct for tool use. An extra body with {"stop": …} is injected when needed (e.g., for JSON‑mode extraction). The local path is intentionally not qualified as a fallback—if the key is absent and no LLM_BASE_URL exists, the code does not fall through to a default localhost; it goes to tier 3 instead.

Tier 3 – Cloudflare AI Gateway.
The last resort calls DeepSeek through the Cloudflare AI Gateway’s OpenAI‑compatible /compat endpoint. The base URL defaults to gateway.ai.cloudflare.com/v1/a036f50e02431c89170b8f977e982a3d/ai-gateway/compat (overridable via CF_AIG_BASE_URL). The model id is provider‑prefixed with deepseek/ by _gateway_model() so the compat surface routes correctly; the default model is deepseek-v4-pro (configurable through DEEPSEEK_MODEL). The gateway token is read from CF_AIG_TOKEN and the function panics with RuntimeError if it is missing. This tier inherits the same DeepSeekLLM class and native function‑calling capability as tier 1, but the endpoint is routed through Cloudflare’s infrastructure for latency monitoring and fallback.

Across all tiers, the timeout and retry count are tunable per tier via env overrides (DEEPSEEK_TIMEOUT, DEEPSEEK_MAX_RETRIES for the DeepSeek tiers; LLM_TIMEOUT, LLM_MAX_RETRIES for the local server). The _selftest in kg/llm.py verifies the per‑call timeout and retry resilience layer offline, without burning a real API call. The final call graph is simple: after make_llm returns, configure_settings assigns the chosen LLM to llama_index.core.Settings.llm, so every downstream component—property graph extraction, retrievers, query engines—uses that one LLM and never demands an OPENAI_API_KEY.

ELI5 — explain it simply
1
Gist

The site can reach the AI model three ways — a paid cloud key, a model on your own machine, or a gateway — and it picks the first one that's available.

2Morego a level deeper

If a DeepSeek key is set, it calls DeepSeek directly; otherwise if a local server address is set it uses that (fully offline); otherwise it goes through Cloudflare's AI Gateway. Embeddings are always local, so only the writing step needs the internet.

3Deepthe full mechanics

make_llm resolves DEEPSEEK_API_KEY → direct, then LLM_BASE_URL → local OpenAILike, then CF_AIG_TOKEN → gateway /compat; all keep is_function_calling_model=False (text path), and configure_settings pins Settings.llm/embed_model so no component reaches for OpenAI.

Code references

Where this chapter's machinery lives in the repo:

roadmap-kg/kg/llm.py:3-13

The three egress tiers in precedence order (module docstring).

python
LLM egress has three tiers, highest precedence first (see ``make_llm``):
  1. ``DEEPSEEK_API_KEY`` set → DeepSeek's own API (api.deepseek.com) directly.
  2. else ``LLM_BASE_URL`` set → that OpenAI-compatible LOCAL server (llama.cpp's
     ``llama-server``, LM Studio, or any ``/v1`` shim, NOT MLX).
  3. else → DeepSeek via the Cloudflare AI Gateway's OpenAI-compatible ``/compat``
     endpoint (authed with ``CF_AIG_TOKEN``), the way the case-study backend does
     it (see backend/llm/client.py).
The DeepSeek tiers (direct + CF gateway) use the in-repo
``kg.deepseek_llm.DeepSeekLLM`` (an ``OpenAILike`` subclass; see that module for why
we don't use ``llama-index-llms-deepseek``); the local tier uses plain ``OpenAILike``.
Embeddings are always in-process FastEmbed (local, no API cost).

roadmap-kg/kg/llm.py:120-132

Tier 1: direct DeepSeek when DEEPSEEK_API_KEY is set — and it never falls through to local.

python
    # must NOT be persisted in roadmap-kg/.env (the auto-commit bot can stage it).
    deepseek_key = os.environ.get("DEEPSEEK_API_KEY", "")
    if deepseek_key:
        model = os.environ.get("DEEPSEEK_MODEL_DIRECT", "deepseek-v4-flash")
        if model == "deepseek-chat":
            # Legacy id (deprecated 2026/07/24) → its current non-thinking successor.
            model = "deepseek-v4-flash"
        return _build_deepseek(
            model=model,
            api_base=os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
            api_key=deepseek_key,
            timeout=float(os.environ.get("DEEPSEEK_TIMEOUT", "120")),
            max_retries=_opt_int("DEEPSEEK_MAX_RETRIES") or 3,

roadmap-kg/kg/llm.py:159-167

Tier 3: the DeepSeek-via-Cloudflare-AI-Gateway /compat fallback.

python
            local_kwargs["max_tokens"] = deepseek_max_tokens
        # Optional local sampler overrides. llama.cpp's `llama-server` honors top_k /
        # min_p / repeat_penalty (and top_p) as sampler knobs, but they aren't OpenAI
        # chat fields — so we tunnel them through the request body via `extra_body`,
        # which OpenAILike spreads verbatim into the /v1 payload (additional_kwargs).
        # All four unset → additional_kwargs stays absent → the request is byte-identical.
        _extra_body: dict = {}
        _top_p = os.environ.get("LLM_TOP_P", "").strip()
        if _top_p:
STUDY AIDSevidence-backed memory techniques
Recall check

What order does make_llm use to resolve the three egress tiers?

Show answer

strict first‑available‑wins order

06. Trust but verify

Generating a code excerpt from a source file sounds like a safe operation: the model sees the code and returns a snippet of it. In practice, a language model can produce an excerpt that looks perfectly plausible but quietly invents an API call, a function name, or an import that does not exist in the real source. The ground pipeline therefore pairs generation with a verification step that checks every identifier in the output against the actual source, accepting the excerpt only when it passes.

The verification starts with _source_identifiers inside roadmap-kg/kg/ground.py. This utility reads every source file in the chapter’s list, tokenizes the raw text into a set of whole identifiers. It splits on any non-alphanumeric character (whitespace, punctuation, operators) so that a name like index_documents becomes a single token. It also strips Python keywords (def, class, return, etc.) and built‑in names (print, len) to avoid false negatives — a hallucinated print call should not be flagged as an invention because the source already uses it. The crucial design choice is whole‑token membership: the source may contain the identifier index_documents; if the model writes documents or index as a standalone token, those are not present in the source’s identifier set and will be counted as violations. This prevents the model from sneaking in a fake name by using a substring of a real one.

Once the excerpt is generated, _code_grounding_violations enforces the bar. It extracts every identifier from the generated code (again tokenizing the same way) and checks each one against the reference set. Any identifier not found is a violation. If violations exist, the pipeline does not ship the excerpt. Instead, it retries the generation — typically with a slightly modified prompt that emphasizes verbatim copying — up to a configurable limit. The constant that governs this limit is not shown in the excerpt but the code lane uses the same retry logic as the audio lane; a chapter that fails repeatedly is simply dropped. The chapter’s slot in the output file becomes empty or omitted, and the page renders without a code block for that topic.

This trust‑but‑verify approach buys a hard guarantee: every code block that reaches the page is composed solely of identifiers that appear in the real source files. The trade‑off is a small chance of rejecting a legitimate excerpt when the model uses an uncommon but valid identifier that happens to be a substring of a larger name — for example, if the source contains database_url and the model writes url as a variable name, that url token is absent from the source set and the excerpt is flagged. In practice, the strict whole‑token rule catches far more hallucinations than it falsely rejects, and the retry loop handles the borderline cases by giving the model another chance with clearer instructions. The result is a pipeline that ships code with confidence, because every shipped token has been verified against the ground truth.

ELI5 — explain it simply
1
Gist

After the AI writes example code, the system checks every name in it against the real files and throws the snippet away if too much of it is made up.

2Morego a level deeper

It first lists the real names in the source, then requires most of the names in the snippet to be real whole words (not just substrings). It retries a few times with feedback, and drops the snippet if it still doesn't match.

3Deepthe full mechanics

_source_identifiers builds the whole-token set; _code_grounding_violations demands one fenced block and ≥2/3 grounded identifiers (whole-token, not substring — execute vs execute_tool); _ground_code_chapter asks up to three times total (initial + 2 retries) with the violations fed back, and the caller ships-but-flags an excerpt that never passes, logging a residual-violations warning.

Code references

Where this chapter's machinery lives in the repo:

roadmap-kg/kg/ground.py:2251-2256

_source_identifiers: the real source's whole-identifier set, matched by exact token not substring.

python
             "the blast radius of an agent with a mouse and keyboard — sandboxing, "
             "confirmations before consequential clicks, and why computer-use "
             "agents ship behind more safeguards than chat models."),
        ],
    ),
    "mcp": GroundConfig(

roadmap-kg/kg/ground.py:2259-2287

_code_grounding_violations: one fenced block + at least two-thirds of identifiers must trace to source.

python
        sources=[
            APP_ROOT / "roadmap-kg" / "data" / "ai-eng-techniques-docs" / "function-calling.harvested.md",
            APP_ROOT / "roadmap-kg" / "data" / "langgraph-docs" / "langchain-tools.md",
            APP_ROOT / "roadmap-kg" / "data" / "agentic-frontier-docs" / "mcp-interop.md",
            APP_ROOT / "roadmap-kg" / "data" / "agentic-frontier-docs" / "mcp-2026-roadmap.md",
            APP_ROOT / "roadmap-kg" / "data" / "agentic-frontier-docs" / "mcp-architecture.md",
            APP_ROOT / "roadmap-kg" / "data" / "agentic-frontier-docs" / "mcp-2026-07-28-release-candidate.md",
            APP_ROOT / "roadmap-kg" / "data" / "agentic-frontier-docs" / "mcp-registry-preview.md",
            APP_ROOT / "roadmap-kg" / "data" / "agentic-frontier-docs" / "ox-mcp-stdio-supply-chain.md",
            APP_ROOT / "roadmap-kg" / "data" / "agentic-frontier-docs" / "censys-mcp-internet-exposure.md",
        ],
        chapters=[
            ("Why An Open Protocol",
             "the integration explosion an open standard solves — every assistant "
             "hand-wiring every data source — and the Model Context Protocol as one "
             "universal way to connect assistants to tools and data."),
            ("Servers Clients And Hosts",
             "the protocol's shape — servers that expose a capability once, clients "
             "embedded in a host application that connect to many servers, and how "
             "a session between them is established."),
            ("Tools Resources And Prompts",
             "what a server can offer — callable tools, readable resources, and "
             "reusable prompts — and how a model discovers and invokes them "
             "through the protocol rather than bespoke glue code."),
            ("Elicitation And Structure",
             "the newer protocol surface — elicitation that lets a server ask the "
             "user for input mid-task, structured tool output, and streamable "
             "transport for long-running calls; and the next-generation revision "
             "locked as a release candidate in May 2026 with the final spec "

roadmap-kg/kg/ground.py:2290-2312

_ground_code_chapter: retry ×3 with grounding feedback appended before giving up.

python
             "framework with tasks and app extensions, and a formal twelve-month "
             "deprecation policy retiring roots, sampling, and logging."),
            ("Registry And Discovery",
             "finding servers — a registry for discovery, and the governance "
             "questions an open ecosystem raises: who publishes, who vets, and "
             "what standardized telemetry should report; where that stands in "
             "mid-2026 — the official registry still in preview with thousands "
             "of servers listed, namespaces authenticated through domain and "
             "code-host ownership, and security scanning explicitly delegated "
             "to package registries and downstream aggregators."),
            ("Securing The Protocol",
             "trust boundaries when tools come from third parties — untrusted tool "
             "output entering the model's context, authorization and approval "
             "flows, and why protocol-level security outlives prompt-level fixes; "
             "what 2026 exposed and fixed — internet scans finding thousands of "
             "unauthenticated servers exposing data and system control, a "
             "disputed command-injection default propagated through the official "
             "SDKs, and in response authorization aligned with enterprise "
             "identity, mandatory issuer validation, and a zero-touch "
             "enterprise-managed authorization extension gone stable."),
        ],
    ),
    "agent-guardrails": GroundConfig(
STUDY AIDSevidence-backed memory techniques
Explain & elaborate · explain why

Why does the verification step use a whole-token membership rule instead of checking for substring matches when comparing identifiers from the generated code against the source code?

07. The audio pipeline: from prose to a same-origin stream

Every guide on this site is listenable, yet none of the audio is recorded by hand. The narration is synthesized from the same text the written page shows, driven by a three-stage pipeline in roadmap-kg/kg/audio/pipeline.py. That module is a convenience wrapper that runs the three stages in order: tts, finalize, and stitch. The first stage, text-to-speech, runs per chapter (or per paragraph, depending on the --per-chapter or --per-paragraph flag). It produces chapter‑level MP3s that are uploaded to R2 via --upload. The second stage, finalize, writes the page‑served data/<slug>.json — an AudioMeta object containing per‑chapter start seconds and the full‑length MP3’s metadata. The third stage, stitch, concatenates those chapter MP3s into one full‑length file on R2, again using the --upload flag. The pipeline is designed so that each stage is independently runnable (python -m kg.audio.pipeline --slug <slug> --per-chapter --upload), and re‑synthesising a single chapter is possible via --chapter N while still running finalize and stitch for the whole. This automation buys atomicity: the audio and the written guide always agree because both derive from the same source text, and a broken manifest is caught by the prebuild test suite (test:audio:all) before the bundle ever ships.

The resulting MP3s live on R2 under the public domain tts.vadim.blog rather than in the app bundle. Storing large binary assets outside the Next.js bundle keeps the worker’s memory footprint small and allows the audio to be served from a separate origin without bloating the deploy. However, serving media cross‑origin is fragile: CORS headers are needed, and all bytes must pass through the browser’s service‑worker fetch handler, which historically stalled playback by downloading the entire file. The site sidesteps both problems by streaming the MP3s through its own same‑origin proxy, defined in app/api/audio/[...path]/route.ts. This route runs on Vercel serverless (Node.js runtime, force-dynamic) and proxies requests from paths like /api/audio/knowledge/<slug>/NN.mp3 to the upstream R2 URL. The key trick: it forwards the browser’s Range header upstream and faithfully relays the 206 Partial Content (or 200, 416) response, along with Accept-Ranges, Content-Range, and Content-Length. This lets the <audio> element seek normally by mapping time to byte offset. The proxy is locked down by a regex (KEY_RE) that only permits knowledge/…mp3 paths, preventing it from becoming an open SSRF vector. It also retries transient upstream failures once with a 250ms backoff.

The player on the page obtains the same‑origin URL via toSameOriginAudioUrl, defined in app/how-it-works/_shared.ts. That helper transforms an R2 public URL (e.g. https://tts.vadim.blog/knowledge/how-it-works-audio/full.mp3) into /api/audio/knowledge/how-it-works-audio/full.mp3. Because the MP3 is now fetched from the same origin as the page, no CORS is required, and the service worker, which registers no fetch handler and self‑unregisters, cannot intercept the stream. Playback progresses byte‑by‑byte via HTTP Range requests rather than buffering the entire file. Logging in the route emits one line per request (key, range, upstream status, length, elapsed) visible in the Vercel runtime logs, making it straightforward to debug seeking or latency issues. The result is a seamless listening experience that stays same‑origin by design, with no manual recording and no fragile cross‑origin setup.

ELI5 — explain it simply
1
Gist

You can press play on any guide. A synthetic voice reads the same words that are on the page, and the app streams the sound so you can jump around without waiting for the whole file to load.

2Morego a level deeper

The spoken files are made ahead of time by a text-to-speech step, stored on Cloudflare's file storage (R2), and played back through the app's own web address instead of the storage's. That same-address trick is what makes the scrubber work: the player asks for just the slice of audio it needs, and the app fetches exactly that slice.

3Deepthe full mechanics

A three-stage Python pipeline (TTS to finalize to stitch) synthesizes per-chapter MP3s, uploads them to R2 under knowledge/<slug>/NN.mp3, and writes the page's audio metadata JSON. Playback goes through a same-origin Cloudflare Worker route that forwards HTTP Range headers and faithfully relays 206/Content-Range, so the audio element can map time to byte offset. The route is allowlisted to knowledge/*.mp3 to avoid becoming an SSRF proxy, streams the body without buffering, and sets Vary: Range plus immutable day-long caching. toSameOriginAudioUrl does the rewrite at render time, and being under /api/ keeps the service worker from intercepting it.

Code references

Where this chapter's machinery lives in the repo:

roadmap-kg/kg/audio/pipeline.py:17-52

One command runs the three audio stages in order — TTS synthesis, finalize (writes the page-served JSON), then stitch — with stitch requiring the chapter MP3s already uploaded to R2.

python
def main(argv: list[str] | None = None) -> int:
    ap = argparse.ArgumentParser(description="Run TTS → finalize → stitch (kg.audio)")
    ap.add_argument("--slug", required=True)
    ap.add_argument("--per-chapter", action="store_true")
    ap.add_argument("--per-paragraph", action="store_true")
    ap.add_argument("--chapter", type=int, default=None,
                    help="re-synthesize only this 0-based chapter in the TTS stage (per-chapter only); finalize + stitch still run in full")
    ap.add_argument("--upload", action="store_true", help="upload to R2 (required for stitch)")
    ap.add_argument("--no-stitch", action="store_true", help="stop after finalize")
    args = ap.parse_args(argv)

    if not (args.per_chapter or args.per_paragraph):
        ap.error("choose a mode: --per-chapter or --per-paragraph")

    tts_argv = ["--slug", args.slug]
    tts_argv.append("--per-paragraph" if args.per_paragraph else "--per-chapter")
    if args.chapter is not None:
        tts_argv.extend(["--chapter", str(args.chapter)])
    if args.upload:
        tts_argv.append("--upload")

    print("── stage 1/3: TTS ──")
    rc = tts.main(tts_argv)
    if rc != 0:
        return rc

    print("\n── stage 2/3: finalize ──")
    rc = finalize.main(["--slug", args.slug])
    if rc != 0:
        return rc

    if args.no_stitch:
        return 0

    if not args.upload:
        print("\n(skipping stitch: needs MP3s on R2 — re-run with --upload)")

app/api/audio/[...path]/route.ts:82-100

The proxy's response headers: normalize the content-type to audio/mpeg, always advertise accept-ranges, Vary on Range so partial and full bodies never collide in a cache, and cache immutable chapter MP3s for a day.

typescript

function responseHeaders(upstream: Response): Headers {
  const h = new Headers();
  for (const name of COPY_RES_HEADERS) {
    const v = upstream.headers.get(name);
    if (v) h.set(name, v);
  }
  h.set("content-type", normalizeContentType(upstream));
  // Always advertise range support so the player offers a seek bar even if the
  // upstream omitted it on a 200 response.
  h.set("accept-ranges", "bytes");
  // Caches/proxies must key on Range so a cached full body is never replayed
  // for a partial request (and vice-versa).
  h.set("vary", "Range");
  // Chapter MP3s are immutable per index: long browser/edge cache, but allow
  // revalidation so a re-rendered chapter can replace a stale copy. `immutable`
  // stops the browser from issuing pointless revalidations while seeking.
  h.set("cache-control", "public, max-age=86400, stale-while-revalidate=604800, immutable");
  return h;

app/how-it-works/_shared.ts:89-100

Render-time rewrite of an R2 knowledge MP3 URL to the same-origin /api/audio proxy; non-matching or unparseable URLs pass through untouched.

typescript
  label: string; // "Gist" | "More" | "Deep"
  body: string;
}
export interface Eli5Chapter {
  index: number;
  title: string;
  levels: Eli5Rung[];
}

interface ContentShape {
  eli5?: { chapters?: Record<string, { title?: string; levels?: { label?: string; body?: string }[] }> };
}
STUDY AIDSevidence-backed memory techniques
Recall check

What technique does the site use to avoid CORS and service worker fetch handler issues for audio playback?

Show answer

streaming the MP3s through its own same‑origin proxy

08. The data plane: a D1 content cache and R2 media

The runtime data plane exists to make new or edited content appear on the live site without a redeploy. Rather than shipping a static build that bakes every lesson and audio guide into the container, the app reads dynamic records from a small Cloudflare D1 database. The database is reached from Vercel’s Node.js runtime over Cloudflare’s REST API: every call to d1Query constructs an INSERT/SELECT statement and POSTs it to api.cloudflare.com using the credentials that d1Configured validates — this means the app only tries D1 when CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_AUDIO_D1_ID, and CLOUDFLARE_D1 are set. The store itself is a single table, content_cache, keyed by (kind, slug). Each row’s payload column holds the exact JSON that the rest of the app already consumes — a ContentIndex, LessonFull, or AudioMeta blob. Reading is deliberately boring: getDoc<T> runs a SELECT payload … WHERE kind = ? AND slug = ? LIMIT 1, calls JSON.parse on the result, and hands the typed object back to the caller. Because the payload matches the existing shapers (lessonWithContentFromFull, etc.), no transformation logic needs to duplicate the static pipeline.

Shared content reads — the index, every lesson, every audio meta — opt into the Next Data Cache by passing { revalidate: 3600, tags: [kind, kind+':'+slug] } to d1Query. This one-hour TTL is a deliberate trade‑off: uncached reads inside the top‑level render path force that entire route to be dynamic (and thus Vercel‑paid per request), so the cache prevents every visitor from hitting D1 directly. The tag pair (kind plus the more specific kind:slug) lets the offline publish pipeline later bust exactly the right rows. The /api/revalidate route (in app/api/revalidate/route.ts) is an authenticated endpoint that the kg.sync_d1 step calls after writing new rows. It loops over the posted tags and calls revalidateTag(t, "max"), which evicts the cached entries from the shared Data Cache. Within seconds the next request fetches the fresh row from D1.

Per‑user or mutating callers (though none appear in the provided consumer paths) would pass no-store to d1Query’s options object, because caching a personalized response would be incorrect. The audio media itself lives on R2 behind tts.vadim.blog. It is proxied same‑origin through the app/api/audio/ route, which forwards the browser’s Range header and relays the upstream 206/200 response. This removes the cross‑origin CORS fragility and lets <audio> seek directly, rather than downloading the whole file.

The entire design fails open. getDoc catches every error — D1 unavailable, network failure, missing table — and returns null. The resolver chain in lib/data.ts (which aggregates content-d1, content-json, and the static articles module) checks for null and falls through to the build‑time JSON export, and then to the static markdown parser. A lesson page never hard‑depends on D1; the database is an optional fast path for live updates. The same pattern applies to the content index, category metadata, and audio guides: if D1 is unconfigured or empty, the app silently degrades to what was bundled at build time. This ensures that a missing database credential, a region outage, or a table rebuild never breaks the site — it only pauses dynamic content delivery until D1 recovers.

ELI5 — explain it simply
1
Gist

Lessons can also come from a small database, so fixing a typo shows up on the next refresh with no redeploy. If that database is ever down you still get the version that shipped, not a blank page.

2Morego a level deeper

The app talks to the database over ordinary web requests with credentials, not a direct wire, and only bothers when those credentials are actually set. There's one table with one row per kind-and-slug, and each row holds exactly the JSON the app already knows how to render. Pages everyone shares remember the answer for an hour; anything personal is never remembered. If a read fails for any reason it comes back empty and the page falls back to the copy baked in at build time.

3Deepthe full mechanics

d1Query POSTs SQL to api.cloudflare.com only when d1Configured sees CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_AUDIO_D1_ID and CLOUDFLARE_D1; getDoc<T> SELECTs one payload from content_cache by (kind, slug) and parses it. Shared reads pass { revalidate: 3600, tags: [kind, kind+':'+slug] } into the Next Data Cache — an uncached read in the top-level render path would force the whole route dynamic and paid per request — while per-user callers pass no-store; /api/revalidate calls revalidateTag(t, "max") after kg.sync_d1 writes. Every failure returns null, so lib/data.ts falls through to the build-time JSON and then the markdown parser: D1 is an optional fast path, never a dependency. Audio sits on R2 behind tts.vadim.blog, proxied same-origin through app/api/audio/ so Range seeking works without CORS.

Code references

Where this chapter's machinery lives in the repo:

lib/d1.ts:1-7

The whole configuration surface: an account id, a database id and an API token. d1Configured is the single predicate every reader checks first, so an unconfigured environment degrades instead of throwing.

typescript
const CF_ACCOUNT_ID = process.env.CLOUDFLARE_ACCOUNT_ID;
const D1_ID = process.env.CLOUDFLARE_AUDIO_D1_ID;
const D1_TOKEN = process.env.CLOUDFLARE_D1;

export function d1Configured(): boolean {
  return Boolean(CF_ACCOUNT_ID && D1_ID && D1_TOKEN);
}

lib/d1.ts:36-50

The one REST call: a bearer-token POST to the D1 query endpoint. The tail is the important half — a caller that passes revalidate opts the read into the Next Data Cache, and everyone else stays no-store, because a no-store read in the render path forces the whole route to dynamic.

typescript
  const url = `https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/d1/database/${D1_ID}/query`;
  const res = await fetch(url, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${D1_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ sql, params }),
    // Per-user / mutating callers (email) omit `opts` and stay uncached;
    // shared content readers opt into ISR caching so they don't poison static
    // generation.
    ...(opts?.revalidate !== undefined
      ? { next: { revalidate: opts.revalidate, tags: opts.tags } }
      : { cache: "no-store" as const }),
  });

lib/content-d1.ts:26-44

getDoc selects one content_cache row by (kind, slug), parses its JSON payload, and caches the read for an hour under bust-able tags; it returns null on any failure so callers fall back to bundled JSON.

typescript
/** Fetch one `content_cache` row and parse its JSON payload. */
async function getDoc<T>(kind: string, slug: string): Promise<T | null> {
  if (!d1Configured()) return null;
  try {
    const rows = await d1Query<PayloadRow>(
      "SELECT payload FROM content_cache WHERE kind = ? AND slug = ? LIMIT 1",
      [kind, slug],
      // ISR: cache content reads (1h TTL) so they do not force every consuming
      // page to dynamic rendering, and tag them so the `kg.sync_d1` publish
      // step can bust them on demand via /api/revalidate. `audio:<slug>` matches
      // the tag that pipeline already POSTs.
      { revalidate: 3600, tags: [kind, `${kind}:${slug}`] },
    );
    const raw = rows[0]?.payload;
    return raw ? (JSON.parse(raw) as T) : null;
  } catch {
    return null; // D1 unavailable / table missing — let the caller fall back.
  }
}

lib/data.ts:71-88

The three-layer ladder in one function: D1 first (edits appear on refresh, no redeploy), the build-bundled JSON export second, the markdown parser last. Each layer catches its own failure, so a missing layer is a fall-through rather than an error.

typescript
export async function getAllLessons(): Promise<Lesson[]> {
  try {
    const { getIndexFromD1 } = await import("./content-d1");
    const { lessonsFromIndex } = await import("./content-json");
    const idx = await getIndexFromD1();
    if (idx) return lessonsFromIndex(idx);
  } catch {
    // D1 unavailable — fall through
  }
  try {
    const { getAllLessonsFromJson } = await import("./content-json");
    return getAllLessonsFromJson();
  } catch {
    // JSON export unavailable — fall through
  }
  const { getAllLessons: fs } = await import("./articles");
  return fs();
}
STUDY AIDSevidence-backed memory techniques
Cloze

The entire design  ____ .

Show answer

fails open

09. The workers fleet and its cron triggers

Every scheduled job on this site runs as a Vercel cron, not as a Cloudflare Worker scheduled event. The infrastructure is unified: one Next.js App Router application ships to Vercel, and vercel.json declares a single scheduled task in its crons array: { "path": "/api/cron/backup", "schedule": "0 4 * * *" }. This means Vercel’s built-in cron system issues an ordinary HTTP GET against the app/api/cron/backup/route.ts endpoint once daily at 04:00 UTC. Because the route is a standard Next.js API handler, it inherits the application’s full runtime (database connections, environment variables, serverless function settings) without needing a separate worker runtime.

The backup route first authenticates the caller using verifyCronSecret, a function imported from @ai-apps/db-backup. Vercel’s cron injects an Authorization: Bearer header whose value must match the CRON_SECRET environment variable; if the header is missing or incorrect, the route returns a 401. This ensures the public URL is not an open trigger. Once authenticated, the handler runs runD1Backup and runQdrantBackup, writing snapshots to R2. The function’s maxDuration is set to 300 seconds in vercel.json under the functions key, giving the backup ample time to complete on Vercel’s serverless infrastructure.

A second scheduled route, app/api/cron/kg-tick/route.ts, shares the same authentication pattern and is designed to run the autonomous knowledge-graph loop. However, it is deliberately left unscheduled — there is no corresponding entry in vercel.json’s crons. Instead, it contacts the KG service on demand only when KG_TICK_ENABLED is set to "1". The default state is off, so wiring a live LLAMAINDEX_SERVICE_URL cannot silently start recurring token consumption. The route itself supports both GET (the cron verb) and POST for manual triggering, but it is never invoked automatically.

Beyond the Next.js application, a handful of Cloudflare Workers exist separately: edge-tasks, aer-ext-api, ai-gateway, and vadim-blog. These are independent services — they are not part of the Vercel app and have no cron triggers. Earlier versions of workers/edge-tasks/wrangler.toml included a [triggers] section with a */10 * * * * cron to keep a Render-hosted RAG service warm, but that target was suspended and the cron was removed. The workers now serve only request-driven tasks (e.g., proxying audio streams or calling DeepSeek directly), while all recurring maintenance — backups, knowledge-graph ticks — lives inside Vercel’s cron ecosystem.

ELI5 — explain it simply
1
Gist

Exactly one thing on this site runs on a timer: a nightly backup. Everything else only happens when something asks for it.

2Morego a level deeper

A scheduled web request hits a backup endpoint once a day at 4am UTC. The endpoint checks a secret in the request header before doing anything, so the public URL isn't an open trigger, then writes database and vector snapshots to object storage. A second job for the knowledge-graph loop exists but is deliberately left off every schedule, so nobody can accidentally start a recurring token bill. A few small Cloudflare Workers still run alongside, but they only answer requests — none of them are on a clock.

3Deepthe full mechanics

vercel.json's crons array holds a single entry: { "path": "/api/cron/backup", "schedule": "0 4 * * *" }, with maxDuration 300 under functions. app/api/cron/backup/route.ts calls verifyCronSecret from @ai-apps/db-backup (401 unless the injected Authorization: Bearer matches CRON_SECRET), then runs runD1Backup and runQdrantBackup into R2. app/api/cron/kg-tick/route.ts shares that auth and accepts both GET and POST, but has no crons entry and only calls out when KG_TICK_ENABLED is "1" — so wiring a live LLAMAINDEX_SERVICE_URL can't silently start recurring spend. The separate Cloudflare Workers (edge-tasks, aer-ext-api, ai-gateway, vadim-blog) are request-driven services with no cron; edge-tasks' old */10 keepwarm trigger was removed once its Render target was suspended.

Code references

Where this chapter's machinery lives in the repo:

vercel.json:9-16

The entire schedule: one daily 04:00 cron hitting /api/cron/backup, and a matching per-route maxDuration raising that function's ceiling to 300s because a full database dump does not finish inside the default budget.

json
  "functions": {
    "app/api/cron/backup/route.ts": {
      "maxDuration": 300
    }
  },
  "crons": [
    { "path": "/api/cron/backup", "schedule": "0 4 * * *" }
  ]

app/api/cron/backup/route.ts:8-28

The handler is a plain authenticated GET: verifyCronSecret rejects anything without the CRON_SECRET bearer the scheduler sends, then the D1 database is dumped to the R2 backups bucket under a 240s budget and a 30-day retention.

typescript
export async function GET(request: Request) {
  const authError = verifyCronSecret(request);
  if (authError) return authError;

  const r2 = {
    accountId: process.env.R2_ACCOUNT_ID!,
    accessKeyId: process.env.R2_ACCESS_KEY_ID!,
    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
    bucketName: "db-backups",
  };

  // The app's data lives in Cloudflare D1 now; back it up as a .sql dump → R2.
  const d1 = await runD1Backup({
    appName: "knowledge",
    accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
    databaseId: process.env.CLOUDFLARE_AUDIO_D1_ID!,
    apiToken: process.env.CLOUDFLARE_D1!,
    r2,
    maxDurationMs: 240_000,
    retentionDays: 30,
  });

app/api/cron/kg-tick/route.ts:34-44

The KG loop's kill-switch, checked immediately after the same bearer check: unless KG_TICK_ENABLED is exactly "1" the route returns skipped. Every tick is real model spend with no human in the loop, so it stays off by default.

typescript
export async function GET(request: Request) {
  const authError = verifyCronSecret(request);
  if (authError) return authError;

  // Opt-in kill-switch: this cron fires hourly with no human in the loop, and
  // every tick is DeepSeek spend on the KG service. Off unless explicitly
  // enabled, so wiring a live LLAMAINDEX_SERVICE_URL can't silently start
  // recurring token consumption.
  if (process.env.KG_TICK_ENABLED !== "1") {
    return NextResponse.json({ ok: true, skipped: "KG_TICK_ENABLED is off" });
  }

workers/edge-tasks/wrangler.toml:7-13

The independent Python edge worker, deployed out-of-band with its own config — and the record of a cron that was deliberately removed: the keepwarm ping existed for a hosted RAG service that no longer runs, so the trigger was deleted rather than left firing at nothing.

toml

# on Render's free tier, which is now SUSPENDED: that box cannot embed in-process
# (fastembed needs ~616Mi against a 512Mi cap — measured), and delegating the
# embedding to Workers AI is exactly the Cloudflare-AI dependency we removed. The
# RAG service therefore runs on localhost only (`make explain-local`: fastembed +
# DeepSeek direct + hybrid + reranker). See specs/case-study/deep-dives/rag-serving.md.
# If a hosted RAG service returns, restore [triggers] + TARGET_URL from git history.
STUDY AIDSevidence-backed memory techniques
Quiz

What is the default state of the kg-tick cron trigger?

Options: The default state is off · It runs every 10 minutes · It is triggered by a Cloudflare Worker · It runs automatically once a day

Show answer

The default state is off

10. Evals and gates: keeping generated content honest

Machine-generated audio guides cannot be blindly trusted: the synthesis pipeline emits files that may be structurally malformed, contain invalid metadata, or include sidecar artifacts like *.tts.json, *.eli5.json, and glossary catalogs that are never meant to be published as audio guides. Without a mechanical gate, these would all surface as generic failures and bury the real content issues the editorial loop must fix. That is why roadmap-kg/kg/audio_gate.py exists — a standalone, deterministic validator that runs over each generated AudioMeta JSON and returns a clean pass/fail verdict (exit 0 or 1) with no hidden state.

The gate is intentionally self-contained: it pulls in no LlamaIndex, embedding, or environment machinery, staying cheap enough to run on every guide without slowing the generation pipeline. Its entry point is gate_audio(meta: dict) -> dict, which mirrors the Rust parity oracle gate.rs. The module is invoked via python -m kg.audio_gate with either a single --slug (for per-file gating) or --all to scan a whole directory. In single‑slug mode the exit code tells the pipeline immediately whether to proceed to the next stage.

The gate splits its checks into two categories to avoid false-positive noise blocking valid content. HARD failures directly set ok = False and block publication. These include structural requirements like at least MIN_CHAPTERS (3), a minimum total word count (MIN_TOTAL_WORDS) and per‑chapter minimum (MIN_WORDS_PER_CHAPTER), titles no longer than MAX_TITLE_WORDS (5) and MAX_TITLE_CHARS, a sentence‑length coefficient‑of‑variation floor (SENTENCE_CV_MIN), and a Flesch Reading Ease floor enforced by _scan_audio_readability. Every HARD rule is mirrored in the Rust oracle and cannot be changed unilaterally. WARN-only heuristics — acronym‑first‑use, numbers 10–19, sentence‑length variety, narration‑polarity, spoken‑list, and ai‑tells — are emitted as warnings that never flip ok, precisely because they are too false‑positive‑prone to be trusted as a gate decision. This design lets the content loop triage failures_by_rule at a glance (e.g. title_too_long×12) without opening each lesson entry.

By keeping the gate cheap, deterministic, and strict on HARD rules while reserving softer checks for the human reviewer, the pipeline avoids blocking on noisy heuristics and ensures that only genuinely broken audio guides ever reach a human’s worklist. The --skip-non-guide flag extends this pragmatism by dropping known sidecar files at validation time, so the default --all scan remains byte‑identical to the Rust oracle but the optional flag keeps the worklist clean.

ELI5 — explain it simply
1
Gist

A program writes a lot of this, so a stricter program checks it before it goes live — is it long enough, simple enough to listen to, not saying anything toxic? If it fails, it doesn't publish.

2Morego a level deeper

There's a fast mechanical check (word counts, a reading-ease score, title length) that can block publishing, and a slower AI judge that scores things like faithfulness and relevance. The reading-ease bar comes from real readability research, and it's tougher for audio because you can't re-read a sentence you missed.

3Deepthe full mechanics

kg.audio_gate is a self-contained gate over AudioMeta JSON: HARD rules (at least 3 chapters, minimum words, titles of 5 words or fewer, Flesch at least 50, pacing bounds) flip ok/fail, while heuristic rules are WARN-only. The HARD constants are mirrored with a retired Rust parity oracle (checked by parity_check.py) to catch drift. make eval-deepeval layers DeepEval metrics (faithfulness/relevancy/bias/toxicity) via LlamaIndex adapters with a DeepSeek judge over the CF AI Gateway, installed into an isolated .venv-eval so its pydantic/otel/pytest deps never touch the RAG service venv.

Code references

Where this chapter's machinery lives in the repo:

roadmap-kg/kg/audio_gate.py:121-138

The HARD thresholds, verbatim from the Rust parity oracle: minimum chapters and words, titles of five words or fewer, a Flesch floor of 50, and sentence-pacing bounds — these are the checks that flip the pass/fail flag.

python

MIN_CHAPTERS = 3
MIN_WORDS_PER_CHAPTER = 40
MIN_TOTAL_WORDS = 400
MAX_TITLE_WORDS = 5
MAX_TITLE_CHARS = 72
SENTENCE_CV_MIN = 0.30
SENTENCE_CV_MIN_SENTENCES = 5
HOSTILE_RATIO_MAX = 0.05
HOSTILE_TOKENS_PER_SENTENCE_MIN = 3
PACING_MEAN_MIN = 11.0
PACING_MEAN_MAX = 22.0
PACING_CV_MIN = 0.35
PACING_MAX_SENTENCE_WORDS = 30
MAX_SPOKEN_LIST_ITEMS = 4
MAX_CLAUSE_COMMAS = 4
NESTING_MIN_SENTENCE_WORDS = 20
FLESCH_MIN = 50.0

roadmap-kg/kg/audio_gate.py:1230-1253

gate_audio makes the rule explicit — ok == not failures. Empty slug/title, too-few chapters, zero duration, and a voice/audio_url mismatch each append a failure and flip the gate; warnings are collected separately and never affect ok.

python
           "(working memory in sentence comprehension).")


# ── Per-chapter hard rules (Rust lint_chapter equivalent) ─────────────────────
def _chapter_hard_failures(idx: int, title: str, script: str) -> list[dict]:
    """Per-chapter HARD failures only (no whole-meta/structural rules). The Python
    equivalent of gate.rs::lint_chapter — used by kg.ground for the in-loop retry so
    the in-loop gate and this standalone gate can never drift."""
    out: list[dict] = []
    _scan_prose(idx, script, out)
    _scan_hostile_tokens(idx, script, out)
    # pacing-rhythm: only the HARD (>30-word sentence) branch belongs here.
    pacing_failures: list[dict] = []
    _scan_pacing_rhythm(idx, script, pacing_failures, [])
    out.extend(pacing_failures)
    _scan_audio_readability(idx, script, out)
    _scan_small_digits_hard(idx, script, out)
    _scan_sentence_integrity(idx, script, out)
    _scan_sentence_nesting(idx, script, out)
    # acronym-first-use HARD (non-allowlisted, unexpanded). In-loop lints one
    # chapter in isolation → no cross-chapter `seen` list; pass a fresh one so
    # every acronym is treated as first-use within that chapter.
    _scan_acronym_first_use(idx, script, [], out, [])
    return out
STUDY AIDSevidence-backed memory techniques
Explain & elaborate · explain why

Why does the audio gate separate its checks into HARD failures and WARN-only heuristics instead of treating all checks equally?

11. Memory science: the evidence base and the schedulers

Two surfaces are built within this Next.js App Router app for actually remembering what you read: the /memorize route (spaced-repetition flashcards) and the /loci route (a memory-palace trainer that walks cards along a route). Both rest on the same FSRS‑6 model from lib/sm2.ts — the shared core that computes stability, difficulty, and the next-review interval — but each surface implements its own scheduler that imports that core. The FSRS‑6 core is reused wholesale; no scheduling math is duplicated.

The loci scheduler lives in lib/loci-scheduler.ts, a file of pure functions that adds exactly two thin wrappers on top of the FSRS‑6 core. The file does not import React, use localStorage, or perform I/O — it is a stateless set of exports that any component can call. The first wrapper is an onboarding ladder: new cards graduate through three fixed short steps ([10min, 1d, 3d] by default) before free-running FSRS takes over. Every grade still calls fsrsUpdate so the latent DSR state (stability and difficulty) stays honest the whole time; the ladder only overrides the resulting interval while phase !== "fsrs". The second wrapper is an in-session Leitner queue: buildSession orders due cards along the route walk, and an again grade recycles the card to the back of the queue for another pass in the same sitting. The first grade a card receives in a session is the one persisted to FSRS — recycle passes are practice-only, never re-persisted. This rule lives in gradeSessionCard. The file exports the LociPhase union type (the four rungs: "onboard1" through "fsrs"), the PHASES read-only array, and the default ladder in minutes. The same lib/sm2.ts core is shared, but the memorize surface likely has its own scheduler with different wrappers — the shared part is only the FSRS‑6 core, not the whole scheduler.

The learning-science pages under app/how-it-works/ document the evidence base that both features apply. The merged page at app/how-it-works/learning-science/page.tsx is a fully static server component (dynamic = "force-static"). It presents a catalogue of 26 evidence-backed learning techniques (sorted by paperCount descending), each linking to DOI-verified peer-reviewed papers, and 13 memory principles hand-authored in ./content.tsx. The principles are grouped by "encoding", "practice", and "structural" groups, each with mechanism prose, evidence items with citation keys, boundary conditions, and design implications for a mnemonic-generation system. Some principles also have an mlAnalog mapping to machine-learning training mechanisms (e.g., “Prioritized experience replay”). The page includes badges showing techniqueCount, paperCount, PRINCIPLE_COUNT, and CITATION_COUNT, plus links to audio guides, a BibTeX download of all citations at /learning-science.bib, and JSON‑LD structured data describing the dataset and the principles as a collection. The technique cards use bare #slug anchors; principle cards use #principle-slug anchors. The only client island is a StartMemorizingRail funnel CTA.

Why this design? The separation of the FSRS‑6 core from the per-surface schedulers means each surface can add its own onboarding logic (like the ladder) and session mechanics (like the Leitner queue) without touching the shared scheduling math. The learning-science pages are compiled entirely at build time from static JSON (data/learning-science.json, data/technique-recommendations.json) and hand-authored ReactNode prose — no runtime queries, no API calls. This trades compile-time authoring for runtime flexibility: the evidence base is frozen at deploy time, while the scheduling logic can be tuned independently for each surface. The pure functions in lib/loci-scheduler.ts ensure that scheduling decisions are deterministic and testable, and the shared fsrsUpdate keeps the DSR state honest across both the ladder and the free-running phase. Together, these files form a system where the science (the static evidence pages) and the practice (the dynamic schedulers) are cleanly separated, each built for its own trade-off: verifiability and discoverability for the former, interactivity and personalization for the latter.

ELI5 — explain it simply
1
Gist

The site doesn't just show you flashcards — it schedules them with a real memory algorithm, and it also publishes the science behind why that works.

2Morego a level deeper

/memorize and /loci decide when to show each card using FSRS, a spaced-repetition algorithm; /how-it-works/learning-science is a catalogue of study techniques and the peer-reviewed papers that back them; and /how-it-works/learning-science#principles explains the underlying reasons they work.

3Deepthe full mechanics

The pure-function FSRS-6 scheduler (onboarding ladder → stability-based intervals at 90% retention, in-session Leitner recycling, cue-fingerprint encoding specificity) is the implementation; /how-it-works/learning-science catalogues evidence-backed techniques with DOI-verified papers (built by roadmap-kg into data/learning-science.json); and /how-it-works/learning-science#principles is a 13-principle deep dive (encoding/practice/structure) whose principle #13, encoding specificity, is exactly the cue-fingerprint rule the scheduler enforces.

Code references

Where this chapter's machinery lives in the repo:

lib/loci-scheduler.ts:34-42

The onboarding ladder: fixed rungs (onboard1 through onboard3, then fsrs) with default intervals of 10 minutes, 1 day, and 3 days before free-running FSRS takes over.

typescript
export const PHASES: readonly LociPhase[] = [
  "onboard1",
  "onboard2",
  "onboard3",
  "fsrs",
] as const;

/** Default onboarding ladder in minutes ([onboard1, onboard2, onboard3]). */
export const DEFAULT_ONBOARDING_MINUTES: readonly number[] = [10, 1440, 4320];

lib/loci-scheduler.ts:111-166

gradeCard always runs fsrsUpdate so the latent stability/difficulty stays honest, but overrides the interval with the ladder rung until the card reaches the fsrs phase, and stamps the cue fingerprint into the returned state.

typescript
export function gradeCard(
  prior: LociSchedState | null,
  rating: FsrsRating,
  opts: {
    now?: number;
    ladderMinutes?: readonly number[];
    retention?: number;
    /** The card's current `cue_fingerprint`, stamped into the returned state. */
    cueFp?: string;
  } = {},
): GradeOutcome {
  const now = opts.now ?? Date.now();
  const ladder =
    opts.ladderMinutes && opts.ladderMinutes.length >= 3
      ? opts.ladderMinutes
      : DEFAULT_ONBOARDING_MINUTES;

  const fromPhase: LociPhase = prior?.phase ?? "onboard1";
  const priorFsrs: FsrsState | null = prior
    ? { stability: prior.stability, difficulty: prior.difficulty }
    : null;

  const elapsedDays = prior?.lastReviewAt
    ? Math.max(0, (now - prior.lastReviewAt) / MS_PER_DAY)
    : 0;

  const nextFsrs = fsrsUpdate(priorFsrs, rating, elapsedDays);

  const nextIdx = nextPhaseIndex(phaseIndex(fromPhase), rating);
  const nextPhase = PHASES[nextIdx];

  // Interval: ladder rung while onboarding, else the FSRS retention interval.
  let intervalMs: number;
  if (nextPhase === "fsrs") {
    const days = intervalForRetention(nextFsrs.stability, opts.retention ?? 0.9);
    intervalMs = Math.max(MS_PER_MIN, days * MS_PER_DAY);
  } else {
    intervalMs = Math.max(1, ladder[nextIdx] ?? DEFAULT_ONBOARDING_MINUTES[nextIdx]) * MS_PER_MIN;
  }

  const isLapse = rating === 1;
  const state: LociSchedState = {
    stability: nextFsrs.stability,
    difficulty: nextFsrs.difficulty,
    dueAt: now + intervalMs,
    lastReviewAt: now,
    reps: isLapse ? 0 : (prior?.reps ?? 0) + 1,
    lapses: (prior?.lapses ?? 0) + (isLapse ? 1 : 0),
    phase: nextPhase,
    ...(opts.cueFp || prior?.cueFp
      ? { cueFp: opts.cueFp ?? prior?.cueFp }
      : {}),
  };

  return { state, intervalMs, isLapse, fromPhase };
}

lib/loci-scheduler.ts:255-303

gradeSessionCard holds the in-session Leitner rule in one place: an again grade recycles the card to the back of the queue, but only a card's first grade is persisted to FSRS — recycles are practice-only.

typescript
export function gradeSessionCard(
  session: LociSession,
  prior: LociSchedState | null,
  rating: FsrsRating,
  opts: {
    now?: number;
    ladderMinutes?: readonly number[];
    retention?: number;
    /** The card's current `cue_fingerprint`, threaded through to `gradeCard`. */
    cueFp?: string;
  } = {},
): SessionGradeResult {
  const now = opts.now ?? Date.now();
  const card = session.queue[0];
  if (!card) {
    // Nothing to grade — return the session unchanged.
    return {
      session,
      card: { cardId: "", prompt: "", answer: "", stopName: "" },
      outcome: gradeCard(prior, rating, { now, ...opts }),
      persist: false,
      recycled: false,
    };
  }

  const persist = !session.gradedOnce.includes(card.cardId);
  const outcome = gradeCard(prior, rating, {
    now,
    ladderMinutes: opts.ladderMinutes,
    retention: opts.retention,
    cueFp: opts.cueFp,
  });

  const recycled = rating === 1 && !sessionElapsedExceeded(session, now);
  const rest = session.queue.slice(1);
  const nextQueue = recycled ? [...rest, card] : rest;

  const nextSession: LociSession = {
    ...session,
    queue: nextQueue,
    gradedOnce: persist ? [...session.gradedOnce, card.cardId] : session.gradedOnce,
    results: [
      ...session.results,
      { cardId: card.cardId, rating, persisted: persist, recycled, at: now },
    ],
  };

  return { session: nextSession, card, outcome, persist, recycled };
}

app/how-it-works/learning-science/page.tsx:14-21

The Memory Science catalogue: a static server-rendered index of evidence-backed techniques and their papers, assembled by the roadmap-kg memory-techniques lane into data/learning-science.json.

tsx
  CITATION_COUNT,
  principlesIn,
} from "./content";
import {
  techniques,
  techniqueCount,
  paperCount,
  GoalRecommendations,

app/how-it-works/learning-science/_principles.tsx:17-24

The Memory Principles deep dive: the 13 underlying principles (mechanism, evidence, boundary conditions, mnemonic-generation implications); hand-authored content lives in ./content.tsx.

tsx
// References list. Hand-authored content lives in ./content.tsx; this file
// only renders. Principle cards carry id={`principle-${slug}`} — four slugs
// (chunking, desirable-difficulties, generation-effect, retrieval-practice)
// also exist as technique slugs, and the technique cards own the bare anchors.

// Inline author-year citation link → the #ref-{key} entry in the References
// section. Content prose names studies in plain text (essay voice); these
// chips appear after each evidence finding.

app/how-it-works/learning-science/content.tsx:1749-1751

Principle #13, encoding specificity — the formal treatment of the same cue-fingerprint rule the /loci and /memorize scheduler enforces.

tsx
      {
        finding: (
          <>
STUDY AIDSevidence-backed memory techniques
Recall check

What is the name of the shared scheduling model used by both the /memorize and /loci surfaces?

Show answer

FSRS‑6 model

12. The guide family and its navigation

The How-It-Works guide is the hub of a small but growing family of companion guides. Alongside the central overview, there are pillar walkthroughs (first-principles, autonomy, agentic-rag, evals), primers for LangGraph, LangSmith, and LlamaIndex, a memory-science deep-dive, a stack reference, and a glossary. Every one of these pages lives in its own route under /how-it-works/ and has its own Text and (where appropriate) Audio view, yet they all share a single navigation chrome: a horizontal strip of hub pills, a per-guide Text/Audio tab bar, and a sequential Back/Next pager. The navigation is built from a single source of truth, a plain data array called CASE_STUDY_GUIDES in lib/case-study-guides.ts. That file is pure data — no JSX, no browser globals — so any server component can import it freely.

Each element of the array is a CaseStudyGuide object with fields: slug (the route segment), basePath (the route prefix for that guide), title, short (compact label for pills), icon (emoji), kind (audio or text), and two optional flags: single (a one-page guide without Text/Audio tabs, like the glossary) and textAtBase (the Text view lives directly at basePath rather than basePath/text, used for guides whose reading page predates the audio companion). This array is the registry — there is no second list of links anywhere.

The shared chrome is rendered by the CaseStudyGuideNav component in components/case-study-guide-nav.tsx. It receives the active guide’s slug and the active tab, and drives three navigational elements from the same CASE_STUDY_GUIDES array. The hub pills iterate over every guide, calling guideHref(g) to compute the correct URL. That helper uses the guide’s single and textAtBase flags to decide whether the link should point to basePath/text, basePath, or basePath directly. The current guide gets aria-current="page" and a visual highlight. The per-guide tab bar appears only if the active guide is not a single page; it renders two Link components — one for textHref (which uses guideHref(guide) logic) and one for audio at basePath/audio. The tab labels are fixed, but the URLs are derived from basePath. The Back/Next pager finds the index of the active guide in the array, then computes prev and next by indexing adjacent positions; each link’s href is again computed with guideHref. Every navigation link — hub pills, tabs, pager arrows — uses the same registry.

The purpose of this design is straightforward but crucial: one registry drives all navigation. To add a new companion guide, a developer appends one object to CASE_STUDY_GUIDES. The hub instantly includes it, the pager sequence expands, and any tab links that depend on the guide’s basePath are automatically correct. There is no need to update a separate sidebar, breadcrumb, or pager file. The risk of broken links, stale entries, or inconsistent labels is eliminated. The trade-off is that all guides must follow the same route pattern — but that consistency is exactly what makes the family navigable. The data structure is small enough to remain readable, and the component stays a server component because it receives props (activeSlug, activeTab) rather than reading search params or state. The result is a navigation system that scales to any number of companions without growing in complexity.

ELI5 — explain it simply
1
Gist

This page is part of a little set of guides that all share the same menu, tabs, and back/next buttons. One list decides what's in the family and in what order.

2Morego a level deeper

There's a single list of guides — name, address, icon — and the navigation reads that list to build the jump-between pills, the Text/Audio tabs, and the previous/next arrows. Nothing gets out of sync, and it's also how the whole set was moved to a new address by editing one file.

3Deepthe full mechanics

lib/case-study-guides.ts is a typed array (slug, basePath, title, short, icon, kind, and single?/textAtBase? flags). GuideNav — a prop-driven server component, self-styled via scoped classes — derives the hub pills, tab hrefs, and a Back/Next pager entirely from basePath plus array order. A single guide hides tabs and links to basePath; a textAtBase guide keeps Text at basePath with audio at /audio; one guideHref helper is shared by pills and pager so they can't drift. The /case-study to /how-it-works migration was a basePath edit in this one file.

Code references

Where this chapter's machinery lives in the repo:

lib/case-study-guides.ts:34-50

The guide-family registry: one typed row per guide (slug, basePath, title, short, icon, and shape flags). Because GuideNav derives every URL from basePath, this array is the single lever for the family's routing and reading order.

typescript
export const CASE_STUDY_GUIDES: CaseStudyGuide[] = [
  { slug: "case-study", basePath: "/how-it-works", title: "How It Works", short: "Overview", icon: "📈", kind: "audio" },
  { slug: "story", basePath: "/how-it-works/story", title: "The Story", short: "Story", icon: "📜", kind: "audio", textAtBase: true },
  { slug: "first-principles", basePath: "/how-it-works/first-principles", title: "First Principles", short: "First Principles", icon: "🧱", kind: "audio", textAtBase: true },
  { slug: "agents-workflows", basePath: "/how-it-works/agents-workflows", title: "Agents & Workflows", short: "Agents & Workflows", icon: "🧩", kind: "audio", textAtBase: true },
  { slug: "knowledge-graph", basePath: "/how-it-works/knowledge-graph", title: "The Knowledge Graph", short: "Knowledge Graph", icon: "🕸️", kind: "text", single: true },
  { slug: "autonomy", basePath: "/how-it-works/autonomy", title: "Agent Autonomy", short: "Autonomy", icon: "🤖", kind: "text" },
  { slug: "agentic-frontier", basePath: "/how-it-works/agentic-frontier", title: "The Agentic Frontier", short: "Frontier", icon: "🚀", kind: "text" },
  { slug: "llamaindex", basePath: "/how-it-works/llamaindex", title: "LlamaIndex Primer", short: "LlamaIndex", icon: "🦙", kind: "audio", textAtBase: true },
  { slug: "agentic-rag", basePath: "/how-it-works/agentic-rag", title: "Agentic RAG & Text-to-SQL", short: "RAG & SQL", icon: "🔍", kind: "text" },
  { slug: "evals", basePath: "/how-it-works/evals", title: "Evaluation & Feedback", short: "Evals", icon: "📊", kind: "text" },
  { slug: "learning-science", basePath: "/how-it-works/learning-science", title: "Memory Science", short: "Memory Science", icon: "📚", kind: "audio", textAtBase: true },
  { slug: "glossary", basePath: "/how-it-works/glossary", title: "Glossary", short: "Glossary", icon: "📖", kind: "text", single: true },
  { slug: "stack", basePath: "/how-it-works/stack", title: "The Stack", short: "The Stack", icon: "🧰", kind: "text", single: true },
];

export function getGuide(slug: string): CaseStudyGuide | undefined {

components/case-study-guide-nav.tsx:80-97

GuideNav derives the text href, the shared guideHref (respecting the single and textAtBase flags), and the Back/Next pager purely from the registry — there is no second link list to keep in sync.

typescript
  const guide = getGuide(activeSlug);
  const base = guide?.basePath ?? "/how-it-works";
  const single = guide?.single ?? false;
  // Most guides put the Text view at `${base}/text`; a `textAtBase` guide
  // (agents-workflows) keeps its reading page AT `base`, with audio at `/audio`.
  const textHref = guide?.textAtBase ? base : `${base}/text`;
  // Each guide's default view: single-page / textAtBase guides link to basePath,
  // the rest to `${basePath}/text`. Shared by BOTH the hub pills and the Back/Next
  // pager so they can't drift.
  const guideHref = (g: (typeof CASE_STUDY_GUIDES)[number]) =>
    g.single || g.textAtBase ? g.basePath : `${g.basePath}/text`;
  // Sequential Back/Next pager over the guide-family order.
  const idx = CASE_STUDY_GUIDES.findIndex((g) => g.slug === activeSlug);
  const prev = idx > 0 ? CASE_STUDY_GUIDES[idx - 1] : undefined;
  const next =
    idx >= 0 && idx < CASE_STUDY_GUIDES.length - 1
      ? CASE_STUDY_GUIDES[idx + 1]
      : undefined;
STUDY AIDSevidence-backed memory techniques
Cloze

The navigation is built from a single source of truth, a plain data array called  ____  in  ____ .

Show answer

CASE_STUDY_GUIDES, lib/case-study-guides.ts

13. The workflow layer: durable, streamable agent runs

The hand-rolled loops that ground claims and tutor flashcards are being ported to event-driven workflows from the llama-index-workflows package, and that port adds zero new dependencies because the pinned llama-index-core already installs that standalone package; llama_index.core.workflow is a one-line shim. In roadmap-kg/kg/claim_workflow.py, the old retrieve → draft → assess → revise loop is re-expressed as @step methods connected by typed events—EvidenceEvent, DraftEvent, ReviseEvent—so the graph’s edges are declared as data rather than hidden inside imperative logic. Each step receives an event, mutates the Context via ctx.store.set, and returns the next event. The workflow runner drains the stream, and every progress event is written to the store and optionally to a JSONL trace.

Durability comes from workflow_store.py, which persists a serialized Context.to_dict snapshot after every progress event. The default store is a local-first SQLite file in .workflow-runs/runs.db, but it also supports Postgres when POSTGRES_URL is set. With a run_id, a crashed run can be resumed via --resume: the store loads the last snapshot, Context.from_dict rebuilds the running state, and only in-flight events are re-queued—completed steps do not re-execute. The store uses a simple runs table with columns for ctx_json, result_json, and timestamps, matching the same schema used by the rag_service twin.

Streaming is wired directly into the service layer: rag_service/main.py mounts a WorkflowServer under /workflows-api (gated by ENABLE_WORKFLOW_SERVER), and the run endpoint in run_routes.py (mentioned in the main file) streams ProgressEvents back to the caller. A consumer can watch the agent think in real time instead of waiting for a single blocking result.

What durable, streamable runs buy over a hand loop is the ability to pause, resume, and observe a long-running reasoning process without losing progress or redoing work. The declarative @step/typed-event pattern makes the graph explicit—every transition is visible in the code as an event type and a handler method—rather than buried in a while loop. And because the workflow lives on the same FastAPI app that already indexes lessons and serves the Next.js frontend, the /workflows-api surface is just another route, deployable alongside highlight RAG without changing the existing architecture.

ELI5 — explain it simply
1
Gist

The site's content robots used to run as one long script each. Now each robot is a set of small steps that pass messages to each other — so a run can be watched live, saved mid-way, and picked up after a crash without redoing finished work.

2Morego a level deeper

The claim-writing agent was rebuilt as a workflow: typed events connect its retrieve → draft → check → revise steps. After every step the run's whole state is saved into a tiny SQLite file, so a crashed run resumes where it left off. Each event can also be appended to a trace file for debugging. The old loop is kept around, and a test proves old and new produce identical results. The flashcard tutor uses the same trick to stream its progress live, and a generic workflow server exists but stays switched off unless a flag turns it on.

3Deepthe full mechanics

kg/claim_workflow.py ports kg/grounded_claim_agent.py to llama-index-workflows @step methods with typed events (EvidenceEvent/DraftEvent/ReviseEvent/CiteEvent, stream-only ProgressEvent); zero new deps — the pinned llama-index-core already ships the standalone workflows package. Durability: kg/workflow_store.py upserts Context.to_dict(JsonSerializer()) per streamed event into .workflow-runs/runs.db and resumes via Context.from_dict (WorkflowCheckpointer is deprecated in workflows 1.3.0). Tracing: kg/workflow_trace.py appends one truncated JSON line per event to .agent-traces/<workflow>-<run_id>.jsonl. Service side: tutor_stream.py adapts ReActAgent.run().stream_events() into SSE frames (progress/tool/thought/result) for POST /tutor/card/stream; workflows.server.WorkflowServer mounts at /workflows-api only behind ENABLE_WORKFLOW_SERVER=1, with the x-llamaindex-secret check as Starlette middleware since mounted sub-apps bypass FastAPI deps. make workflow-selftest runs the offline suite, including byte-for-byte parity and crash/resume.

Code references

Where this chapter's machinery lives in the repo:

roadmap-kg/kg/claim_workflow.py:61-94

The graph's edges as typed events — EvidenceEvent carries retrieved corpus evidence to the draft step, DraftEvent carries a candidate claim to the grounding oracle, ReviseEvent loops a rejected claim back with its missing terms, and the stream-only ProgressEvent mirrors the reference loop's trajectory shape.

python


class ClaimStartEvent(StartEvent):
    slug: str


class EvidenceEvent(Event):
    """Corpus evidence retrieved and vocabulary terms computed — ready to draft."""
    evidence: dict
    terms: list


class DraftEvent(Event):
    """A candidate claim (fresh draft or revision) awaiting the grounding oracle."""
    claim: str


class ReviseEvent(Event):
    """The oracle rejected the claim; carry the missing terms back for a rewrite."""
    claim: str
    missing: list


class CiteEvent(Event):
    """Grounding loop finished with ``--verify-citation`` on — check DOIs before stop."""
    result: dict


class ProgressEvent(Event):
    """Stream-only trace of one agent step (same shape as the reference trajectory)."""
    step: int
    action: str
    observation: Any

roadmap-kg/kg/workflow_store.py:57-77

The whole durability story: save_ctx upserts the latest serialized Context per run (one SQLite row, latest snapshot wins), and load_ctx hands it back for Context.from_dict resume — no deprecated WorkflowCheckpointer involved.

python
    run_id             TEXT PRIMARY KEY,
    workflow           TEXT NOT NULL,
    status             TEXT NOT NULL,
    ctx_json           JSONB,
    result_json        JSONB,
    pending_event_json JSONB,
    created_at         DOUBLE PRECISION NOT NULL,
    updated_at         DOUBLE PRECISION NOT NULL
);
CREATE INDEX IF NOT EXISTS runs_run_id_created_at_idx ON runs (run_id, created_at);
"""

_PG_CONN = None  # module-level lazy singleton — see _pg_connection


def _db_path() -> Path:
    return Path(os.environ.get("WORKFLOW_RUNS_DB", str(_DEFAULT_DB)))


def _pg_connection(url: str):
    """Lazy singleton Postgres connection, reused across every ``WorkflowRunStore()``

services/llamaindex/rag_service/main.py:27-32

The opt-in gate, verbatim: the generic WorkflowServer REST surface mounts under /workflows-api only when ENABLE_WORKFLOW_SERVER=1, so a production deploy without the flag is byte-identical.

python
from .research_routes import router as research_router
from .routes import router
from .runtime import get_runtime
from .settings import get_settings
from .sql_routes import router as sql_router
from .trajectory_routes import router as trajectory_router
STUDY AIDSevidence-backed memory techniques
Spaced review

In a few days, come back and re-test yourself on three concrete ideas from this section: how @step methods with typed events (like EvidenceEvent) make the workflow graph explicit, how the workflow store persists snapshots to resume a crashed run, and how streaming ProgressEvents lets you watch the agent think in real time.

System design themes behind this site

This knowledge base is a Next.js App Router app compiled onto a single Cloudflare Worker — and most of what makes it work is a handful of system-design principles applied at every layer. Each one below is anchored to the real file that implements it.

Ship content as code. Every guide body lives in committed JSON under data/ and is statically imported at build time. The serverless runtime has no reliable request-time filesystem, so anything a page needs is compiled into the bundle (or traced in via outputFileTracingIncludes) — which also means content deploys are atomic: a page and its data can never disagree in production. What must change without a rebuild (auth, progress, highlights) lives in D1 instead.

Gate the build, not the deploy. package.json wires the audio-manifest suite in front of every build (prebuildtest:audio:all), so a broken manifest cannot reach the bundler. Deploy is Vercel's prebuilt-archive flow (scripts/deploy.sh): vercel build --prodvercel deploy --prebuilt, after which the script asserts the apex serves before treating the deploy as live.

One registry drives navigation. The guide family (this page, the audio case study, the LlamaIndex primer, the pillar deep-dives) is one array in lib/case-study-guides.ts. The hub pills, tabs, and pager all derive from its basePath values — the 2026-07-02 URL rename of the whole family was, at the nav layer, an edit to that single file.

Local-first ML at build time. The grounding pipeline embeds with FastEmbed (bge-small) in-process — no embedding API, no key — and memoizes every vector in a SQLite cache keyed by sha256(model ‖ text) (roadmap-kg/kg/memory_common.py). Re-indexing an unchanged corpus costs near zero, and the pipeline runs on a laptop with the network unplugged. Serving is the deliberate exception: the production RAG service embeds queries remotely (the same bge model via Workers AI through the Cloudflare AI Gateway, EMBED_REMOTE=1) because the in-process ONNX runtime alone nearly fills its 512Mi free-tier instance — the full story is in the RAG serving deep dive.

Degrade gracefully, fail honestly. LLM egress (roadmap-kg/kg/llm.py) tries three tiers in order: the direct DeepSeek API, a local llama.cpp server (LLM_BASE_URL), then Cloudflare AI Gateway. When every tier is down, generation stops — pages ship a "being prepared" placeholder rather than fabricated prose. (This guide's own rewrite was hand-authored during exactly such an outage.)

Trust, but verify. Generated pages must quote real code: roadmap-kg/kg/ground.py checks every excerpt against the retrieved source and drops sections that fail after three retries. The code references on this page follow the same rule mechanically — scripts/build-code-refs-hiw.mjs slices cited line ranges verbatim out of the repo and refuses to build if a range no longer exists.

Stay same-origin. Narration MP3s live on R2, but the player streams them through the app's own /api/audio proxy (Range/206), so playback needs no CORS and can't be stalled by the service worker (toSameOriginAudioUrl above).

Design for the constraint. The serverless runtime shapes the odd-looking choices: the build must use webpack (next build --webpack — turbopack emits no output file traces, and the functions need those to read data/**), the scheduled work runs as Vercel Crons (vercel.json/api/cron/backup, /api/cron/kg-tick), and both local dev and prod read the same D1 over its REST API — a documented, deliberate trade-off.

The global theme: single source of truth, gates in front of builds, local-first computation, honest failure, and verification over trust — the same principles every guide on this site keeps returning to. System design is less a topic than the lens.

The learning science behind it

This site is not just a place to read about learning — several of its own features are working applications of memory science. Each lens below pairs one feature of the guide with the documented memory-science principle it puts into practice, states the mechanism in plain terms, and links to the full write-up on the learning-science principles page. The prose is generated by LlamaIndex, grounded ONLY in that principles corpus — not paraphrased from memory.


Encoding specificity and transfer-appropriate processing makes memory stick because a cue only retrieves a trace if it was encoded with that exact cue. The study site applies this by using the same local model to encode every stored chunk and every incoming question, ensuring the retrieval cue lives in the same representational space as the original encoding, so it can land near the right trace.

Powers Local-first embeddings

Machine-learning analog Retrieval-augmented generation

Godden & Baddeley (1975) · Kang et al. (2025) · Lewis et al. (2020) · Tulving & Thomson (1973)


Desirable difficulties works because effortful processing builds storage strength, distinct from retrieval strength, making memory more durable. The verification gate embodies this by deliberately making each page harder: generated code is checked against the real source, failures trigger feedback and retries. This slower, more effortful process forces deeper processing, and because the difficulty is overcome, what survives is far more reliable.

Powers Trust but verify

Machine-learning analog Curriculum learning

Bengio et al. (2009) · Bjork (1994) · Hwang et al. (2026) · Lee et al. (2026) · Shu et al. (2026)


The spacing effect works because distributed practice beats massed practice — memories restudied after a delay are retained far longer than those crammed back to back. Expanding schedules (1 day, 3 days, 10 days, 30 days) are implemented by FSRS. This site’s review queue uses FSRS-6, which fits individual forgetting curves per item per user, applying the spacing effect.

Powers Memory science: the evidence base and the schedulers

Machine-learning analog Experience replay (continual learning)

Cepeda et al. (2006) · Cepeda et al. (2008) · Ebbinghaus (1885) · Rolnick et al. (2019) · Settles & Meeder (2016) · Spens & Burgess (2024) · Xiao & Wang (2024)


Effortful retrieval strengthens memory by multiplying retrieval routes, unlike passive re-exposure. This flashcard feature applies Retrieval practice (the testing effect) by requiring learners to pull each answer from memory before it is revealed, forcing the effortful recall that builds durable traces rather than the illusion of learning from re-reading.

Powers /memorize

Machine-learning analog Associative memory / pattern completion

Eldho Paul & Sunar (2026) · Karpicke & Blunt (2011) · Ramsauer et al. (2020) · Roediger & Karpicke (2006)


The generation effect holds that self-produced material is remembered better than material simply received. On this study site, a pipeline generates each page section from retrieved source material rather than copying it. By producing its own content, the system applies the principle: building the page itself leads to stronger representation and retention than if the content were merely handed to it.

Powers The content grounding pipeline

Machine-learning analog Self-generated training data (STaR)

Shao et al. (2026) · Slamecka & Graf (1978) · Zelikman et al. (2022)

Structure

Working memory is limited in chunks, not bits, so recoding raw material into meaningful units multiplies capacity. A guide that breaks a large body of material into a small set of separately navigable chapters, each holding a few related ideas, applies Chunking by recoding the whole subject into manageable grouped units, preventing overload and making learning stick.

Powers The guide family and its navigation

Machine-learning analog Subword tokenization (BPE)

Cowan (2001) · Elsner et al. (2026) · Ericsson et al. (1980) · Lee et al. (2025) · Miller (1956) · Sennrich et al. (2016)

The dependency structure is the shared mechanism that integrates these principles into a single learning system. Encoding quality (principles 1-7) sets the starting strength of a trace, retrieval practice (9-10) grows that strength, spacing (8) determines whether growth compounds or decays, and encoding specificity (13) keeps the whole loop connected. A study site built on all of them avoids the failure of impressive demos with poor month-three retention, because encoding techniques alone need a spaced retrieval schedule for lasting effect.

Explore the memory principles →