Back to Knowledge Base

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

This learning app is a self-paced AI-engineering curriculum delivered as a single Next.js App Router application, deployed to one Cloudflare Worker. The entire site—interactive explainers, persistent audiobook player, lesson pages—is a static-plus-dynamic hybrid where every piece of content resolves from the same ordered source of truth.

The curriculum lives as plain markdown files under content/. Each file is named by its slug (e.g., tokenization.md) and contains a level-1 heading for the title, followed by body text. The critical structural constant is LESSON_SLUGS in lib/articles.ts. This array lists every slug in the exact order the authors intend students to study—starting with the foundation phase (lessons 1–9) and running through phase 8 (lessons 73–80). Position in this list fixes each lesson’s number; a derived constant LESSON_NUMBER maps slug to that 1-indexed position. The array also defines the audiobook spine: the persistent player auto-advances through slugs in this order, and anything beyond lesson 80 (the Appendix) is excluded from continuous-play but is still reachable via direct links.

lib/data.ts consumes that ordered list to build lesson metadata. The function getAllLessons() reads every .md file from content/, filters only those slugs that appear in LESSON_NUMBER, then extracts title, excerpt, word count, reading time, and difficulty (computed from the lesson’s position inside its phase). The resulting Lesson objects are sorted by number, producing a static array that drives the roadmap, category groupings, and the audio player’s playlist.

When a user requests a specific lesson, getLessonBySlug(slug) in lib/data.ts resolves the content. It follows a three-layer fallback: first it tries to fetch from a D1 database (the content-cache written by a Rust sync-d1 pipeline—new or edited markdown appears after a refresh without redeploying the Worker). If D1 is unconfigured or empty, it falls through to a JSON file exported at build time by export-content. As a last resort, it reads the raw markdown from disk using resolveContentFile(slug), which constructs the path content/${slug}.md and parses the file with fs.readFileSync. The content is then combined with the metadata into a LessonWithContent object.

This single ordered list is the source of truth for both numbering and the play order because it prevents the kind of drift that would occur if numbering were derived from filesystem ordering, a database row, or a separate menu config. The list is version-controlled alongside the markdown files, so adding a new lesson or reordering an existing one is a single edit in one place: insert the slug at the correct index, bump subsequent numbers automatically via the position-based LESSON_NUMBER map, and the rest of the system—roadmap grouping, difficulty calculation, player progression, URL paths—adjusts consistently. The design trades the flexibility of dynamic ordering for a guarantee that every lesson’s number and position is deterministic, auditable, and synchronized with the author’s intended learning sequence.

ELI5 — explain it simply
1
Gist

It's a website that teaches you how to become an AI engineer, plus a bunch of interactive explainers. It's one app running on one server at the edge of the network.

2Morego a level deeper

The lessons are markdown files kept in a fixed order, and that order is also the playlist for the audio version. The other pages are hand-built interactive guides whose content is baked into the build as JSON; the lessons themselves are served from a small edge database. Either way the server never reads files while you browse.

3Deepthe full mechanics

It's a Next.js App Router app: a dynamic [slug] route serves each lesson D1-first from the content_cache table (synced from content/ markdown by the content pipeline, ordered by LESSON_SLUGS, and revalidated hourly via ISR tags); guide pages statically import committed data/*.json; either way the Cloudflare Worker never does a runtime filesystem read — lesson reads go through the Worker's native DB binding.

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 Rust `sync-d1`
// pipeline → 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 is the name of the array that lists every lesson slug in the exact order intended for study?

Show answer

LESSON_SLUGS

02. Build and deploy

The deployment pipeline for this site is built around a deliberate inversion of the usual “deploy fast, test later” philosophy: gate the build, not the deploy. Every broken audio manifest, missing section, or malformed JSON is caught before a single byte reaches the edge, so the production site almost never sees a 500 from bad data. The price is a longer build sequence, but the reward is a deploy that is trusted to serve.

The chain begins in package.json with the cf:deploy script, which runs three steps in order: cf:build, wrangler deploy, and cf:verify. The build step itself is a composition. First, npm run test:audio:all (also hooked as prebuild) checks every audio‑guide metadata file offline using scripts/test-audio-meta.mjs. This is the integrity gate: if a .json is missing a required field or references a non‑existent audio file, the script exits non‑zero and the entire deploy aborts. Only after that passes does the actual compilation begin.

The Next.js build is pinned to next build --webpack. This is not accidental. Next 16 defaults to the new Rust‑based bundler (Turbopack for production), but OpenNext—the adapter that converts the Next output into a Cloudflare Worker bundle—expects Webpack’s module graph. Using the default bundler produces a .next directory that OpenNext cannot reshape into a valid Worker; the --webpack flag forces the older, fully compatible Webpack pipeline. It is slower, but re‑guarantees that the intermediate artifacts are consumable.

After next build finishes, OpenNext runs with --skipNextBuild (because the Next build already happened) and emits a single Worker bundle into .open-next/worker.js. This is the actual server‑side logic, but it is not referenced directly in the deployment configuration. Instead, wrangler.jsonc declares its entry point as workers/app/index.ts. That file is a thin wrapper—its job is to import .open-next/worker.js and re‑export its fetch handler, while also attaching the scheduled cron handler. The wrapper pattern is a deliberate decoupling: the build can change the internal structure of .open-next/ without requiring a change to the deployment configuration, and it keeps the wrangler.jsonc stable across OpenNext upgrades.

Once the Worker bundle is ready, wrangler deploy uploads it to Cloudflare. The deploy command exits 0 as soon as the upload succeeds—it does not verify that the site actually renders. This is a known blind spot: a revoked D1 token or a misconfigured binding can leave every route returning a 500 while the deploy “succeeds”. That hole is patched by npm run cf:verify, which runs scripts/verify-deploy.sh. The script issues curl requests to two routes: the apex (/) and a dynamic, D1‑backed lesson route (/embeddings). Each request includes a cache‑busting query parameter (?cb=<timestamp>-<attempt>) so that edge cached responses don’t mask a runtime failure. It retries up to five times with a four‑second sleep between attempts (both configurable via environment variables) to absorb cold starts and edge propagation delays. Only when both routes return 200 does the script exit cleanly; otherwise it prints a failure message and suggests running npx wrangler tail to inspect the logs.

The full pipeline—integrity gate, Webpack‑backed Next build, OpenNext emission, wrapper entry point, and post‑deploy smoke test—means that a broken manifest never reaches production. If the build passes, the deploy is almost guaranteed to work. If the deploy succeeds, the smoke test confirms it. The only remaining risk is a runtime failure that occurs after the first request (e.g., a D1 query that times out under load), but that is a different class of problem, handled by monitoring and alerting. For the data‑path failures that historically haunted this site, the chain has proven robust.

ELI5 — explain it simply
1
Gist

Turn the code into a bundle a Cloudflare edge server can run, upload it, and then poke the live site to make sure it actually loads before calling it done.

2Morego a level deeper

Build with Next (using webpack), repackage it for Cloudflare Workers with OpenNext, let wrangler upload it, then run a little smoke script that requests two pages and fails the deploy if they don't come back healthy.

3Deepthe full mechanics

cf:build runs the audio tests, next build --webpack, and the OpenNext build; wrangler deploy bundles workers/app/index.ts (which wraps .open-next/worker.js and adds the cron handler); verify-deploy.sh cache-busts and retries the apex plus a D1-backed route. On merge to main, CI clones the monorepo for workspace deps and overlays the app at the merged SHA before running the same chain.

