Knowledge Graphs for AI Agents
Track 5 β€” Production

M11 Β· Production Operations: Drift, Staleness & Observability

Module 12 of 14
⏱ Estimated time: 60 minutes πŸ“‹ Prerequisites: M08 (enrichment hooks), M09 (MCP serving), M10 (the assembled stack) 🎚 Level: Advanced

Learning Objectives

SKILL LEVEL: ADVANCED

  • Explain why a stale structural graph is worse than no graph, and why the dangerous failure mode is "wrong with zero signal," not "wrong once."
  • Recount the four documented mechanisms by which a rebuild hook fails silently β€” and identify which ones your own setup is exposed to.
  • Describe artifact desync among graph.json, graph.html, and GRAPH_REPORT.md, and the scale wall that makes "just rebuild" a non-answer on monorepos.
  • Implement the five-point production checklist, including a CI freshness gate that mechanically compares artifact timestamps against git log -1.
  • Answer the defining question β€” "how would you detect a stale graph within 24 hours?" β€” in specific, mechanical terms.

Graphs Don't Self-Heal

Open with the scenario this entire module defends against. Your coding agent confidently refactors a payments module using a knowledge graph that is three weeks out of date β€” and the system never warned anyone. Every update hook reported success. Every dashboard was green. The agent's mental model quietly diverged from reality, and you found out when the refactor shipped.

The root cause is a single sentence you first met in M09's cache trap, now generalized: Graphify builds a static index. It does not update automatically as developers commit code. Without hooks explicitly wired in β€” and working correctly β€” the graph silently drifts from the codebase, and nothing in the tool's happy-path output tells you it's happening.

This is not hypothetical. In one reported case, GRAPH_SUMMARY.md β€” the ~700-token "cheap orientation" file handed to agents before a task β€” was measured two weeks stale on origin/main: last updated May 9, checked May 23. For that entire window it was served to agents as ground truth. As one GitHub issue discussion put it: "An agent reasoning over a stale bundle gives confidently wrong answers. If you refactor a module and forget to rerun /graphify, your agent is navigating with a stale map."

BEFORE: Think of a city's paper street map versus its GPS feed. A paper map printed in May is honest about what it is β€” you know it can't show last week's road closure, so you double-check when something looks off.

PAIN: Now imagine a GPS that shows a live-updating interface, timestamps, a cheerful "route recalculated" β€” but silently stopped receiving updates two weeks ago. You extend it full trust because everything about it signals freshness. You drive into the closed road at speed.

MAPPING: A graph behind an MCP server is the fake-live GPS. The serving layer answers instantly and authoritatively whether the graph is an hour old or a month old β€” nothing in the answer's shape reveals its age. That's why a stale graph is worse than no graph: no graph sends the agent to read files (slow, but current); a stale graph delivers high-confidence wrong answers with no signal to distrust them.

Graph drift is the accumulating divergence between a codebase's actual state and the state its knowledge artifacts describe. Some lag is inherent β€” graphs are always somewhat behind between rebuilds, and that's tolerable. The failure mode is not "the graph is wrong once." The failure mode is the graph being wrong with zero signal that it's wrong: graphify update reports success, the hook ran, nothing crashed β€” and the map is still lying.

Animation Β· DRIFT_TIMELINE β€” the gap nobody sees
code (git HEAD)
graph.json
built: May 9 βœ“
πŸ€– "verify_token has one caller β€” safe to change." (from the May 9 graph)
Press play: watch two weeks pass without a rebuild.
⚠️ Common Misconceptions

"We installed the hooks, so we're covered." β€” "We installed the hooks" and "we have a production-grade setup" are different claims. The next section documents four ways installed hooks fail while reporting success.

"A slightly stale graph is slightly wrong." β€” Staleness isn't linear. One renamed function can invert a blast-radius answer completely; the graph doesn't degrade gracefully, it lies precisely.

"OKF's freshness story is proven; only Graphify drifts." β€” OKF was six weeks old at spec level when these reports were written. Its freshness and synchronization promises are explicitly unproven at organizational scale β€” not dishonesty, just youth. Plan for the uncertainty.

"The hook ran and reported success" should comfort you. It's about to stop doing that.

Anatomy of a Silent Failure: Four Documented Mechanisms

