KNOWLEDGE GRAPHS FOR AI AGENTS
Track 3 Β· Narrative Knowledge

M08 β€” The Enrichment Pipeline: Self-Updating Graphs

⏱ ~60 min πŸ“‹ Prerequisites: M06–M07 Intermediate
Module 9 of 14

Learning Objectives

  • Explain why bundle maintenance β€” not the format β€” is the real engineering work of adopting OKF.
  • Describe Google's two-pass enrichment pattern (draft, then cite) and what breaks when the citation pass is skipped.
  • Assemble the six-stage pipeline: diff-scoped scan β†’ draft/update β†’ re-link β†’ lint β†’ publish, triggered by a git hook.
  • Argue, with numbers, why diff-scoping is the difference between a pipeline that runs on every commit and one that gets disabled.
  • Implement a working, loudly-failing enrichment hook for orderflow.

The Gap the Spec Deliberately Leaves

Here is the central claim of this module, and arguably of the whole Track: adopting OKF is trivial β€” one required field, a folder convention, plain Markdown links. The real engineering work is the enrichment pipeline that keeps the graph accurate and current as the code changes. The spec has no opinion on how a bundle gets generated or stays true. That is left entirely β€” and intentionally β€” to whoever adopts it.

Why does this gap matter so much? Because a knowledge graph that lags reality doesn't degrade gracefully β€” it lies confidently. An agent reading a bundle that says billing-service has two dependencies, when a third was added last sprint, will happily plan a refactor that breaks the third. Stale context isn't just slow; it's actively wrong, and wrong with the full authority of "documented knowledge." (Module 11 is an entire postmortem on how this happens silently even when you think you've automated it.)

The reason to be optimistic anyway comes from the LLM Wiki insight the whole ecosystem is built on: "LLMs don't get bored, don't forget to update a cross-reference, and can touch 15 files in one pass. The bookkeeping that causes humans to abandon personal wikis is exactly what LLMs are good at." Humans let documentation rot because upkeep is tedious. The enrichment pipeline's bet is that tedium is precisely the workload to delegate to a background agent β€” with mechanical guardrails around it.

BEFORE: Think of a city map printed on paper. The day it's printed, it's perfect. The city, meanwhile, keeps building: a new bridge, a closed street, a renamed square.

PAIN: Nobody re-surveys the whole city every morning β€” that costs too much, so the map just… ages. Six months later, drivers confidently turn onto a street that no longer exists. The map didn't get vaguer; it stayed crisp and became wrong.

MAPPING: The enrichment pipeline is the city's survey crew β€” but a smart one. It doesn't re-survey the whole city; it watches the construction permits (the git diff) and re-draws only the blocks that changed, every single day. Cheap enough to run constantly, which is the only schedule that keeps a map honest.

The Two-Pass Agent: Draft, Then Cite

Google's own reference implementation β€” the BigQuery metadata agent β€” established the pattern everyone now copies. It is a two-pass agent:

Pass 1 β€” Draft: walk every changed asset and draft (or update) one concept file per asset, derived from its actual schema, interfaces, and structure. Pass 2 β€” Cite: cross-reference each drafted concept against existing documentation β€” runbooks, ADRsArchitecture Decision Records: short documents capturing a significant design decision, its context, and its consequences. orderflow's docs/adr-001-event-bus.md is one., PR descriptions β€” and link them in as citations.

Translated to code, pass 1 walks every changed module/service/API in the repo and drafts a concept from its interfaces and call graph; pass 2 adds citations back to the runbooks, ADRs, and PRs that explain why. The passes have different jobs and different failure modes. Pass 1 without pass 2 produces text that is plausible-sounding but unverified β€” the LLM describes what the code seems to do, with no anchor to human-recorded intent. That's precisely the "confident but wrong" content you built this system to eliminate. Keep the two-pass structure; a single-pass pipeline is a rumor mill with good formatting.