Code references

Where this chapter's machinery lives in the repo:

package.json:17-23

The build → deploy → verify script chain (cf:build, cf:deploy, cf:verify).

json
    "build": "NODE_OPTIONS=--disable-warning=DEP0040 next build --webpack",
    "start": "next start",
    "deploy": "npm run cf:deploy",
    "cf:build": "npm run test:audio:all && NODE_OPTIONS=--disable-warning=DEP0040 next build --webpack && NODE_OPTIONS=--disable-warning=DEP0040 opennextjs-cloudflare build --skipNextBuild",
    "cf:preview": "npm run cf:build && wrangler dev",
    "cf:deploy": "npm run cf:build && NODE_OPTIONS=--disable-warning=DEP0040 wrangler deploy && npm run cf:verify",
    "cf:verify": "bash scripts/verify-deploy.sh",

scripts/verify-deploy.sh:30-47

The post-deploy smoke loop: retry each route, fail if it never returns 200.

bash
for route in "${ROUTES[@]}"; do
  ok=0
  for attempt in $(seq 1 "$RETRIES"); do
    cb="$(date +%s)-$attempt"
    code="$(curl -s -o /dev/null -w '%{http_code}' "${BASE}${route}?cb=${cb}" || echo 000)"
    if [ "$code" = "200" ]; then
      echo "  ✓ ${route} → 200 (attempt ${attempt})"
      ok=1
      break
    fi
    echo "  … ${route}${code} (attempt ${attempt}/${RETRIES})"
    [ "$attempt" -lt "$RETRIES" ] && sleep "$SLEEP"
  done
  if [ "$ok" -ne 1 ]; then
    echo "  ✘ ${route} never returned 200 after ${RETRIES} attempts"
    fail=1
  fi
done

wrangler.jsonc:74-80

The Worker's cron triggers (daily D1→R2 backup, hourly KG tick) and observability.

json
    // NEON_AUTH_COOKIE_SECRET worker secret — `wrangler secret put …`, also in
    // scripts/sync-worker-creds.sh). Provisioned via `neonctl neon-auth enable
    // --project-id damp-scene-16205296`. lib/auth.ts resolves the session and
    // admits only OWNER_EMAIL; until the URL is real, owner routes fail closed.
    "NEON_AUTH_BASE_URL": "https://ep-purple-hat-as5nl249.neonauth.c-4.eu-central-1.aws.neon.tech/neondb/auth"
  },
  // Daily D1→R2 backup (/api/cron/backup) + hourly autonomous KG loop (/api/cron/kg-tick).
STUDY AIDSevidence-backed memory techniques
Cloze

This is the  ____ : if a .json is missing a required field or references a non-existent audio file, the script exits non-zero and the entire deploy aborts.

Show answer

integrity gate

03. The content grounding pipeline

The guide pages are not written from memory; they are generated by retrieval over the real source files, using the content grounding pipeline assembled in roadmap-kg/kg/memory_common.py. The pipeline’s first step is to split the raw source text into chunks suitable for embedding and search. This is done by _default_splitter, a SentenceSplitter from LlamaIndex that cuts text into tokens. By default it uses a chunk size of 512 tokens and a chunk overlap of 64 tokens, both tunable via the environment variables CHUNK_SIZE and CHUNK_OVERLAP. Every memory lane — the on‑page reading lane and the spoken audio lane — uses this identical splitter so they segment source content the same way.

The overlap is not a cosmetic detail; it is the precondition for faithful retrieval. An important fact or phrase that straddles the exact boundary between two 512‑token chunks would be truncated in one and start in the next, making it invisible to a nearest‑neighbour search that only sees whole chunks. By overlapping the chunks, each boundary fact is fully present in at least one chunk, and often appears redundantly in two, so retrieval never loses the middle of a sentence or a cross‑chunk reference. Without overlap, the pipeline would systematically miss content at chunk seams, producing gaps in the generated guide.

Once the source is split, each chunk is embedded into a vector. The embedding is performed by a call to cached_text_embeddings, which consults a persistent SQLite cache keyed on a SHA‑256 hash of the text and the model name (default BAAI/bge-small-en-v1.5). This cache lives at _APP_ROOT / ".embed-cache" / "embeddings.db" and is shared with the /query service, so repeated grounding runs re‑use vectors instead of re‑embedding unchanged texts. The cache can be bypassed with EMBED_CACHE=0 for benchmarking.

The embedded chunks are then assembled into a searchable index. The function sync_nodes (also in memory_common.py) takes the list of chunk nodes and their embeddings, and pushes them into a vector store. By default the store is Qdrant, falling back to an in‑memory VectorStoreIndex when Qdrant is not configured. The index is opened via open_vector_index with the same namespace (e.g. "inline-memory" or "audio-memory"), ensuring each lane has its own collection. A single document ID is attached via _attach_scope so chapters can be tracked as groups.

This index is the engine that powers the recall cues and callback devices in the guide. When a chapter needs a grounded term or sentence to quiz the reader, the pipeline queries the index for the most relevant chunk, extracts the centermost term using term_centroid_score (a cosine‑bonus scoring function that rewards terms that are semantically close to the chapter’s centroid), and presents it as the recall answer. The chunk boundary that overlap made safe is now the boundary that delivers a complete, answerable fact. The entire chain – split, embed, index, retrieve, score – runs over the real source, not a human‑written summary, which means the generated guide stays mechanically faithful to the codebase it explains.

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 is the first step in the content grounding pipeline?

Options: split the raw source text into chunks suitable for embedding and search · embed each chunk into a vector · assemble chunks into a searchable index · retrieve the centermost term using term_centroid_score

Show answer

split the raw source text into chunks suitable for embedding and search

04. Local-first embeddings

The site never stores an embedding API key. Every vector in the pipeline is produced locally by a single in-process FastEmbed model built in roadmap-kg/kg/llm.py’s make_embed function. That function instantiates a FastEmbedEmbedding with the model named by the environment variable EMBED_MODEL (defaulting to BAAI/bge-small-en-v1.5, 384 dimensions). The ONNX weights are downloaded exactly once per process life: the constructed object is stored in a module-level dictionary _EMBED_CACHE keyed by the model name, so any subsequent call to make_embed returns the same object without re‑loading the weights. This means there is no embedding API key anywhere in the stack — the operation is purely local and works with the network unplugged.

That local model alone would still re‑embed the entire corpus every time a lane runs (audio memory, inline memory, mnemonic grounding). To eliminate that cost, roadmap-kg/kg/memory_common.py provides a persistent cache: cached_text_embeddings. It accepts a list of texts, computes a SHA‑256 hash of model + separator + text via embed_cache_key (optionally partitioned by a namespace), and checks a local SQLite database. Only the texts whose hashes are not yet stored are sent to the embedding model; all others are returned from the cache as packed float32 vectors. The database lives at the path given by _embed_cache_path() (which defaults to a .embed-cache/embeddings.db in the app root), and the cache can be disabled entirely by setting EMBED_CACHE=0.

