Turning a customer's docs into an llms family

Problem

A documentation site is the wrong shape for an agent. It is hundreds or thousands of HTML pages, each wrapped in navigation, each linking to the others, none of them saying which page answers a given question. An agent pointed at it either crawls everything (expensive and slow) or guesses a page from the URL (usually wrong). What it needs is a family of files: a small index that says where things are, a full file it can grep, a budgeted file it can load whole, and a facts file it can retrieve from — every line pointing back at a page and a heading.

Four public docsets were run through the hub’s export in the last two days of August 2026. They were chosen because they are large, they publish their own llms.txt or llms-full.txt (so the acquisition ladder’s cheapest rung applies), and they differ in shape: Cloudflare is a product tree, PayPal is an API reference, Claude’s platform docs are a mix, LangChain is small. This post is the numbers, the commands, and what the lint said.

Inputs

DocsetPagesAcquired viaDeterministic units
developers.cloudflare.com 1,943llms-full.txt (57 MB upstream, 2,000-page cap) 25,142
developer.paypal.com 1,507structured crawl (its llms-full.txt redirects to a 1.5 KB llms.txt) 38,710
docs.claude.com (served from platform.claude.com) 666llms.txt + page .md twins 13,432
docs.langchain.com 529llms-full.txt 12,933

“Deterministic units” are the snippets, table rows (parameters), definitions and changelog entries that docset_refine extract pulls out without a model. No LLM pass ran on these four; this is the zero-token layer only.

Two of the four needed a detour. PayPal’s llms-full.txt is a redirect to its short index, so the probe (which now requires real Source:-delimited pages) fell through to a crawl, and its pages have no .md twins. Claude’s llms.txt at docs.claude.com lists pages hosted on platform.claude.com, so the docset key changed mid-run and the stale four-page docset had to be deleted afterwards.

Commands

# cwd: ~/.global-ai-hub
# 1. Which rung of the acquisition ladder does each host support?
.venv/bin/python scripts/docset_rollout.py probe

# 2. Queue the four hosts and run mirror → refine → index on this box only
.venv/bin/python scripts/pipeline_manager.py add \
  https://developers.cloudflare.com https://developer.paypal.com \
  https://docs.claude.com https://docs.langchain.com
.venv/bin/python scripts/pipeline_manager.py run --local-only --crawlers 2 --max-pages 2000

# 3. (What the refine stage runs per docset, if you want it by hand — no model tokens)
PYTHONPATH=scripts .venv/bin/python -m docset_refine all --no-units \
  ~/.claude/skills/web-text-mirror/text-mirror/developers.cloudflare.com.md

# 4. Lint the export directory against its mirror
.venv/bin/python scripts/llms_lint.py check \
  ~/.claude/skills/web-text-mirror/text-mirror/developers.cloudflare.com.llms/ \
  --mirror ~/.claude/skills/web-text-mirror/text-mirror/developers.cloudflare.com.md

--local-only matters: the remote boxes in the pool do not have llms_acquire.py, so a placement there would fall back to a trafilatura crawl and reconstruct, badly, a file the site hands out for free.

Outputs

Every docset produced <stem>.llms/{llms.txt, llms-full.txt, llms-small.txt, llms-facts.txt, manifest.json} plus one <section>/llms.txt per section once the root index crossed the 10 KB split threshold.

DocsetRoot index (bytes)Spoke indexesFull (tokens)Facts (tokens)
developers.cloudflare.com 9,241 243 4,162,267 1,889,300
developer.paypal.com 4,104 193 2,921,259 1,680,485
docs.claude.com 1,977 73 7,493,540 768,209
docs.langchain.com 1,508 15 1,552,458 738,488

The small file is the same size everywhere by construction: build_small fills an exact 200,000-character budget (about 50k tokens, the ceiling at which editor agents stay stable) and asserts on it. The root index is under 10 KB on all four because the sections were pushed out into spokes; the spokes together are the real index — for Cloudflare,

243 files totalling about 587 KB, which is the

honest size of a 1,943-page table of contents with a description per page.

Every unit in the facts file is one line, - [type] text — url#anchor, and every anchor resolves to a heading in the mirror (see the anchors post for why that was not true a day earlier).

What the lint found

Before the split landed, all four docsets carried a High: S1 (index over 100 KB — an index that is itself a site dump). After build_split_index, the estate gate reported:

Facts files pass P7 (every line typed from the twelve allowed types, every line sourced) and R3 (anchors resolve against the mirror) on all four.

Lessons

Reproduce

The exports live in this repository under outputs/exports/<stem>.llms/; each manifest.json carries the byte and token counts quoted above (the blog’s figures are regenerated from them at build time by site/tools/gen_figures.py). To rebuild from scratch, run the commands block on a hub checkout, then llms_lint.py check <stem>.llms/ --mirror <stem>.md — it exits 1 while a High remains. Recipe 02 in the examples cookbook walks a split root by hand.