TWO_PASS β€” draft, then anchor to human sources
Static view: pass 1 drafts the concept skeleton (Responsibilities and Dependencies derived from code structure) with an empty Citations section. Pass 2 draws citation threads from the concept to the runbook, the ADR, and the PR that explain the WHY β€” anchoring generated text to human-recorded intent.

The Pipeline, Stage by Stage

Wrap the two-pass agent in triggering, scoping, and gating, and you get the pipeline every serious deployment converges on:

the enrichment pipeline
commit pushed
      |
      v
diff-scoped module scan        (which services/files actually changed?)
      |
      v
draft / update concept docs    (two-pass enrichment agent)
      |
      v
re-link cross-references       (update Dependencies AND Dependents, both sides)
      |
      v
lint                           (spec-compliance rules β€” a HARD gate)
      |
      v
publish                        (commit the bundle / push through CI / register to a catalog)
PIPELINE_FLOW β€” one commit travels the pipeline
Static view: a commit flows through six stages β€” diff scan, draft/update, re-link, lint, publish. The diff filter shrinks 12 changed files to the 1 affected concept. In the failure run, lint blocks the pipeline before publish and reports loudly, so a malformed bundle never ships.

Two stages deserve emphasis beyond the diagram. Re-link runs on both sides: when billing-service gains a dependency on fraud-check, billing's # Dependencies gains a link and fraud-check's # Dependents (if it exists) gains the back-link. One-directional updates are how the M07 warning β€” bundles silently rotting β€” actually happens in practice. Lint is a hard gate, not a report: if the enrichment agent produced structurally invalid output, the pipeline must refuse to publish. A lint that only warns is decoration; the whole trust model of the bundle rests on "what's published passed the gate."

The pipeline also maintains log.md as it goes β€” every refresh appends a dated entry recording what the bundle learned. That's how the M06 distinction (knowledge history vs file history) gets kept true mechanically rather than by heroic discipline.

Diff-Scoping: The Entire Cost Model in One Decision

If you remember one sentence from this module: scope every enrichment pass to the git diff, not the whole repository. The entire cost model of self-updating knowledge hangs on this choice.

Run the numbers on a mid-size repo. A full-repo enrichment pass over 400 services means 400 concept drafts through an LLM β€” call it hundreds of thousands of tokens β€” per commit, at forty commits a day. That pipeline gets disabled by Friday of its first expensive week, and the bundle starts rotting the moment it's off. A diff-scoped pass on a typical commit touches one or two services: two drafts, two citation lookups, a lint run. That's cheap enough to run on every push, which is the only cadence that keeps the graph honest. As the field puts it: a team that re-scans everything on every commit will find the pipeline too expensive to keep running well before their repo gets large enough to actually need it.

πŸ’° Why It Matters β€” the arithmetic

400-service repo, 40 commits/day. Full-repo pass: 400 drafts Γ— ~1.5K tokens β‰ˆ 600K tokens per commit β†’ 24M tokens/day. Diff-scoped: ~2 drafts Γ— ~1.5K β‰ˆ 3K tokens per commit β†’ 120K tokens/day β€” a 200x difference, for identical freshness on the concepts that actually changed. One is a rounding error; the other is a budget line that gets your pipeline killed.

⚠️ Common Misconceptions

"Nightly full rebuilds are simpler and good enough." β€” Simpler, yes. But a nightly rebuild means up to 24 hours of confident staleness, and β€” worse β€” it trains you to tolerate lag. The commit-triggered, diff-scoped design keeps staleness bounded by minutes and cost bounded by the diff.

"The LLM drafts everything, so the pipeline is only as reliable as the LLM." β€” The pipeline's trust comes from its mechanical stages: diff detection, both-sides re-linking, lint gating are all deterministic code. The LLM only writes prose inside a scaffold those stages verify. (And in this course's labs, pass 1 is fully deterministic β€” built from the structural graph, no LLM at all.)