Everything in this section comes from real issue-tracker reports on the tools you've been using β€” it reads like a postmortem because it effectively is one. Four independent mechanisms, each capable of stopping rebuilds while producing no visible error.

Animation Β· SILENT_FAILURES β€” green lights over red states

nohup missing
Windows hook
rebuild never ran

allowlist drift
CODE_EXTS vs detect.py
commit ignored

resource gate
CPU>50% β†’ skip
stale, unbounded

MCP cache
loaded at startup
fresh disk, stale memory
Press play: each door shows a green lamp β€” and the truth underneath it.

1 β€” Windows hooks fail silently: the missing nohup

The post-commit and post-checkout hooks that trigger rebuilds use nohup to detach the rebuild process. Git for Windows' shell doesn't have nohup. The rebuild simply doesn't happen β€” and no error surfaces to the developer, because the hook framework doesn't treat a missing binary as a failure worth reporting loudly. Every Windows developer on the team commits daily, believes the graph updates, and is wrong. (This is why the M08 lab's hook deliberately avoids nohup and backgrounding entirely: loud and synchronous beats quiet and detached.)

2 β€” The extension allowlist drifted from the source of truth

The hook decides whether a commit warrants a rebuild by checking changed files against a hardcoded CODE_EXTS extension list. That list drifted out of sync with the authoritative list in graphify/detect.py. Result: commits touching valid, parseable code files silently skip triggering a rebuild, because the hook's gate logic doesn't recognize the extension. Note the shape of this bug β€” two copies of one fact, maintained separately, diverging. You'll see it again in the artifact-desync section, and it's the same disease OKF's "path is the identity" rule (M06) was designed to prevent.

3 β€” Resource gating turns "busy" into "stale, indefinitely"

Rebuilds are gated on a resource check before firing:

the gate, as reported (Python)
def _resources_ok():
    cpu_ok = psutil.cpu_percent(interval=0.1) <= 50 * cpu_count_fraction
    mem_ok = psutil.virtual_memory().available >= 2 * 1024**3   # 2GB
    return cpu_ok and mem_ok

if not _resources_ok():
    # rebuild is silently skipped; next commit will retry
    return

Each line is reasonable in isolation β€” you don't want a graph rebuild fighting your CI job for CPU. But the skip is silent, and the recovery strategy is "the next commit will retrigger it." On a quiet branch, or a repo where commits are infrequent, that next commit might be days away. The graph doesn't just lag; it lags for an unbounded, unmonitored duration. A gate without a catch-up path converts a temporary condition (busy machine) into a permanent one (stale graph).

4 β€” Caching outlives regeneration

M09's trap, now in its production context: MCP server integrations cache graph.json at startup with no hot-reload. Even a perfectly successful graphify update may never reach the agent actually querying the graph until the server process restarts. Fresh graph on disk, stale graph in the agent's working memory, simultaneously β€” and no artifact anywhere records that the two have diverged.

πŸ”₯ Why this list matters

Individually, each is a normal, fixable bug. Collectively, they describe a system whose entire value proposition β€” "the graph is fresh, so the agent's answers are trustworthy" β€” has multiple silent single points of failure, none of which announce themselves. Your defense cannot be "fix these four bugs"; new ones will ship. Your defense is refusing to trust freshness you haven't measured β€” which is the checklist below.

Artifact Desync: When the Three Outputs Disagree

Recall from M04 that one Graphify run produces three artifacts: graph.json (machine-readable), graph.html (visual), and GRAPH_REPORT.md (narrative). Three outputs from one run β€” which means three things that can stop agreeing.

If graphify update refuses to overwrite an existing graph.json under certain conditions, the three can desync: the human-readable report telling one story, the machine-readable graph another, with no built-in cross-check to flag the mismatch. A reviewer skims GRAPH_REPORT.md and sees the new module documented; the agent queries graph.json and doesn't know it exists. Both are "the graph," and they disagree.

There's an operational cousin to this problem: if graphify-out/ is tracked in git, every regeneration dirties the working tree, which blocks clean CI/publish workflows. Teams that don't catch it early end up with commit-hygiene problems layered on top of freshness problems. The fix is one line in .gitignore β€” but it must be a decision, not an accident, because ignoring the artifacts also means your CI must rebuild them rather than trusting committed copies.

βœ… Why It Matters

Desync converts your redundancy into a liability. Three artifacts were supposed to serve three audiences (machines, eyes, reviewers); unsynchronized, they serve three different versions of the truth. The freshness gate you'll build below cross-checks them against each other precisely because nothing else does.

The Scale Wall: When Regeneration Itself Becomes the Bottleneck

Assume every hook fires correctly, every time. Regeneration still isn't free β€” and at scale it isn't even bounded.

The worst offender is centrality. Graphify's suggest_questions() calls betweenness centralityA graph metric scoring how often a node sits on shortest paths between other nodes β€” the god-node detector from M01. Computing it exactly requires touching a large share of all vertex-edge combinations., which is O(VΓ—E). On a monorepo measured at 450,000 nodes and 690,000 edges, that's roughly 310 billion operations β€” and it's a single-threaded graph algorithm, not an embarrassingly parallel one, so more cores barely help. In practice: a full rebuild on a 96-core Xeon was killed after 114 minutes without completing, while the same class of rebuild took roughly 10 minutes on an Apple M3 Pro on a different repo. Read those two data points together: cost here is non-linear in repo shape and nearly unpredictable from hardware specs alone.

"So use incremental updates" β€” yes, and calibrate your expectations. One reported case measured ~0.8 seconds for a small incremental diff; another measured 10+ seconds for incremental rebuilds against a full real monorepo. "Incremental" means "smaller than a full rebuild" β€” a much weaker guarantee than "fast." If your mental model says incremental β‰ˆ instant, a 10-second post-commit hook will surprise you exactly where it hurts: in every developer's commit loop.

πŸ’° The Honesty Rule, applied

This is also where the ~500-file payoff floor from M04 gets its production teeth. Below the floor, you pay tooling tax for savings that don't exist. Above it, you pay a different tax β€” regeneration time, gating complexity, desync monitoring β€” that grows with VΓ—E. The honest question is never "should everyone run a graph?" It's "at my repo's size and churn, does the measured savings exceed the measured maintenance?" Both sides of that inequality are measurable. Measure them.

Silent failures, desync, unbounded rebuilds. Put the pieces together and the conclusion writes itself: the mechanism meant to keep your graph fresh can fail without telling you β€” and even when it works, it may not finish in useful time. That's not a reason to abandon the stack. It's the reason production-grade means something specific.

What Production-Grade Actually Requires

Five requirements. Each one exists because of a specific failure you've now seen. Treat graph freshness as an observable property, not an assumption β€” that's the headline over all five.

  1. Log rebuild success/failure explicitly, with timestamps, somewhere you'll actually see it. Never trust hook exit codes alone β€” doors 1–3 all exit clean. A one-line append to a rebuild log turns "did it run?" from a belief into a query.
  2. Cross-check the three artifacts against each other AND against the latest commit. graph.json, GRAPH_REPORT.md, and graph.html should agree on when they were generated, and that timestamp should be recent relative to git log -1. A one-line CI comparison catches the two-week-stale scenario before an agent does.
  3. Restart or hot-reload MCP servers after regeneration. Don't assume a running process picks up a freshly written graph.json β€” verify it, or bounce it (door 4).
  4. Gate rebuilds on resources only WITH an explicit retry/alert path. A silent no-op under load is fine as a first line of defense; it is not fine as the only line. Pair the gate with a scheduled catch-up job, plus an alert when staleness exceeds a threshold you've consciously chosen (door 3's fix).
  5. Verify you're above the ~500-file payoff floor at all. Below it, the pipeline is ceremony, not engineering. Measure your own repo before adopting wholesale.
