Sovereign AI Ecosystem

'Compile-Time AI in Practice: How We Built a Kubernetes Knowledge Compiler' [post] deterministic

"How we built k8s-docs-compiler — a Kubernetes knowledge compiler that applies compile-time AI: intelligence moved to the build step, shipped as a static, queryable, versioned knowledge graph with zero runtime inference.

compile-time-aiknowledge-compilerkuberneteslocal-first-aiknowledge-graphnextjssovereign-aiknowledge_systemsovereigntycompile_time_ai

Compile-Time AI in Practice: How We Built a Kubernetes Knowledge Compiler

*Published 2026-07-18 · Daniel Kliewer*

The smartest thing you can do with a model is to stop asking it questions at runtime.

That sentence is the whole thesis behind [k8s-docs-compiler](https://github.com/kliewerdaniel/k8s-docs-compiler) — a project that turns the entire Kubernetes documentation into a static, queryable, *readable* knowledge graph and ships it to [k8s-docs-compiler.vercel.app](https://k8s-docs-compiler.vercel.app/) with **zero inference at runtime**.

This post is a walkthrough of exactly how we built it, and why every design decision traces back to one idea: **intelligence belongs at compile time, not query time.**

> Full source: https://github.com/kliewerdaniel/k8s-docs-compiler > Live demo: https://k8s-docs-compiler.vercel.app/

---

The problem with "ask the docs a question"

Most "AI for docs" products are chatbots. You type a question, a model reads the docs, a model generates an answer, a model decides what to cite. The model is **in the loop every single request**. That means:

- Every answer costs a round-trip to a model (latency + tokens + money). - Every answer is non-deterministic — the same question can yield a different answer tomorrow. - Every answer is hard to audit — you're trusting a black box that may or may not have read the right page. - The docs themselves never actually become *more useful*. The model is a flashlight pointed at a messy room; the room stays messy.

Compile-time AI flips the order. Instead of querying the model when someone asks, we query the model **once, while building the artifact**. The model's intelligence gets *baked into* the knowledge base. What we ship is a static graph of facts — each one traceable to a source document — that any client can read with a SQL query or a JSON fetch. No model is awake when a user visits.

> "If the loop is the product, observability becomes the OS."

That's the stance this project operationalizes.

---

The architecture: a 5-phase compiler

We modeled the build like a real compiler. Sources go in one end; deterministic, versioned, inspectable artifacts come out the other.

SOURCES PHASE 1 PHASE 2 PHASE 3 PHASE 4 PHASE 5 kubernetes/website ─▶ INGEST ─▶ PARSE / IR ─▶ KNOWLEDGE PASSES ─▶ OPTIMIZE ─▶ ARTIFACTS (content/en/docs + fetch & front-matter, glossary edges, dedupe, JSON / SQLite / swagger.json) normalize, shortcodes, API objects, RBAC, compress, GEXF / index provenance concepts, ownership, control- drop orphans api paths plane, kubectl flow │ OPTIONAL AI PASS (off by default): summaries · prerequisites · clusters

Every phase is pure Python. The output is a typed **Intermediate Representation** (IR) — Node and Edge dataclasses — that the rest of the pipeline consumes. Crucially, **the deterministic core never needs a model**. The AI pass is a *separate, opt-in layer* bolted onto Phase 3.

What the deterministic build actually extracts

From 1,632 Kubernetes docs (after excluding contribute/), 163 glossary terms, and the OpenAPI swagger.json (628 API objects / 564 paths):

| Metric | Value | |--------|-------| | Build time | ~9 s (single deterministic pass) | | Nodes | 7,021 (1,411 pages, 4,800 concepts, 628 api_objects, 162 glossary, 15 roles, 5 controllers) | | Edges | 10,228 (part_of, related_to, references, api_for, RBAC REQUIRES, control-plane, kubectl-flow) | | Validation | clean — no dangling edges, confidence in range |

Edges are where the value lives. The compiler doesn't just index pages — it reconstructs the *relationships* between them:

- **glossary edges** — a page's Hugo glossary_tooltip shortcodes become machine edges (e.g. a Deployment page → Pod, Service, Ingress). - **API objects** — parsed from swagger.json, cross-linked to docs by group/kind. - **RBAC REQUIRES** — derived from manifest verbs and the control-plane schema. - **control-plane + kubectl flow** — the "what actually happens when you run kubectl apply" path, as a walkable graph. - **ownership / part_of** — concept→page→api-object hierarchy.

Every node and edge carries provenance — the source document, line, and the supporting quote. Every deterministic fact is confidence=1.0.

---

The AI pass: intelligence, baked in

The deterministic graph is a *scaffold*. It knows that "Pod" relates to "Deployment," but the Deployment page itself might only have a title and a one-line summary pulled from front-matter. For the resource to be genuinely *readable* — the thing you'd actually send someone instead of a docs link — it needs synthesized prose.

That's the one job we hand to a model, and we hand it **at compile time**.

The method

We point the compiler at a **local inference endpoint** and let it synthesize a structured knowledge card for every node that lacks a real body. The default is Ollama on localhost:11434 running llama3.1:8b — chosen because it returns clean JSON reliably. (We also probed a 35B thinking model on :8080; it emits empty content and only answers inside reasoning_content, so we made the client endpoint-agnostic and fell back to the 8B for content generation.)

bash python -m compiler.cli compile \ --docs-root kubernetes/website/content/en/docs \ --swagger swagger.json --version v1.34 --out out \ --ai --ai-passes synthesis,prerequisites,clusters \ --ai-url http://localhost:11434 --ai-model llama3.1:8b

For each node, the pass sends **only the extracted source quotes** (not the whole internet) and asks for a structured card:

> overview · why-it-matters · key facts · pitfalls · related

The card is stored as the node body (rendered in the frontend **Docs** view) plus a one-line summary. Three design rules make this *compile-time AI* rather than *chatbot AI*:

1. **Grounded, not generative.** The model explains quotes that already exist in the corpus. It does not invent Kubernetes behavior. Output is tagged derived_by="ai:llama3.1:8b" with confidence < 1.0 and stays pinned to its source provenance. 2. **Batched + cached by content hash.** Nodes are synthesized N-at-a-time per model call. Results are cached by a hash of their source quotes, so a crash or a re-run costs nothing — only *new or changed* nodes hit the model. This is what turned a fragile 35-minute run into something resumable. 3. **Pluggable.** synthesis, prerequisites, clusters are separate passes in a registry. You add or remove LLM calls per use case without touching the deterministic core. --ai-passes synthesis runs one; --ai runs all.

The result on the real corpus: **1,210 AI-synthesized nodes** with readable documentation, plus **382 prerequisite_of edges** (AI-suggested learning paths) and **cluster tags** across 400+ nodes for topic-filtered browsing.

Why this is the point

The model ran *once*, for ~7 minutes, on a laptop. It will never run again for the life of the deployment. A visitor to the site gets a synthesized, citation-backed explanation of any Kubernetes concept — and the bill for that explanation was paid at build time.

---

The product: a backend-free static site

The compiler emits dataset.json (the full IR), knowledge.db (SQLite), knowledge.gexf (graph), and index.json (search). The frontend is a **Next.js static export** that consumes dataset.json and renders:

- **Graph / Explore** — walk the knowledge graph. - **API Explorer** — 628 API objects + 564 API paths. - **Relationships / RBAC** — ownership chains and "what permissions does this manifest require?" - **Learn / Start Here** — prerequisite-ordered onboarding paths. - **Docs** — the synthesized know

Sources

DanielKliewer.com blog · source

Related (3)

discusses Compile-Time AI conf=0.9
discusses Knowledge Systems conf=0.7
discusses Local-First / Sovereignty conf=0.5

← all Blog