"Once the hook is installed, we're done." β€” Installing the hook is where M11 begins. Hooks fail silently in at least four documented ways. A pipeline without freshness observability is a demo that hasn't failed yet.

End-to-End Trace: One Commit, Both Sides, Next Day

Walk the whole thing with concrete files. A developer adds a fraud-check call to orderflow's billing webhooks:

  1. A developer pushes a change to services/billing/webhooks.py, adding a dependency on a new fraud-check service.
  2. The git hook fires and triggers a diff-scoped scan β€” only billing-service and anything directly touched by the diff gets re-examined. Not the whole repository.
  3. Pass 1 drafts the update: reading the service's new interface and call sites, it rewrites the # Dependencies section of billing-service.md to include fraud-check.
  4. Pass 2 adds citations: it cross-references the change against the PR description explaining why the dependency was added, and links it in.
  5. Cross-references update on both sides β€” billing-service.md now links to the fraud-check concept, and fraud-check.md's # Dependents section gains the back-link.
  6. Lint runs against the updated bundle, catching anything the enrichment pass got structurally wrong.
  7. The bundle publishes β€” committed alongside the code change, or pushed through CI to a served location.
  8. The next day, an orchestrator picks up an unrelated task touching fraud-check. It reads index.md, loads just that one concept β€” including its now-current link back to billing-service β€” and never re-scans either codebase to understand the relationship.
BOTH_SIDES β€” one new dependency, two updated files
Static view: the new dependency updates BOTH concept files β€” billing-service.md's # Dependencies gains "calls fraud-check" and fraud-check.md's # Dependents gains "billing-service" β€” connected by a link edge, with a dated log.md entry recording what the bundle learned.

Walk it, step by step

One commit, walked through the hook end to end. Watch for the failure mode this module keeps warning about: the hook that runs, exits zero, and updates nothing.

Bridging Structural β†’ Narrative: graphify export

You don't have to draft pass-1 content from thin air. On July 1, 2026 β€” three weeks after the OKF spec shipped β€” a bidirectional Graphify↔OKF integration toolkit landed, exporting Graphify's structural graph straight into an OKF-compliant markdown catalog:

terminal
graphify export --format okf --out docs/knowledge/

The pitch is elegant: Graphify gives you precise "what calls what" (deterministic, zero-LLM, from Track 2); OKF gives you the portable, agent-friendly "why this exists." Structural graph plus narrative catalog, one pipeline. Bundles conventionally live at docs/knowledge/ or .well-known/okf/ in the repo. This is exactly how the labs' enrichment hook works: the structural graph supplies the Dependencies facts mechanically, and the LLM (when used at all) only writes the connective prose β€” the division of labor that keeps hallucination out of the load-bearing parts.

When β€” and When Not β€” to Build This

The honest decision framework, because pipelines have carrying costs:

Build it if you already run multiple agents against your repo or knowledge base. The pipeline pays for itself directly: fewer tokens re-deriving context per task, fewer stale-context bugs from agents acting on outdated mental models. The minimal viable version fits in an afternoon: okf init, a git hook, one enrichment pass over your most-changed service β€” not the whole repo β€” and index.md as the entry point every agent reads first. Measure the token delta on your next multi-agent task, and decide from data.

Skip it β€” for now β€” if you're not running agentic workflows against your code. OKF's entire value proposition is agent consumption. Without agents consuming it, "you are maintaining a wiki nobody reads." That's not a mild inefficiency; it's the exact failure mode (docs rot because upkeep has no consumer) this whole system was invented to escape.

πŸ’° About that "95% token reduction" number

Early analyst commentary claims token-consumption reductions of up to roughly 95% versus naive document loading, for focused and stable knowledge domains. Treat this figure the way this course treats every headline number: it is anecdotal and unvalidated at production scale. It plausibly indicates the direction of savings for pre-compiled context; it is not a planning input. Your measured delta on your repo is.

Code Walkthrough: A Working, Loudly-Failing Hook