Animation Β· FRESHNESS_GATE β€” the check that refuses to trust
git log -1
HEAD: 14:02
graph.json
mtime: 14:03
🚦 freshness
gate
βœ… pipeline continues
agents may trust the graph
β›” exit 1 β€” BLOCKED
"predates HEAD by 1 commit β€” rebuild + restart MCP"
Press play: one fresh pass, then a stale commit hits the gate.

Code Walkthrough: Build the Freshness Gate

Checklist items 2 and 4 are one small script. Let's build it in three chunks β€” this is the same gate you'll run in the lab, and the one the capstone wires into CI.

Chunk 1 β€” compare the graph's clock against git's

What: read graph.json's mtime and HEAD's commit time; fresh means the graph was generated at or after the last commit. Why: this single comparison mechanically detects every one of the four silent failures β€” however the rebuild died, the symptom is identical: the artifact predates HEAD. Gotcha: compare against commit time, not wall-clock age. A repo untouched for a month with a month-old graph is perfectly fresh.

freshness_gate.py β€” the core comparison
import subprocess, sys
from pathlib import Path

def sh(args, cwd):
    r = subprocess.run(args, cwd=cwd, capture_output=True, text=True)
    if r.returncode != 0:
        raise RuntimeError(f"{' '.join(args)}: {r.stderr.strip()}")
    return r.stdout.strip()