The trade‑off this design buys is speed and autonomy. Because the cache key includes the model name, swapping EMBED_MODEL is a clean miss — no stale vectors from a different model can leak in. And because the cache is incremental (one row per unique text, not per batch), editing a single chapter re‑embeds only that chapter’s text; the rest are served from disk at near‑zero cost. The result is a pipeline that runs locally, requires no network, and on the second run over an unchanged corpus makes zero embeddings calls — the same ~98× repeat‑run speedup that the /query service already enjoyed. The only side effect is the cache file, and the embedding values are bit‑for‑bit identical to what the uncached model would produce.

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
    model_name = os.environ.get("EMBED_MODEL", "BAAI/bge-small-en-v1.5")
    cached = _EMBED_CACHE.get(model_name)
    if cached is not None:
        return cached

    from llama_index.embeddings.fastembed import FastEmbedEmbedding

    kwargs: dict = {"model_name": model_name}
    threads = _opt_int("EMBED_THREADS")
    if threads is not None:
        kwargs["threads"] = threads
    cache_dir = os.environ.get("EMBED_CACHE_DIR", "").strip()
    if cache_dir:
        kwargs["cache_dir"] = cache_dir

    embed = FastEmbedEmbedding(**kwargs)
    _EMBED_CACHE[model_name] = embed
    return embed

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

Every vector in the pipeline is produced locally by a single in-process  ____  model built in roadmap-kg/kg/llm.py's  ____  function.

Show answer

FastEmbed, make_embed

05. The LLM egress tiers

The language model egress in the roadmap knowledge‑graph service is resolved by make_llm() inside roadmap‑kg/kg/llm.py. That function inspects environment variables in strict, first‑available‑wins order — three tiers, each with its own API base, key source, timeout, and retry configuration. The precedence is enforced by early return statements, not by a cascade that tries whatever is around. That choice is deliberate: it prevents a stray environment variable from silently pulling a production request onto a local model.

The highest‑priority tier fires when DEEPSEEK_API_KEY is set. In that case _build_deepseek() is called directly with api_base=“https://api.deepseek.com”. No fallthrough occurs — the key is sourced out‑of‑band (a one‑shot env or an out‑of‑repo .env file) and must never be committed. The model id is read from DEEPSEEK_MODEL_DIRECT; if it happens to be the legacy deepseek-chat, the code transparently maps it to deepseek-v4-flash, its current non‑thinking successor. A direct key means the code never reaches the local server branch, so a stray LLM_BASE_URL cannot quietly hijack the request.

If no DEEPSEEK_API_KEY exists but LLM_BASE_URL is set, the function instantiates a plain OpenAILike from llama_index.llms.openai_like, pointed at wherever LLM_BASE_URL points — typically a local llama.cpp server or LM Studio. The model field is informational (the server ignores it), and a smaller default context window (LLM_CONTEXT_WINDOW, default 32768) plus a longer timeout (LLM_TIMEOUT, default 300 seconds) compensate for a slower local box. Tool calling is disabled because quantized models handle it unevenly.

The lowest‑priority fallback is the Cloudflare AI Gateway. It calls _build_deepseek() with api_base from deepseek_base_url() (which defaults to a Cloudflare gateway URL, or reads CF_AIG_BASE_URL) and api_key from deepseek_token() (reads CF_AIG_TOKEN). The model id is provider‑prefixed by _gateway_model() — it prepends deepseek/ so the compat surface knows which provider to route to. The same DEEPSEEK_CONTEXT_WINDOW and optional LLM_MAX_TOKENS cap apply across both DeepSeek tiers.

Fixed precedence with early returns is safer than “try whatever is around” because it avoids ambiguous fallthrough when multiple environment variables are accidentally set. If the code attempted a local server only when the direct API call failed, a transient network blip could cause an unexpected local fallback with different behavior and no warning. By committing to the first available tier and returning immediately, the developer controls exactly which path runs, and any mismatched configuration produces a clear error rather than a silent substitution. The LLM_BASE_URL guard is especially sharp: when DEEPSEEK_API_KEY is present, the local branch is unreachable even if LLM_BASE_URL happens to be set elsewhere in the environment — the intention of “I want the real DeepSeek” is honoured without risk of accidental local routing.

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
    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
    # 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(
        model=_gateway_model(os.environ.get("DEEPSEEK_MODEL", "deepseek-v4-pro")),
        api_base=deepseek_base_url(),
        api_key=deepseek_token(),
        timeout=float(os.environ.get("DEEPSEEK_TIMEOUT", "120")),
        max_retries=_opt_int("DEEPSEEK_MAX_RETRIES") or 3,
    )
STUDY AIDSevidence-backed memory techniques
Recall check

What mechanism enforces the fixed precedence of the LLM egress tiers to prevent accidental fallthrough?

Show answer

early returns

06. Trust but verify

A grounded code excerpt is only as reliable as the LLM that produced it. Even when the generator is constrained by a retrieval-augmented pipeline, it can still fabricate plausible-looking APIs — a function called execute might appear correct but never exist in the real source. To catch these hallucinations before they reach a reader, the grounding pipeline adds a lightweight verification step that checks every generated code block against the real source identifiers.

The tokenizer that builds the source’s identifier set is _source_identifiers. It uses a regex _IDENT_RE to extract every whole identifier — sequences like index_documents, _build_query_engine, source_idents — and stores them as a Python set[str]. Whole‑token membership is critical: if the real source contains index_documents, then a hallucinated documents or index must not count as grounded just because those tokens appear as substrings of a longer real name. The old substring‑containment test allowed that loophole, so the current design requires an exact match of the entire token.

The checker function is _code_grounding_violations. It first verifies that the emitted markdown contains exactly one fenced code block (_FENCE_RE). Then it extracts all identifiers from that block, discards generic Python stopwords (_CODE_STOPWORDS like def, return, list), and computes the ratio of grounded identifiers — those that appear in the real source’s identifier set — to total identifiers. If the ratio falls below ²⁄₃ (67%), the function returns a list of violation messages naming the unknown symbols.

When a violation is found, the caller _ground_code_chapter retries the query. It feeds the previous violation messages back into the prompt as explicit feedback — e.g., “only 2/4 identifiers trace to the source — quote real symbols, do not invent APIs”. This loop runs up to max_attempts = 3. After the final attempt, if violations still exist, the chapter is still written to the output JSON, but the ground_code function increments a residual counter and prints a warning ("WARNING residual grounding issues"). The residual count is logged so the content team can manually inspect the offending excerpt, but the system does not silently ship unverified code: the warning flags it for human review. The combination of automated retry and persistent logging enforces the “trust but verify” principle — the pipeline trusts the generator enough to try again, but it verifies that the final output actually draws from the real source before considering it clean.

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
    ),

    # ── short-term-memory (/short-term-memory enrichment, slice B3) ────────────
    # Corpus-only: the two LangGraph/LangChain memory docs are the faithful
    # short-term corpus. email_memory_reflect_graph.py is long-term episodic/
    # semantic memory and is deliberately EXCLUDED to keep the short-term vs

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

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