Let's build the orderflow enrichment hook β€” the same one shipped in labs/M08-enrichment-hook/solution/. It is deliberately LLM-free: pass 1 derives Dependencies from the structural graph, which makes the lab runnable offline and demonstrates that the pipeline's skeleton is deterministic engineering, not prompt magic.

Chunk 1 β€” find what changed. WHAT: ask git for the files in the last commit and map them to owning services. WHY: this is the diff-scope filter β€” everything downstream only runs for touched services. GOTCHA: normalize path separators; on Windows, git can hand you backslashes that break prefix matching.

refresh_concepts.py β€” chunk 1
import json, subprocess, sys
from datetime import datetime, timezone
from pathlib import Path

SERVICE_MAP = {                                # changed-path prefix -> (concept, module prefix)
    "services/billing": ("services/billing-service.md", "services.billing"),
    "services/orders": ("services/orders-service.md", "services.orders"),
    "shared": ("services/shared-libraries.md", "shared"),
}

def changed_files(repo: Path) -> list[str]:
    result = subprocess.run(["git", "diff", "--name-only", "HEAD~1", "HEAD"],
                            cwd=repo, capture_output=True, text=True)
    if result.returncode != 0:                # first commit, detached head, etc. β€” fail LOUDLY
        raise RuntimeError(f"git diff failed: {result.stderr.strip()}")
    return [ln.strip().replace("\\", "/") for ln in result.stdout.splitlines() if ln.strip()]
refresh_concepts.mjs β€” chunk 1
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";

const SERVICE_MAP = {                          // changed-path prefix -> [concept, module prefix]
  "services/billing": ["services/billing-service.md", "services.billing"],
  "services/orders": ["services/orders-service.md", "services.orders"],
  "shared": ["services/shared-libraries.md", "shared"],
};

function changedFiles(repo) {
  try {
    return execFileSync("git", ["diff", "--name-only", "HEAD~1", "HEAD"], { cwd: repo, encoding: "utf8" })
      .split("\n").map((l) => l.trim().replaceAll("\\", "/")).filter(Boolean);
  } catch (exc) {                              // fail LOUDLY β€” a silent [] here is graph drift
    throw new Error(`git diff failed: ${exc.message}`);
  }
}

Chunk 2 β€” regenerate the affected concept's Dependencies from the structural graph. WHAT: rebuild graph.json (the Track 2 extractor), then collect the touched service's outbound cross-service edges. WHY: this is pass 1 done deterministically β€” the facts come from the AST, so this section cannot hallucinate. GOTCHA: preserve the concept's frontmatter and prose; replace only the generated section, or the hook will eat human-written content.

refresh_concepts.py β€” chunks 2–3
def service_dependencies(graph: dict, module_prefix: str) -> list[str]:
    deps = set()
    for edge in graph["edges"]:
        src = edge["source"].split(":", 1)[1]
        dst = edge["target"].split(":", 1)[1]
        if src.startswith(module_prefix) and not dst.startswith(module_prefix):
            deps.add(f"- calls `{dst}` ({edge['provenance']})")   # provenance travels with the fact
    return sorted(deps)

def refresh_concept(bundle: Path, concept_rel: str, deps: list[str]) -> None:
    path = bundle / concept_rel
    path.parent.mkdir(parents=True, exist_ok=True)
    if path.exists():                          # surgical: replace ONLY the generated section
        head, sep, _tail = path.read_text(encoding="utf-8").partition("## Dependencies")
        base = head if sep else path.read_text(encoding="utf-8")
    else:
        name = Path(concept_rel).stem
        base = f"---\ntype: Service\ntitle: {name}\n---\n\n# {name}\n\n"
    block = "## Dependencies\n" + ("\n".join(deps) or "- (none detected)") + "\n"
    path.write_text(base.rstrip() + "\n\n" + block, encoding="utf-8")