def check_fresh(repo: Path, graph: Path, max_age_commits: int = 0) -> int:
    if not graph.exists():
        print(f"STALE: {graph} does not exist β€” run the extractor")
        return 1
    graph_mtime = int(graph.stat().st_mtime)
    head_time   = int(sh(["git", "log", "-1", "--format=%ct"], cwd=repo))
    if graph_mtime >= head_time:
        print("FRESH: graph generated at/after HEAD commit")
        return 0
    lag = int(sh(["git", "rev-list", "--count",
                  f"--since={graph_mtime}", "HEAD"], cwd=repo))
    if lag > max_age_commits:
        print(f"STALE: graph predates HEAD by {lag} commit(s)")
        print("  fix: rerun the extractor, then RESTART any MCP server "
              "still holding the old graph in memory")
        return 1
    print(f"FRESH ENOUGH: lags by {lag}, within threshold {max_age_commits}")
    return 0
freshness_gate.mjs β€” the core comparison
import { execFileSync } from "node:child_process";
import { statSync, existsSync } from "node:fs";

const sh = (args, cwd) =>
  execFileSync(args[0], args.slice(1), { cwd, encoding: "utf-8" }).trim();

function checkFresh(repo, graph, maxAgeCommits = 0) {
  if (!existsSync(graph)) {
    console.log(`STALE: ${graph} does not exist β€” run the extractor`);
    return 1;
  }
  const graphMtime = Math.floor(statSync(graph).mtimeMs / 1000);
  const headTime = parseInt(sh(["git", "log", "-1", "--format=%ct"], repo));
  if (graphMtime >= headTime) {
    console.log("FRESH: graph generated at/after HEAD commit");
    return 0;
  }
  const lag = parseInt(sh(["git", "rev-list", "--count",
                           `--since=${graphMtime}`, "HEAD"], repo));
  if (lag > maxAgeCommits) {
    console.log(`STALE: graph predates HEAD by ${lag} commit(s)`);
    console.log("  fix: rerun the extractor, then RESTART any MCP server " +
                "still holding the old graph in memory");
    return 1;
  }
  console.log(`FRESH ENOUGH: lags by ${lag}, within threshold ${maxAgeCommits}`);
  return 0;
}

Chunk 2 β€” cross-check the artifacts

What: re-extract into a throwaway file and compare node/edge counts against the stored graph. Why: a timestamp can be fresh while the content is wrong (a partial rebuild, a refused overwrite) β€” the count comparison catches desync that clocks can't. Gotcha: clean up the throwaway file in a finally; a leftover .freshcheck.json becomes tomorrow's confusing fourth artifact.

freshness_gate.py β€” cross-check (Python; Node mirrors it)
import json

def cross_check(repo: Path, graph: Path, extractor: Path) -> int:
    stored = json.loads(graph.read_text(encoding="utf-8"))
    fresh_out = graph.with_suffix(".freshcheck.json")
    try:
        sh([sys.executable, str(extractor), str(repo),
            "--out", str(fresh_out)], cwd=repo)
        fresh = json.loads(fresh_out.read_text(encoding="utf-8"))
    finally:
        fresh_out.unlink(missing_ok=True)      # never leave a 4th artifact
    if (len(stored["nodes"]), len(stored["edges"])) != \
       (len(fresh["nodes"]),  len(fresh["edges"])):
        print(f"DESYNC: stored {len(stored['nodes'])}n/{len(stored['edges'])}e "
              f"vs fresh {len(fresh['nodes'])}n/{len(fresh['edges'])}e")
        return 1
    print("CROSS-CHECK OK: stored counts match a fresh extraction")
    return 0

Chunk 3 β€” wire it into CI, before anything trusts the graph