python
    # data/audio/ (where kg.ground writes the served JSON; /evals precedent).
    "short-term-memory": GroundConfig(
        slug="short-term-memory",
        title="Short-Term Memory — Audio Guide",
        sources=[
            APP_ROOT / "roadmap-kg" / "data" / "langgraph-docs" / "langchain-short-term-memory.md",
            APP_ROOT / "roadmap-kg" / "data" / "langgraph-docs" / "langgraph-memory.md",
        ],
        chapters=[
            ("What Short-Term Memory Is",
             "why a language-model call is stateless and how short-term memory "
             "reintroduces statefulness by selectively feeding recent context into "
             "the next call, and how this differs from long-term memory that persists "
             "across sessions in external storage."),
            ("Message History And Window",
             "how a conversation is held as an ordered list of role and content "
             "messages within a single thread, why the context window is a hard token "
             "budget the history must fit inside, and the trade-off of keeping the full "
             "history versus a bounded window."),
            ("Trimming The History",
             "how trimming keeps only the most recent messages so the prompt stays "
             "inside the token budget, what gets lost when early turns are dropped, and "
             "why the system prompt is kept while older turns are removed."),
            ("Summarizing The History",
             "how summarization compresses earlier turns into a short running summary "
             "that is prepended cheaply, how it preserves the gist at lower token cost, "
             "and the trade-off of staleness and missed detail versus plain trimming."),
            ("Threads And Checkpointers",
             "how a checkpointer persists the conversation state keyed by a thread "

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

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

python
            ("Failure Modes To Watch",
             "the common failures of short-term memory — overflowing the context "
             "window, important facts lost in the middle of a long prompt, a stale "
             "summary acting on outdated facts, and state leaking across threads."),
            ("Testing And Operations",
             "how to count tokens before each call, how to test recall by asking for a "
             "fact many turns later, and how tracing the exact message list sent to the "
             "model makes a memory system observable and debuggable."),
        ],
    ),
}