refresh_concepts.mjs β€” chunks 2–3
function serviceDependencies(graph, modulePrefix) {
  const deps = new Set();
  for (const edge of graph.edges) {
    const src = edge.source.split(":")[1], dst = edge.target.split(":")[1];
    if (src.startsWith(modulePrefix) && !dst.startsWith(modulePrefix))
      deps.add(`- calls \`${dst}\` (${edge.provenance})`);  // provenance travels with the fact
  }
  return [...deps].sort();
}

function refreshConcept(bundle, conceptRel, deps) {
  const file = path.join(bundle, conceptRel);
  fs.mkdirSync(path.dirname(file), { recursive: true });
  let base;
  if (fs.existsSync(file)) {                   // surgical: replace ONLY the generated section
    const text = fs.readFileSync(file, "utf8");
    const idx = text.indexOf("## Dependencies");
    base = idx >= 0 ? text.slice(0, idx) : text;
  } else {
    const name = path.basename(conceptRel, ".md");
    base = `---\ntype: Service\ntitle: ${name}\n---\n\n# ${name}\n\n`;
  }
  const block = "## Dependencies\n" + (deps.join("\n") || "- (none detected)") + "\n";
  fs.writeFileSync(file, base.trimEnd() + "\n\n" + block, "utf8");
}

Chunk 3 β€” log, lint, and refuse to publish on failure. WHAT: append the dated log.md entry, run the M06/M07 lint, and exit non-zero if it fails. WHY: the log keeps knowledge history true mechanically; the lint gate is what makes the published bundle trustworthy. GOTCHA: the hook itself must not swallow this exit code β€” no nohup, no backgrounding, no || true. Every one of those turns a failed refresh into silent drift (M11 documents each, from real issue trackers).

.git/hooks/post-commit
#!/bin/sh
# orderflow enrichment hook β€” diff-scoped, loud on failure.
# No nohup, no backgrounding, no silent skips (see M11 for the postmortems).
set -e
REPO_ROOT="$(git rev-parse --show-toplevel)"
python3 "$REPO_ROOT/tools/refresh_concepts.py" "$REPO_ROOT" || {
    echo "[enrich] FAILED β€” bundle NOT updated. Fix before trusting the graph." >&2
    exit 1
}
βœ… What Just Happened?

You assembled the whole pipeline: git supplies the diff scope, the structural graph supplies the facts (with provenance tags riding along), surgical file edits preserve human prose, log.md records what the bundle learned, and lint gates publication. The LLM's only remaining job β€” optional β€” is pass-2 prose and citations. That's the correct division of labor: deterministic machinery for trust, generation for language.

Hands-On Exercise: Install and Break the Hook

πŸ“‚ Get the files: labs/M08-enrichment-hook on GitHub β€” or clone the whole course once: git clone https://github.com/varasrinivas/knowledge-graph-course.git

What you'll build: the full hook from labs/M08-enrichment-hook/, installed into a git-initialized copy of orderflow β€” then broken on purpose, twice. Time: 45–60 min. Prerequisites: M06 and M07 labs.

  1. Step 1 β€” Make sample-project a git repo.
    terminal
    cd labs/sample-project
    git init -q && git add -A && git commit -qm "baseline"
  2. Step 2 β€” Study, then install the hook. Read solution/refresh_concepts.py β€” the SERVICE_MAP table is the part you'd adapt to any real repo. Then copy solution/post-commit into .git/hooks/ (chmod +x on macOS/Linux).
  3. Step 3 β€” Trigger it. Add a comment to services/billing/webhooks.py, commit, and watch the enrichment log lines appear.
    βœ… Checkpoint

    You see: changed-file detection β†’ Dependencies refreshed with an edge count β†’ log.md updated β†’ lint result. If the hook didn't fire: exact filename post-commit, executable bit, correct hooks directory.

  4. Step 4 β€” Break it (lint gate). Delete type: from a concept, commit any code change. The hook must FAIL loudly, naming the file, exit non-zero.
    βœ… Checkpoint

    If it passed, your lint gate is decorative β€” exactly the failure M11 is about. Fix the concept and re-commit.

  5. Step 5 β€” Reflect. In one paragraph: which pipeline stages in your hook are deterministic, which would involve an LLM in a production pass 2, and why that split is what makes the bundle trustworthy.