What: run the gate as a pipeline step ahead of any agent-consuming job. Why: placement is the point β€” the gate is worthless after an agent has already acted on a stale graph. Gotcha: a failing gate must block (exit nonzero), not warn. Warning-only freshness checks are ignored by their second week.

ci.yml β€” the gate as a hard gate
steps:
  - name: Graph freshness gate
    run: python ci/freshness_gate.py . --max-age-commits 0 --cross-check
  - name: OKF bundle lint          # trust gates travel in pairs (next section)
    run: python ci/validate.py knowledge
  - name: Agent-consuming jobs
    run: ...                        # only reachable if both gates passed
πŸ”Ž What Just Happened?

Roughly sixty lines convert freshness from a belief into a measurement: clock vs clock catches every silent hook death, count vs count catches desync, and CI placement guarantees no agent consumes what the gate hasn't blessed. Notice what the gate does not do β€” it doesn't fix anything. It refuses to proceed and names the fix. Detection and repair stay separate, so a broken repair path can't silence detection.

Walk it, step by step

The code changes, the graph does not, and an agent answers confidently from a map that is now a lie. Watch the gate catch it β€” and watch the earlier step where nothing catches it at all.

OKF-Side Governance: Lint as the Trust Gate

The structural graph isn't the only artifact that rots. Recall OKF's deliberate leniency from M06: consumers must tolerate missing optional fields, unknown types, and broken links. That leniency is why adoption is easy β€” and it means a bundle can silently degrade into unlinked, out-of-date files without triggering any error anywhere. Drifted types ("API Endpoint" vs "Endpoint" vs "Route"), dangling cross-references, concepts nobody re-linked after a refactor: all of it is spec-conformant.

That's why lint holds the same position for the bundle that the freshness gate holds for the graph: run okf lint (13 built-in spec-compliance rules) as a hard CI gate before agents are allowed to act on the bundle. Given how lenient the spec itself is, lint is the only thing standing between "technically conformant" and "actually useful." The M08 pipeline already ends every enrichment pass with lint; CI re-running it is the backstop for edits that bypassed the pipeline.

The Defining Question

Everything in this module compresses into one test. Before you adopt Graphify+OKF β€” or before you audit a setup you already have β€” write down, concretely, how you would detect a stale graph within 24 hours.

Acceptable answers are mechanical: "CI runs the freshness gate on every push and blocks on lag > 0"; "a nightly job compares artifact timestamps to HEAD and pages past a 6-hour threshold"; "the MCP server exposes a status tool and the orchestrator refuses graphs older than the last commit." Unacceptable answers are beliefs: "the hooks handle it," "we'd notice," "it hasn't happened yet."

If you can't answer in specific, mechanical terms, you don't have a production-grade setup. You have a demo that hasn't failed yet. The real production question was never "is it 71Γ— or 7Γ—" β€” that's a number you quote in a meeting. The question that determines whether your setup survives contact with a real codebase is whether you'll know the graph went stale within a day, or find out in a month, the hard way, after an agent has shipped a confidently wrong refactor.

Hands-On Exercise

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

What you'll build: the freshness gate, run green, then deliberately broken and caught. Time: 30–45 min. Lab folder: labs/M11-freshness-gate/. Prerequisite: the M08 lab (a git repo whose graph updates on commit).

Step 1 β€” Run the gate on a fresh repo

cd labs/sample-project
python ../M11-freshness-gate/solution/freshness_gate.py . --max-age-commits 0

Expected: FRESH: graph.json generated at/after HEAD commit, exit 0.

βœ… Checkpoint

If it reports STALE immediately, your M08 hook isn't firing β€” which is itself the lesson: you just detected a silent failure mechanically.

Step 2 β€” Make it stale on purpose

git commit --allow-empty -qm "commit without rebuilding the graph"
python ../M11-freshness-gate/solution/freshness_gate.py . --max-age-commits 0

Expected: STALE: graph.json predates HEAD by 1 commit(s) + the fix command + exit 1. This is the step most "production" setups have never actually run.

Step 3 β€” Cross-check the artifacts

Re-run with --cross-check: the gate verifies stored node/edge counts against a fresh extraction, catching content desync that timestamps miss.

Step 4 β€” Wire it into CI

- run: python labs/M11-freshness-gate/solution/freshness_gate.py . --max-age-commits 0 --cross-check