def _ground_chapter(qe, title: str, ask: str, max_attempts: int = 3,
                    words: int = 200) -> tuple[str, str, list[str]]:
    """Query the engine for one chapter, retrying with gate feedback if the
    spoken script trips a hard audio rule OR drifts off the retrieved evidence.
    Returns (script, reading, residual HARD violations). The local model
    converges instead of shipping a known failure; the Rust audio-gate is still
    the authoritative final check.

    Three feedback signals drive the retry. HARD audio-rule violations and HARD
    groundedness violations (a refusal, an empty-response sentinel, or meta-
STUDY AIDSevidence-backed memory techniques
Explain & elaborate · explain why

Why does the grounding pipeline require an exact match of whole identifiers when checking code blocks against the source identifiers, instead of using a substring test?

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

Every guide on this site can be listened to, yet none of the audio is recorded by hand. The narration is synthesized from the same prose text that appears on the screen. The mechanism is a three‑stage pipeline in roadmap‑kg/kg/audio/pipeline.py, which wraps three independently runnable stages: tts, finalize, and stitch.

The pipeline runs from the command line with a slug (e.g. --slug how‑it‑works) and a mode — --per‑chapter or --per‑paragraph — plus an --upload flag. Stage one, tts.main, sends each chapter’s (or paragraph’s) text to a TTS provider, produces MP3 fragments, and optionally uploads them to R2. Stage two, finalize.main, reads those fragments and writes the page‑served data/<slug>.json — a single manifest that maps chapter titles to their audio URLs and seek offsets. Stage three, stitch.main, concatenates every chapter MP3 into one full‑length file on R2 (e.g. knowledge/case‑study‑audio/full.mp3), also producing a unified manifest with per‑chapter start times. The build script make stitch‑langgraph automates this for the LangGraph content, but the same pattern applies across guides.

The MP3s live on R2 rather than in the app bundle because the Cloudflare Worker runtime has no filesystem — anything a page needs must be compiled into the bundle, and audio files are too large. R2 is the same‑origin object store that serves the Worker itself, so streaming stays inside the same Cloudflare network. The player never fetches the files directly from the R2 public domain (tts.vadim.blog). Instead, app/api/audio/[...path]/route.ts runs on the same Worker and acts as a same‑origin proxy. It accepts a path like /api/audio/knowledge/case‑study‑audio/full.mp3, validates the key against the regex KEY_RE = /^knowledge\/…\.mp3$/ (to prevent open proxying), forwards the browser’s Range and If‑Range headers upstream, and relays the 206 Partial Content or 200 response — including Accept‑Ranges, Content‑Range, and Content‑Length. The <audio> element can seek freely because the browser receives proper byte ranges from the same origin.

The rewrite that makes this possible is toSameOriginAudioUrl in app/how‑it‑works/_shared.ts. It takes an R2 public URL (e.g. from the unified audio manifest), extracts the pathname, and prepends /api/audio. The function is used when building fullAudioMeta: the audio_url field is rewritten to the proxy URL, and each per‑chapter URL is cleared so the player uses a single <audio> element and seeks within the stream via chapters[].start_secs. Because the fetch goes to the Worker’s own origin, no CORS headers are needed, and the service worker — which has been intentionally neutered (public/sw.js registers no fetch handler and self‑unregisters) — cannot intercept or stall the stream. The audio plays progressively, byte by byte, just as the browser expects.

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("--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.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)")
        return 0

    print("\n── stage 3/3: stitch ──")
    return stitch.main(["--slug", args.slug])

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
  index: number;
  title: string;
  levels: Eli5Rung[];
}

interface ContentShape {
  eli5?: { chapters?: Record<string, { title?: string; levels?: { label?: string; body?: string }[] }> };
}
const _content = contentJson as ContentShape;

// The 12 ELI5 ladders, in chapter order. Each chapter carries its three rungs
// (Gist/More/Deep) as text. Tolerant of a partial sidecar.
STUDY AIDSevidence-backed memory techniques
Recall check

What text is the audio narration synthesized from?

Show answer

same prose text

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

A static site is immutable: new content means a build and deploy. This site sidesteps that limitation with a runtime data plane that reads fresh content from Cloudflare D1, a SQLite database that lives at the edge. When a lesson or audio guide is added or edited, the Rust sync-d1 pipeline writes an updated row into the content_cache table, and the next user refresh sees it without a redeploy.

The table has a minimal schema: a compound key of kind and slug together identify each row, and a payload column holds the exact JSON objects the frontend already consumes — ContentIndex, LessonFull, or AudioMeta. Reading is deliberately boring. Inside lib/content-d1.ts, the function getDoc<T>(kind, slug) constructs a SELECT payload FROM content_cache WHERE kind = ? AND slug = ? LIMIT 1, calls d1Query with D1QueryOptions that enable ISR caching (revalidate: 3600 and a tag like "lesson:my-slug"), parses the first row’s payload with JSON.parse, and hands the result to the existing shaper (e.g. lessonWithContentFromFull). If D1 is not configured or the row is missing, every reader returns null — the page then falls back gracefully to the bundled JSON from the build step.

The trade-off here is deliberate simplicity over flexibility. No ORM, no schema migrations in the app code, no secondary indexes — just a key-value‑ish cache that mirrors the static JSON shape. This keeps the read path extremely cheap and easy to reason about, and the single payload column means the data plane never needs to know the internal structure of every content type. The downside is that queries beyond a single-row lookup (e.g. “all lessons updated this week”) are impossible without adding another table or doing full scans, but the pipeline handles that offline.

The d1Query function itself, in lib/d1.ts, has two execution paths. In the Worker runtime, it grabs the native DB binding (declared in wrangler.jsonc as a d1_databases entry) via getCloudflareContext().env.DB. That path uses the Worker’s platform identity, needs no API token, and never triggers fetch — so it never forces a route to dynamic rendering. The fallback path (used during local dev or if the binding is unavailable) constructs an HTTP request to the Cloudflare API v4 endpoint, using environment variables CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_AUDIO_D1_ID, and CLOUDFLARE_D1. That path is explicitly a build/dev fallback and depends on a revocable token; the comment in d1.ts warns that it should be avoided in production.

The fail‑open design is central. Every D1 access is guarded by d1Configured(), which checks whether the native binding exists or the three environment variables are set. If D1 is unconfigured, d1Query throws an error — but the content‑reader functions in lib/content-d1.ts catch that error inside getDoc and return null. Similarly, if a SELECT returns zero rows, getDoc returns null. This means a page that calls getIndexFromD1 or getLessonBySlugFromD1 can immediately fall back to the static JSON imported at build time, and the site never hard‑depends on D1 being available.

Media assets (audio files, images) are not in D1; they live in R2, served from a public domain (tts.vadim.blog). Wrangler config maps the R2_BUCKET_NAME and R2_PUBLIC_DOMAIN, but media reading is handled separately, not through d1Query. The data plane covers only the structured content that changes between deploys.

The ISR caching layer (next: { revalidate: 3600, tags: [...] }) on the HTTP fallback path is a further optimisation: shared content reads are stored in the Next.js Data Cache for one hour, so they do not force every consuming route into fully dynamic rendering. The tags allow the pipeline to bust specific entries on demand via /api/revalidate. On the native binding path, ISR caching is not applied because the binding is already zero‑latency and does not participate in the fetch cache. Callers wrap the entire content‑read chain in React’s cache()getIndexFromD1 is cache(async () => ...) — which deduplicates concurrent calls for the same page render.

This runtime data plane is the mechanism that lets new lessons appear on refresh without redeployment, while preserving the static‑first character of the app. The fail‑open fallbacks mean an outage in D1 or a missing row never breaks the site — it merely shows the slightly stale build‑time content.

ELI5 — explain it simply
1
Gist

Lessons live in a small database right at the edge, so a typo fix shows up on refresh — no full redeploy. And if that database is ever down, you still get the last version that shipped, not a blank page.

2Morego a level deeper

Text and lesson data sit in Cloudflare D1 (a SQLite database close to the user); big files like audio sit in R2 object storage. The app reads one row by its kind and slug, and if the row is missing it quietly falls back to the built-in copy — so a broken database can't take the page down.

3Deepthe full mechanics

content_cache rows map (kind, slug) to a JSON payload. On the Worker the app uses the native D1 binding (platform identity, no token, and it doesn't force dynamic rendering); the HTTP-plus-token path is a build/dev fallback that opts into ISR (revalidate 3600 + tags) so reads don't poison static generation and the Rust sync-d1 step can revalidateTag after publishing. Every read degrades to null and falls back to bundled JSON or R2. The deploy smoke test hits a D1-backed [slug] lesson (/embeddings), not just the apex, to catch a broken data path.

Code references

Where this chapter's machinery lives in the repo:

wrangler.jsonc:24-31

The native D1 binding (DB) uses the Worker's platform identity, so runtime content reads need no API token — the same database referenced by the CLOUDFLARE_AUDIO_D1_ID var for the build/dev fallback.

json
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "knowledge-audio-progress",
      "database_id": "796da983-1708-437c-8740-69155da25e33",
      "remote": true
    }
  ],

lib/content-d1.ts:27-44

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

typescript
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 Rust `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/d1.ts:51-65

d1Query prefers the native binding path (no token, and it never forces a route to dynamic the way a no-store fetch would), falling back to the HTTP-plus-token REST API only off-Worker.

typescript
export async function d1Query<T = D1Row>(
  sql: string,
  params: (string | number | null)[] = [],
  opts?: D1QueryOptions,
): Promise<T[]> {
  // Native binding path (Worker runtime): no API token, no fetch — so it never
  // forces a route to dynamic the way a `no-store` fetch does. ISR caching from
  // `opts` does not apply here; callers wrap reads in React `cache()`.
  const binding = getD1Binding();
  if (binding) {
    const stmt = binding.prepare(sql);
    const bound = params.length ? stmt.bind(...params) : stmt;
    const { results } = await bound.all();
    return (results ?? []) as T[];
  }
STUDY AIDSevidence-backed memory techniques
Cloze

This site sidesteps that limitation with a runtime data plane that reads fresh content from  ____  and media assets from  ____ .

Show answer

Cloudflare D1, R2

09. The workers fleet and its cron triggers

The Next.js application ships as a single Cloudflare Worker, built by OpenNext into a bundle at .open-next/worker.js. That bundle handles all request routing, middleware, and API routes — but it does not export a scheduled handler for cron triggers. Cloudflare Workers receive cron invocations as a distinct scheduled event, not an HTTP request, so the worker entry point must define a scheduled method to process them. This is why a thin wrapper at workers/app/index.ts exists: it becomes the actual default export that Cloudflare runs. The wrapper’s fetch method is a one-line delegation to openNextHandler.fetch, preserving every Next.js behaviour unchanged. The only addition is a scheduled handler that translates cron events into synthetic HTTP requests to the Next.js app.

Inside the scheduled method, the code reads the cron expression from event.cron and maps it to a specific API route. Two cron schedules are declared in wrangler.jsonc under triggers.crons: "0 4 * * *" for a daily backup and "0 * * * *" for the hourly autonomous knowledge-graph tick. The wrapper constructs a Request targeting the app’s origin (from env.APP_ORIGIN or a hardcoded fallback), appending the corresponding path — /api/cron/backup or /api/cron/kg-tick — and signs it with a bearer token from env.CRON_SECRET. That synthetic request is then passed to openNextHandler.fetch, so the Next.js bundle handles the cron logic as if it were a normal HTTP call. This design means the Next.js codebase never needs to know about Cloudflare’s event system; all scheduling translation lives in the small wrapper.

A separate Python-based Cloudflare Worker at workers/edge-tasks handles auxiliary tasks: it includes a keepwarm cron (*/10 * * * *) that pings a Render-hosted LlamaIndex RAG endpoint to prevent cold starts, and a POST / endpoint that expands interview-prep content using DeepSeek through Cloudflare’s AI Gateway. That worker is configured in its own wrangler.toml with independent secrets and environment variables. The fleet therefore comprises two independent workers: the main Next.js worker (wrapped with the cron shim) and the edge-tasks worker. The separation keeps the core Next.js path simple and fast while offloading background tasks to a simpler, language-appropriate worker that can be deployed and scaled separately.

ELI5 — explain it simply
1
Gist

The site runs as small programs on Cloudflare's edge instead of one big server. A timer wakes them up — once a day to back up the database, once an hour for housekeeping, and every ten minutes to poke a sleepy helper service so it stays awake.

2Morego a level deeper

The main worker just forwards web requests to the Next.js app and adds a scheduled handler for timed jobs. A separate Python worker handles on-demand deepen-this-content calls and the keepwarm ping. The search backend runs somewhere else again (Render). Splitting them means one failing doesn't sink the rest.

3Deepthe full mechanics

workers/app/index.ts wraps the OpenNext bundle and adds a scheduled() handler that dispatches by cron string: 0 4 * * * to /api/cron/backup (D1 to R2), otherwise /api/cron/kg-tick (the hourly KG loop), both bearer-gated on CRON_SECRET. workers/edge-tasks is a python_workers Worker with a POST deepen endpoint (DeepSeek via the CF AI Gateway) and a */10 keepwarm cron against the Render RAG health URL. services/llamaindex is the FastAPI retrieval service on Render with its own autodeploy. Distinct deploy paths mean distinct failure domains.

Code references

Where this chapter's machinery lives in the repo:

workers/app/index.ts:33-49

The scheduled cron handler dispatches by expression — the daily 04:00 to /api/cron/backup and the hourly to /api/cron/kg-tick — forwarding CRON_SECRET as a bearer so the routes stay gated.

typescript
    event: unknown,
    env: unknown,
    ctx: WorkerExecutionContext,
  ): Promise<void> {
    const e = (env ?? {}) as WorkerEnv;
    const origin = e.APP_ORIGIN ?? "https://ai-engineer-roadmap.xyz";
    // Dispatch by cron expression (wrangler.jsonc `triggers.crons`):
    //   "0 4 * * *"  → daily D1→R2 backup
    //   anything else (the hourly "0 * * * *") → the autonomous KG loop tick.
    const cron = (event as { cron?: string } | null)?.cron ?? "";
    const path = cron === "0 4 * * *" ? "/api/cron/backup" : "/api/cron/kg-tick";
    const req = new Request(`${origin}${path}`, {
      headers: e.CRON_SECRET ? { authorization: `Bearer ${e.CRON_SECRET}` } : {},
    });
    await openNextHandler.fetch(req, env, ctx);
  },
};

wrangler.jsonc:74-77

The two cron schedules declared for the main Worker: the daily D1-to-R2 backup and the hourly autonomous KG loop tick.

json
    // NEON_AUTH_COOKIE_SECRET worker secret — `wrangler secret put …`, also in
    // scripts/sync-worker-creds.sh). Provisioned via `neonctl neon-auth enable
    // --project-id damp-scene-16205296`. lib/auth.ts resolves the session and
    // admits only OWNER_EMAIL; until the URL is real, owner routes fail closed.

workers/edge-tasks/wrangler.toml:17-24

The independent Python edge worker: a DeepSeek deepen endpoint plus a ten-minute keepwarm cron aimed at the Render RAG health URL, deployed out-of-band with its own config.

toml
name = "edge-tasks"
main = "src/entry.py"
compatibility_date = "2025-05-01"
compatibility_flags = ["python_workers"]

[triggers]

crons = ["*/10 * * * *"]
STUDY AIDSevidence-backed memory techniques
Quiz

What does the wrapper's `scheduled` handler do with cron events?

Options: translates cron events into synthetic HTTP requests to the Next.js app · directly calls internal Next.js functions without an HTTP request · sends the cron event as a raw Cloudflare event to the Next.js bundle · ignores the cron event and only handles fetch requests

Show answer

translates cron events into synthetic HTTP requests to the Next.js app

10. Evals and gates: keeping generated content honest

Machine-generated audio scripts cannot be trusted blindly. A TTS pipeline might produce a chapter with no narration, a title that runs fifty words, or a sentence so complex it is unreadable when spoken. Any of those would ship a broken guide to the user. The eval gates exist to catch these defects before publish, and the cheapest, most reliable gate is the mechanical audio gate—kg.audio_gate.

The gate runs over a single AudioMeta JSON (the structure that holds the full script, chapters, duration, and audio URL for a guide) and returns exit code 0 if the guide is ok, exit code 1 if it has hard failures. It is intentionally self-contained: it pulls in no LlamaIndex, no embedding models, no environment machinery. That means it can run on every guide during generation without adding latency or dependency risk. The entire gate lives in one file and costs a fraction of a second per guide.

Inside gate_audio(meta), the gate collects two lists: failures and warnings. Only failures flip the ok bit. The hard failures are crisp, deterministic rules that block publish when violated. The meta-level checks verify that the slug and title are non‑empty, that there are at least MIN_CHAPTERS chapters (currently three), that duration_secs is not zero, and that the TTS state is consistent (a pending-tts voice must have an empty audio URL). Per‑chapter hard checks include verifying the chapter index matches the array position, that chapter titles are non‑empty, no longer than MAX_TITLE_WORDS (five words) and MAX_TITLE_CHARS, and that titles are unique. The script itself is scanned by a series of _scan_* functions: prose rules reject backticks, code fences, pipe characters, bullet lists, headings, markdown links, visual references, and closing phrases; _scan_hostile_tokens catches common mispronunciations; _scan_pacing_rhythm enforces a maximum sentence length (hard failure when a sentence exceeds thirty words); _scan_audio_readability enforces a minimum Flesch Reading Ease score; _scan_small_digits_hard flags digits below ten that should be spelled out; _scan_sentence_integrity and _scan_sentence_nesting catch structural issues; and _scan_acronym_first_use ensures every acronym is expanded at its first appearance in the chapter.

Alongside these hard blocks, a set of WARN-only heuristics live in a separate path. They are too false‑positive‑prone to block publish—things like monotone sentence‑variety cadence, teens that should be spelled out, and seven TTS‑quality signals (prosodic‑break density, garden‑path sentences, cloze floor, homograph risk, filler density, pronoun ambiguity, working‑memory load). These warnings never appear in the failures list; they contribute to a quality score that can be reviewed offline.

Why a cheap deterministic gate? Because the generation loop runs it after every chapter rewrite, inside a retry loop, and any expensive or non‑deterministic check would make the loop slow or unstable. The gate’s hard rules are compile‑time constants shared with the Rust parity oracle (gate.rs) so there is no drift between the in‑loop gate and the standalone publish gate. By keeping the gate rule‑based, cheap, and stateless, the system catches the majority of shipping defects—mispronunciations, formatting leaks, missing chapters, unreadable prose—without ever needing to run a language model. The rare, fuzzy issues bubble up as warnings and are handled by human review.

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
def gate_audio(meta: dict) -> dict:
    """Gate one AudioMeta dict. ``ok == not failures``. Mirrors gate.rs::gate_audio."""
    failures: list[dict] = []
    warnings: list[dict] = []

    slug = meta.get("slug", "") or ""
    title = meta.get("title", "") or ""
    voice = meta.get("voice", "") or ""
    duration_secs = int(meta.get("duration_secs", 0) or 0)
    audio_url = meta.get("audio_url", "") or ""
    chapters = meta.get("chapters", []) or []
    full_script = meta.get("full_script", "") or ""

    if not slug.strip():
        _v(failures, None, "meta-slug", "empty slug")
    if not title.strip():
        _v(failures, None, "meta-title", "empty title")
    if len(chapters) < MIN_CHAPTERS:
        _v(failures, None, "min-chapters", f"{len(chapters)} chapters (min {MIN_CHAPTERS})")
    if duration_secs == 0:
        _v(failures, None, "zero-duration", "duration_secs is 0")
    pending = voice == "pending-tts"
    if pending != (audio_url == ""):
        _v(failures, None, "tts-state", f"voice={voice!r} but audio_url={audio_url!r}")
STUDY AIDSevidence-backed memory techniques
Explain & elaborate · explain why

Explain in your own words why the mechanical audio gate (kg.audio_gate) is designed as a cheap, deterministic, rule-based system instead of using expensive or non-deterministic checks, and how this design helps catch the majority of shipping defects before publish?

11. Memory science: the evidence base and the schedulers

The app exposes two distinct learning surfaces, both engineered for remembering what you read: /memorize (spaced‑repetition flashcards) and /loci (a memory‑palace trainer that walks cards along a route). They rest on the same FSRS‑6 core in lib/sm2.ts, but each has its own scheduler — only the FSRS‑6 model is shared, not the whole scheduling logic.

lib/sm2.ts exports fsrsUpdate, which takes a rating (FsrsRating: again, hard, good, easy) and a prior FsrsState (stability, difficulty, reps, lapses) and returns the updated state along with a suggested interval via intervalForRetention. This is the mathematical engine that estimates how long a memory will last. Both surfaces call it, but they wrap it in different layers to fit their interaction models.

/loci uses the scheduler in lib/loci-scheduler.ts, a set of pure functions — no React, no localStorage, no I/O. It adds two thin wrappers on top of lib/sm2.ts. First, an onboarding ladder: new cards graduate through fixed short intervals defined by the PHASES constant (["onboard1", "onboard2", "onboard3", "fsrs"]), with default durations like DEFAULT_ONBOARDING_MINUTES (10 minutes, 1 day, 3 days). Every grade still calls fsrsUpdate so the latent DSR state stays honest, but the ladder overrides the resulting interval while phase !== "fsrs". Once graduated, intervals come from intervalForRetention(stability). Second, an in‑session Leitner queue: buildSession orders due cards along the route; an again grade recycles the card to the back for another pass in the same sitting. The first grade a card receives in a session is persisted to FSRS — recycle passes are practice‑only, logged but never re‑persisted. That rule lives in gradeSessionCard. The data shape LociSchedState also includes a cueFp fingerprint: when the cue later differs (e.g., a new route), the schedule resets via reconcileCues — encoding specificity in action.

/memorize uses a different scheduler; the code for it isn’t shown in the provided context, but it also depends on fsrsUpdate from lib/sm2.ts. The design decision to keep separate schedulers means each surface optimises for its own cognitive‑load pattern. /memorize can be a straightforward implementation of FSRS‑6 with no extra wrappers. /loci needs the onboarding ladder (so users don’t drown in brand‑new cards) and the Leitner queue (to support walking the same route multiple times in a session). The shared core is the model itself, not the scheduling policy.

The learning‑science and principles pages under app/how‑it‑works/ document the evidence base each feature applies. The catalogue at app/how‑it‑works/learning-science/page.tsx is a fully static page built from data/learning-science.json and data/technique-recommendations.json at build time. It lists techniques (spaced repetition, retrieval practice, interleaving) sorted by paper count, each linked to DOI‑verified papers, with JSON‑LD structured data on a CollectionPage / Dataset. The principles deep‑dive at app/how‑it‑works/learning-science/principles/page.tsx is hand‑authored in ./content.tsx with typed Principle, Citation, and EvidenceItem interfaces — ReactNode paragraphs for inline emphasis and compile‑time checking of citation keys. It explains mechanism, boundary conditions, and implications for mnemonic‑generation systems. The /loci feature directly implements spacing (via FSRS intervals), retrieval practice (through graded recall), and encoding specificity (via cueFp). The learning‑science pages are the written justification for why these schedulers are built the way they are — grounded in peer‑reviewed evidence, not intuition.

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

// Memory-science index — a static, server-rendered catalogue of evidence-backed
// learning techniques (spaced repetition, retrieval practice, interleaving, …)
// and the peer-reviewed papers behind each, assembled by the roadmap-kg
// memory-techniques lane into data/learning-science.json at build time. Pure
// reading: no audio, no client JS, so it's a fully static server component.
// Chrome (Topbar/Footer/skip-link/<main>) is global in app/layout.tsx — this
// page only returns its own content.

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

// Memory-principles deep dive — the mechanisms companion to the
// /how-it-works/learning-science technique catalogue. Where the parent page catalogues
// evidence-backed techniques and their papers, this page explains the 13
// underlying principles: mechanism, key evidence, boundary conditions, and
// what each implies for a mnemonic-generation system. Hand-authored content
// lives in ./content.tsx; this file only renders. Pure reading — no client
// JS, fully static. Chrome (Topbar/Footer/<main>) is global in app/layout.tsx.

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: (
          <>
            Six weeks of loci training in novices durably improved recall{" "}
STUDY AIDSevidence-backed memory techniques
Recall check

What is shared between the two learning surfaces in the app?

Show answer

the FSRS‑6 model

12. The guide family and its navigation

The "How It Works" guide acts as the centerpiece of a small family of companion guides — the primers (First Principles, LlamaIndex, Memory Science), the pillar walkthroughs (Agentic RAG, Evaluation, Autonomy), and a standalone Glossary — that all share a single navigation shell. Every piece of that shell is driven by one plain JavaScript array in lib/case-study-guides.ts called CASE_STUDY_GUIDES. Each entry in the array is an object with properties like slug, basePath, title, short, icon, kind, single, and textAtBase. There is no second list of links, no manual href string scattered across components.

The GuideNav component in components/case-study-guide-nav.tsx consumes this array to produce three navigation elements: the cross-guide hub (a row of pills linking every guide), the per-guide Text/Audio tab bar, and the sequential Back/Next pager. The component receives only activeSlug and activeTab as props; it never needs to know how many guides exist or what their URLs should be. That knowledge lives entirely in the data array.

URLs for every link are derived from a guide’s basePath using a small helper function, also inside the component file, called guideHref. It checks the single and textAtBase flags to decide whether the default view lives at the raw basePath (for single‑page guides like the Glossary or for guides whose reading page predates an audio companion) or at ${basePath}/text. Because guideHref is used by the hub pills, the tab bar’s Text link, and the Back/Next pager, all three navigation elements automatically stay synchronized with any change to a guide’s basePath or flags. The tab bar itself is conditionally rendered — it is hidden for guides with single: true, because those guides have no Text/Audio split and their hub link already points to the single view.

The Back/Next pager relies on the order of CASE_STUDY_GUIDES itself. It finds the current guide’s index in the array, then picks the previous and next entries. This guarantees that the pager follows the same sequence as the hub pills, and that adding or removing a guide from the array updates the pager automatically.

The design trades a tiny duplication of field names (each guide object repeats the same shape) for a massive reduction in cross‑file coupling. There is no need to update a separate navigation config, no risk of a hub link pointing to one URL while the pager points to another. The array becomes a single point of truth that defines the order, the labels, the icons, and the URL construction rules for the entire guide family. Anyone adding a new companion guide can do so by inserting one row of data; the navigation shell, including the hub’s aria-current highlight and the pager’s sequential arrows, will adapt without touching any JSX.

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: "first-principles", basePath: "/how-it-works/first-principles", title: "First Principles", short: "First Principles", icon: "🧱", kind: "audio", textAtBase: true },
  { slug: "star", basePath: "/how-it-works/star", title: "STAR Stories", short: "STAR", icon: "⭐", kind: "text", single: true },
  { slug: "agents-workflows", basePath: "/how-it-works/agents-workflows", title: "Agents & Workflows", short: "Agents & Workflows", icon: "🧩", kind: "audio", textAtBase: true },
  { slug: "autonomy", basePath: "/how-it-works/autonomy", title: "Agent Autonomy", short: "Autonomy", 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: "principles", basePath: "/how-it-works/learning-science#principles", title: "Memory Principles", short: "Principles", icon: "🧠", kind: "audio", textAtBase: true },
  { slug: "glossary", basePath: "/how-it-works/glossary", title: "Glossary", short: "Glossary", icon: "📖", kind: "text", single: true },
];

export function getGuide(slug: string): CaseStudyGuide | undefined {
  return CASE_STUDY_GUIDES.find((g) => g.slug === slug);
}

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 guideHref function checks the  ____  and  ____  flags to decide whether the default view is at basePath or at basePath/text.

Show answer

single, textAtBase

13. The workflow layer: durable, streamable agent runs

The hand-rolled loops that originally grounded claims and judged flashcard relevance in a single while cycle are being systematically ported to event-driven workflows from the llama-index-workflows package. The migration costs exactly zero new dependency entries because the pinned llama-index-core already bundles llama-index-workflows as a standalone package, and llama_index.core.workflow is merely a one-line shim. Direct imports like from workflows import Context, Workflow, step survive any future core bump without a lockfile change.

In kg/claim_workflow.py, the old retrieve → draft → assess → revise loop is re-expressed not as procedural logic but as a graph of @step methods connected by typed events. ClaimStartEvent carries the technique slug; the retrieval step produces an EvidenceEvent with corpus evidence and vocabulary terms; the drafting step emits a DraftEvent holding the candidate claim; the oracle’s rejection sends a ReviseEvent carrying the missing terms back into the drafting step. A CiteEvent marks the end of the grounding loop, and a ProgressEvent streams every step’s action and observation for external consumers. Each edge is declared as a Python dataclass, so the workflow graph is explicit data, not implicit control flow. Loops (revision cycles) are handled by emitting ReviseEvent from the assessment step; the step machinery re-queues that event naturally, bounded by the max_revisions parameter.

Durability and resumability come from kg/workflow_store.py. This module maintains a SQLite database at .workflow-runs/runs.db. After every ProgressEvent, the running Context is serialized with JsonSerializer and upserted into the store via save_ctx. If a run crashes, a later invocation with --resume RUN_ID calls Context.from_dict on the stored snapshot. The restored context is already marked as running, so completed steps are not re-executed; only the in-flight events are re-queued. The store also tracks status (running/done) and final results, and is entirely local-first using only stdlib sqlite3 and json.

The FastAPI service in rag_service/main.py wires the workflow into its endpoints. Routes, defined in research-routes and routes, invoke run_claim_workflow (or its async wrapper arun_claim_workflow) with an on_progress callback. That callback pushes each ProgressEvent to the client, typically via Server-Sent Events or a WebSocket channel, so the user sees the agent thinking live. Because the workflow steps themselves are deterministic and the progress events carry the same shape as the reference loop’s in-memory trajectory, the streaming layer is a transparent addition—not a separate simulation.

What this buys over a hand-rolled loop is twofold. First, streaming makes the agent’s reasoning observable to the user without polling or storing the entire trajectory in a mutable variable. Second, durability turns a single-session operation into a recoverable, auditable run: a production crash during a long citation check no longer wastes the already-completed retrieval and drafting work. The typed event graph also makes the extension path obvious—adding an extra validation step requires only a new event class and a @step that receives and emits it, without touching the existing loop structure.

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
    def save_ctx(self, run_id: str, workflow: str, ctx_dict: dict[str, Any]) -> None:
        """Upsert the latest serialized ``Context`` for a running workflow."""
        now = time.time()
        with self._conn() as conn:
            conn.execute(
                "INSERT INTO runs (run_id, workflow, status, ctx_json, created_at, updated_at)"
                " VALUES (?, ?, 'running', ?, ?, ?)"
                " ON CONFLICT(run_id) DO UPDATE SET ctx_json=excluded.ctx_json,"
                " workflow=excluded.workflow, updated_at=excluded.updated_at",
                (run_id, workflow, json.dumps(ctx_dict, ensure_ascii=False), now, now),
            )

    def load_ctx(self, run_id: str) -> dict[str, Any]:
        """Return the latest context snapshot for ``run_id`` (KeyError if unknown)."""
        with self._conn() as conn:
            row = conn.execute(
                "SELECT ctx_json FROM runs WHERE run_id = ?", (run_id,)
            ).fetchone()
        if row is None or row[0] is None:
            raise KeyError(f"no persisted context for run '{run_id}'")
        return json.loads(row[0])

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
app.include_router(research_router)

# Opt-in generic workflow REST surface (run-llama WorkflowServer, a Starlette sub-app)
# under /workflows-api — env-gated so prod deploys are byte-identical without the flag.
if os.environ.get("ENABLE_WORKFLOW_SERVER") == "1":
    from .workflow_server import build_workflow_server
STUDY AIDSevidence-backed memory techniques
Spaced review

Come back in a few days and quiz yourself: how do typed events like EvidenceEvent and ReviseEvent connect the @step methods in the claim workflow? How does the SQLite store at .workflow-runs/runs.db make runs durable and resumable with --resume? And how does the on_progress callback push ProgressEvent to show the agent thinking live?

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 Worker runtime has no filesystem, so anything a page needs must be compiled into the bundle — 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), and cf:deploy is a chain — tests → next build --webpack → OpenNext bundle → wrangler deployscripts/verify-deploy.sh smoke. A broken manifest cannot reach the bundler, and a broken deploy fails the verify step loudly.

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. 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.

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 at the edge. Narration MP3s live on R2, but the player streams them through the Worker'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. Cloudflare Workers shape the odd-looking choices: the build must use webpack (next build --webpack — turbopack output doesn't survive OpenNext), the Worker entry (workers/app/index.ts) wraps the OpenNext bundle to add cron scheduled handlers, and local dev binds the LIVE remote D1 ("remote": true in wrangler.jsonc) so dev and prod read the same data — 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 means retrieval succeeds only when the cue was encoded with the trace. This study site applies that principle by using the same local model to encode both every stored chunk and every incoming question. That guarantees the retrieval cue lives in exactly the representational space the material was encoded into, so the cue can land near the right trace and trigger memory.

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 explains that effortful processing builds storage strength, making long-term retention superior to easy, shallow learning. The verification gate enforces this: each code excerpt must pass a real-source check, with failures fed back and retried. This added effort slows production but builds durable knowledge because the difficulty is systematically overcome, not frustrating—turning mere generation into reliable mastery.

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 shows that distributed practice beats massed practice; expanding schedules (1, 3, 10, 30 days) are what SuperMemo/Anki-style algorithms implement. This site’s review queue implements FSRS-6, which fits individual forgetting curves per item per user, directly 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)


Retrieval practice (the testing effect) strengthens memory because effortful retrieval builds multiple retrieval routes, unlike passive re-exposure. The flashcard feature applies this by forcing the learner to pull each answer from memory before seeing it—a self-test that feels harder than rereading but produces stronger, longer-lasting retention, as evidence shows retrieval practice outperforms restudy even when students believe restudy is more effective.

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 explains that self-produced material is remembered better than received material. An AI pipeline that generates each page section from retrieved source rather than copying it applies this principle: by producing its own content, the system retains information more effectively, mirroring the ML analog where a model's self-generated reasoning becomes its training signal.

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 into a few separately navigable chapters applies this principle by recoding the whole subject into a handful of grouped units, fitting within working memory’s limited span. This is Chunking.

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 shared mechanism is that these principles are not arbitrary study tricks but general constraints on any system storing and retrieving under interference. Encoding principles determine a trace’s initial strength; retrieval practice governs how that strength grows; spacing controls whether growth compounds or decays; and encoding specificity is the invariant that keeps the whole loop connected. A study site built on all of them outperforms any single feature because ignoring practice or spacing leaves month-three retention weak, while the specificity principle ensures cues match the encoding context, reinforcing the entire cycle.

Explore the memory principles →