Knowledge Check

1. What does the OKF spec say about how bundles get generated and stay current?

It mandates a git post-commit hook
It requires the Knowledge Catalog service to sync them
Nothing β€” generation and maintenance are left entirely to the adopter
It requires a full re-scan on every release tag

2. Why does the two-pass pattern insist on a separate citation pass?

To reduce token costs by batching lookups
A draft-only pipeline produces plausible-sounding but unverified descriptions β€” citations anchor generated text to human-recorded intent
The spec requires a citations field in frontmatter
Pass 2 is where lint runs

3. A team runs full-repo enrichment on every commit of their 400-service monorepo. What happens, per the field's experience?

The bundle becomes maximally fresh and stays that way
The pipeline becomes too expensive and gets disabled β€” before the repo is even big enough to need it β€” and then the bundle rots
Git rejects the hook for exceeding runtime limits
Lint failures increase linearly with repo size

4. In the fraud-check trace, why must re-linking update BOTH billing-service.md and fraud-check.md?

The spec rejects one-directional links
One-directional links are how bundles silently rot β€” an agent reading only fraud-check.md would never learn billing depends on it
Because lint rule 13 requires symmetric links
To keep the two files byte-identical

5. What does `graphify export --format okf` bridge?

OKF bundles into vector embeddings
The structural layer into the narrative layer β€” the AST graph exported as an OKF markdown catalog
Markdown concepts into graph.html visualizations
git history into log.md

6. Your team runs zero agentic workflows but wants to adopt the enrichment pipeline "to be ready." What does this module advise?

Build it now β€” earlier is always cheaper
Build only the lint stage now
Wait β€” without agents consuming the bundle, you're maintaining a wiki nobody reads
Adopt RAG first, then OKF becomes unnecessary

Module Summary

The gapThe spec defines the format; keeping a thousand-file bundle accurate at forty commits/day is YOUR system. That pipeline is the real adoption cost.
Two passesDraft from structure, then cite from runbooks/ADRs/PRs. Draft-only = plausible but unverified. Never ship pass 1 alone.
Six stagescommit β†’ diff-scoped scan β†’ draft/update β†’ re-link (BOTH sides) β†’ lint (hard gate) β†’ publish. log.md appends what the bundle learned.
Diff-scope or die~200x cost difference vs full-repo passes. Expensive pipelines get disabled; disabled pipelines mean confident staleness.
The bridgegraphify export --format okf: verified AST facts feed the narrative catalog. Deterministic machinery for trust; LLM only for prose.
Adoption gateNo consuming agents β†’ no pipeline. Start with your most-changed service, measure the token delta, decide from data. ("95%" claims are anecdotal β€” your number isn't.)

What we built: orderflow's self-updating machinery β€” a post-commit hook that diff-scopes, refreshes Dependencies from the structural graph with provenance intact, logs, lints, and fails loudly.

Next module preview: the bundle exists and updates itself. Now it has to reach an agent efficiently. M09 moves to Track 4: serving graphs over MCP β€” one tool call instead of N file reads, and the honest arithmetic of what that saves.

References

  • Google Cloud: How the Open Knowledge Format can improve data sharing (McVeety & Hormati, 2026)
  • OKF Specification v0.1 β€” github.com/GoogleCloudPlatform/knowledge-catalog
  • okf CLI β€” github.com/superops-team/okf Β· Kiso β€” github.com/oak-invest/kiso
  • Course corpus: "Standardizing Agent Memory" (the pipeline blueprint); "Open Knowledge Format: A Complete Guide" (end-to-end walkthrough, pitfalls); "Production-Grade OKF + Graphify Setup" (what M11 inherits)