Place it before any agent-consuming step. Troubleshooting: gate errors with a git failure β†’ you're not inside the repo; graph.json does not exist β†’ run the extractor once first.

Stretch goals: add a staleness-age alert (page past N hours, not just N commits); add an MCP status tool to your M09 server and make the gate query it, closing the fresh-disk/stale-memory gap.

πŸ” Context Engineering Lens

Graph drift is context rot at the infrastructure altitude. M03B's definition β€” "degradation caused by accumulated stale, contradictory, or low-relevance content… the key signal is signal-to-noise ratio, not raw size; you can hit it at 60% window utilization" β€” describes a rotting transcript; this module describes the same disease in the map the transcript relies on. Compaction cures the first; the freshness gate is compaction for the second. Full mapping in M02B.

Knowledge Check

1. Why is a stale graph worse than no graph at all?

Stale graphs cost more tokens to query
No graph forces slow-but-current file reads; a stale graph delivers high-confidence wrong answers with no distrust signal
Stale graphs are expensive to rebuild
It isn't β€” some graph always beats no graph

2. A Windows developer commits daily; the team's rebuild hook uses nohup. What happens?

Rebuilds never run on that machine, with no visible error
The hook errors loudly on every commit
Git falls back to running the rebuild in the foreground
Windows doesn't execute git hooks at all

3. What makes the psutil resource gate dangerous, given each check is individually reasonable?

The CPU threshold is set too low
The skip is silent and the retry depends on a future commit that may be days away β€” unbounded, unmonitored staleness
psutil measures CPU incorrectly
Rebuilds should never be skipped for any reason

4. Your freshness gate compares graph.json's mtime to git log -1 --format=%ct. Why commit time instead of wall-clock age?

Freshness is relative to the code's last change, not to the calendar
Commit time is easier to obtain
File mtimes can't be trusted at all
To avoid timezone bugs

5. Why does the gate ALSO re-extract and compare node/edge counts (--cross-check)?

It's faster than comparing timestamps
A timestamp can be fresh while the content is wrong; counts catch desync that clocks can't
To verify graph.html renders correctly
To validate EXTRACTED vs INFERRED tags

6. "How would you detect a stale graph within 24 hours?" Which answer passes the module's test?

"Our hooks handle it automatically"
"The team would notice wrong answers quickly"
"A nightly CI job compares artifact timestamps to HEAD and pages us past a 6-hour lag threshold"
"We use a strong enough model that it compensates"

Module Summary

The core insight

Graphs don't self-heal. The dangerous failure isn't "wrong once" β€” it's wrong with zero signal: hooks report success while the map lies (two weeks stale, in production, documented).

Four silent failures

Missing nohup on Windows; CODE_EXTS allowlist drift; silent resource-gate skips with unbounded retry; MCP startup caches. Plus artifact desync among the three outputs.

The scale wall

Betweenness centrality is O(VΓ—E): 450KΓ—690K β‰ˆ 310B ops; a 96-core Xeon killed at 114 min vs ~10 min on an M3 Pro. Incremental means smaller, not fast (0.8s vs 10+s). gitignore graphify-out/.

The defense

Freshness as an observable: log rebuilds, cross-check artifacts vs HEAD in CI, restart MCP servers after regen, gate resources only with catch-up + alerts, verify the ~500-file floor. Lint is the bundle's twin gate.

What we built on orderflow: a CI freshness gate that passes green, catches a deliberate no-rebuild commit with exit 1 and an actionable fix, and cross-checks artifact content β€” the mechanical answer to the 24-hour question.

Next module preview: the machinery is production-grade; the last lever is human. M12 covers structuring projects so agents don't get lost in the first place β€” layered CLAUDE.md files, skills, subagents, deny rules β€” and where this 90-day-old ecosystem goes next.

References

  • "Production-Grade OKF + Graphify Setup, Not Just a Demo" β€” the drift postmortem and checklist (Data Science Collective, Aug 2026)
  • Graphify issue tracker reports (silent hook failures, scale measurements)
  • okf CLI β€” okf lint, 13 spec-compliance rules (github.com/superops-team/okf)
  • Course labs: labs/M11-freshness-gate/, labs/M08-enrichment-hook/
  • M01 (centrality), M04 (payoff floor), M09 (cache trap) β€” the concepts this module operationalizes