"""Compile chapter-owned learner graphs into maps and curriculum catalogs.
Book records live in chapter *_concepts.yml files. The independent field model
and book-link overlay live in taxonomy. Published JSON, DOT, SVG, chapter
viewers, and the catalog are deterministic build artifacts.
"""
from __future__ import annotations
import hashlib
import html
import json
from pathlib import Path
import re
import shutil
import subprocess
import textwrap
import xml.etree.ElementTree as ET
from urllib.parse import quote as urlquote
import yaml
# The ML Systems Taxonomy page: its source folder and its public address.
SITE_URL = "https://mlsysbook.ai"
SITE_DIR = "taxonomy"
PAGE_URL = f"{SITE_URL}/{SITE_DIR}/"
class ConceptGraphError(ValueError):
"""An authored graph cannot be resolved against its chapter contracts."""
def digest(value: str) -> str:
return hashlib.sha256(value.encode()).hexdigest()
def sections(text: str) -> dict[str, str]:
"""Index anchored headings, excluding executable and literal fenced blocks."""
result = {}
heading = re.compile(r"^(#{1,6})\s+.*?\{#([\w-]+)[^}]*\}.*$", re.M)
# Preserve offsets but mask fences so code comments cannot become headings.
masked = re.sub(
r"^(`{3,}|~{3,})[^\n]*\n.*?^\1\s*$",
lambda m: " " * len(m[0]),
text,
flags=re.M | re.S,
)
matches = list(heading.finditer(masked))
for i, match in enumerate(matches):
level = len(match[1])
end = next(
(m.start() for m in matches[i + 1 :] if len(m[1]) <= level), len(text)
)
if match[2] in result:
raise ConceptGraphError(f"Duplicate section anchor: {match[2]}")
result[match[2]] = text[match.start() : end]
return result
def _required(record: dict, keys: tuple[str, ...], context: str) -> None:
if not isinstance(record, dict):
raise ConceptGraphError(f"{context}: expected a mapping")
for key in keys:
if not isinstance(record.get(key), str) or not record[key].strip():
raise ConceptGraphError(f"{context}: missing nonempty {key}")
def companion_resources(root: Path, chapter: dict, name: str) -> list[dict]:
"""Resolve explicitly authored activities; existence never implies readiness."""
records = chapter["contract"].get("explorer_graph", {}).get("resources", {})
if not isinstance(records, dict) or any(k not in chapter["owned"] for k in records):
raise ConceptGraphError(f"{chapter['id']}: resources must reference owned concepts")
items = records.get(name, [])
if not isinstance(items, list):
raise ConceptGraphError(f"{chapter['id']}: companion resources must be a list")
result = []
for record in items:
_required(record, ("title", "path", "kind", "status"), chapter["id"])
path = Path(record["path"])
resolved = (root / path).resolve()
if path.is_absolute() or ".." in path.parts or not resolved.is_relative_to(root.resolve()) or not resolved.is_file():
raise ConceptGraphError(f"{chapter['id']}: missing or unsafe companion source: {path}")
if record["kind"] not in ("implementation", "simulation", "lab", "slides", "hardware"):
raise ConceptGraphError(f"{chapter['id']}: unknown companion kind")
if record["status"] not in ("early-release", "ready", "skeleton"):
raise ConceptGraphError(f"{chapter['id']}: unknown companion status")
result.append({**record, "url": "https://github.com/harvard-edge/cs249r_book/blob/dev/" + urlquote(path.as_posix(), safe="/")})
return result
def load_graph(root: Path, volumes: tuple[str, ...] = ()) -> dict:
"""Resolve all nodes before relationships, allowing cross-chapter reuse."""
chapters, nodes, edges, maps = {}, {}, {}, {}
for number in range(1, 5):
volume = f"vol{number}"
if volumes and volume not in volumes:
continue
config = root / f"books/config/_quarto-pdf-{volume}.yml"
order = config.read_text() if config.exists() else ""
paths = list((root / "books" / volume).glob("*/*_concepts.yml"))
paths.sort(
key=lambda p: (
order.find(f"{volume}/{p.parent.name}/")
if f"{volume}/{p.parent.name}/" in order
else len(order),
p.parent.name,
)
)
for path in paths:
contract = yaml.safe_load(path.read_text())["concept_map"]
source = path.parent / contract["source"]
# Do not let authored paths escape their owning chapter.
if source.resolve().parent != path.parent.resolve():
raise ConceptGraphError(f"Source must be in owning chapter: {path}")
text = source.read_text()
indexed = sections(text)
title_match = re.search(r"^#\s+(.+?)(?:\s+\{.*)?$", text, re.M)
key = f"{volume}/{path.parent.name}"
owned = contract.get("primary_concepts", []) + contract.get(
"secondary_concepts", []
)
claims = contract.get("claims", {})
title = title_match[1] if title_match else path.parent.name
# Clean published form (.github/scripts/flatten-vol-urls.sh): the
# volume tree is flattened, so chapters sit directly under /volN/.
url = f"https://mlsysbook.ai/{volume}/{path.parent.name}/{source.stem}.html"
chapters[key] = dict(
id=key,
volume=volume,
title=title,
path=path,
source=source,
sections=indexed,
contract=contract,
owned=owned,
url=url,
source_hash=digest(text),
maps=[],
)
graph = contract.get("learner_graph")
if graph is None:
continue
if not isinstance(graph, dict) or graph.get("schema_version") != 1:
raise ConceptGraphError(f"{path}: unsupported learner_graph schema")
for node in graph.get("concepts", []):
_required(node, ("id", "label", "teaches", "summary", "section"), key)
aliases = node.get("aliases", [])
if not isinstance(aliases, list) or any(not isinstance(a, str) or not a.strip() for a in aliases):
raise ConceptGraphError(f"{key}: concept aliases must be nonempty strings")
ident = node["id"]
if not re.fullmatch(
r"vol[1-4]\.[a-z0-9.-]+", ident
) or not ident.startswith(volume + "."):
raise ConceptGraphError(
f"Invalid volume-scoped concept ID: {ident}"
)
if ident in nodes:
raise ConceptGraphError(f"Duplicate concept ID: {ident}")
if node["teaches"] not in owned or node["teaches"] not in claims:
raise ConceptGraphError(
f"{ident}: teaches must name an owned concept with a claim"
)
if node["section"] not in indexed:
raise ConceptGraphError(
f"{ident}: missing section {node['section']}"
)
nodes[ident] = {
**node,
"chapter": key,
"volume": number,
"resources": companion_resources(root, chapters[key], node["teaches"]),
"claim": claims[node["teaches"]],
"concept": node["teaches"],
"url": url + "#" + node["section"],
}
for relation in graph.get("relationships", []):
_required(
relation,
(
"id",
"source",
"target",
"predicate",
"explanation",
"conditions",
"status",
),
key,
)
if relation["id"] in edges:
raise ConceptGraphError(
f"Duplicate relationship ID: {relation['id']}"
)
if relation["status"] not in ("draft", "reviewed"):
raise ConceptGraphError(f"Unknown review status: {relation['id']}")
evidence = relation.get("evidence", {})
_required(evidence, ("section", "quote"), relation["id"])
section = indexed.get(evidence["section"])
if section is None:
raise ConceptGraphError(
f"{relation['id']}: missing evidence section"
)
normalized = lambda s: " ".join(s.split())
if normalized(evidence["quote"]) not in normalized(section):
raise ConceptGraphError(
f"{relation['id']}: evidence quote changed or is absent"
)
edges[relation["id"]] = {
**relation,
"label": relation["predicate"],
"chapter": key,
"evidence_url": url + "#" + evidence["section"],
"evidence_hash": digest(section),
}
for view in graph.get("maps", []):
_required(view, ("id", "focus_question", "description"), key)
if view["id"] in maps or not re.fullmatch(
r"vol[1-4]\.[a-z0-9]+(?:[.-][a-z0-9]+)*", view["id"]
):
raise ConceptGraphError(
f"Duplicate or invalid map ID: {view['id']}"
)
maps[view["id"]] = {
**view,
"chapter": key,
"title": view["focus_question"],
}
chapters[key]["maps"].append(view["id"])
for edge in edges.values():
if edge["source"] not in nodes or edge["target"] not in nodes:
raise ConceptGraphError(
f"{edge['id']}: relationship references missing concept"
)
for view in maps.values():
members = view.get("concepts", [])
relations = view.get("relationships", [])
if not members or len(members) != len(set(members)):
raise ConceptGraphError(
f"{view['id']}: empty or repeated concept membership"
)
if not relations or len(relations) != len(set(relations)):
raise ConceptGraphError(
f"{view['id']}: empty or repeated relationship membership"
)
for ident in members:
if ident not in nodes:
raise ConceptGraphError(f"{view['id']}: missing concept {ident}")
for ident in relations:
if ident not in edges:
raise ConceptGraphError(f"{view['id']}: missing relationship {ident}")
edge = edges[ident]
if edge["source"] not in members or edge["target"] not in members:
raise ConceptGraphError(
f"{view['id']}: relationship endpoint outside map"
)
for check in view.get("checks", []):
if (
check.get("concept") not in members
or not check.get("question")
or not check.get("explanation")
):
raise ConceptGraphError(f"{view['id']}: invalid learning check")
choices = check.get("choices", [])
if (
len(choices) < 2
or not isinstance(check.get("answer"), int)
or not 0 <= check["answer"] < len(choices)
):
raise ConceptGraphError(f"{view['id']}: invalid check answer")
volume_views = {}
for view in maps.values():
scope = view.get("scope", "focused")
if scope not in ("focused", "volume"):
raise ConceptGraphError(f"{view['id']}: unknown authored map scope")
volume = view["chapter"].split("/")[0]
if any(not nodes[i]["chapter"].startswith(volume + "/") for i in view["concepts"]):
raise ConceptGraphError(f"{view['id']}: map must stay inside its volume")
if scope == "volume":
if volume in volume_views:
raise ConceptGraphError(f"{volume}: only one primary volume map is allowed")
if view.get("volume") != volume:
raise ConceptGraphError(f"{view['id']}: volume scope must name its owning volume")
volume_views[volume] = view["id"]
for chapter in chapters.values():
if chapter["owned"] and chapter["volume"] in volume_views:
ident = volume_views[chapter["volume"]]
chapter["maps"] = [ident] + [m for m in chapter["maps"] if m != ident]
result = dict(chapters=chapters, nodes=nodes, edges=edges, maps=maps)
add_volume_maps(result, root)
return result
def add_volume_maps(graph: dict, root: Path) -> None:
"""Join chapter-authored concept propositions into one navigable map per book.
Evidence is the existing, editorially authored teaching claim. It establishes
provenance and freshness; draft edges still require semantic review.
"""
chapters, nodes, edges, maps = (
graph[k] for k in ("chapters", "nodes", "edges", "maps")
)
for volume in sorted({c["volume"] for c in chapters.values()}):
members, relations = set(), []
def resolve(chapter_id: str, name: str) -> str:
chapter = chapters.get(chapter_id)
if (
not chapter
or not chapter_id.startswith(volume + "/")
or name not in chapter["owned"]
):
raise ConceptGraphError(
f"Volume map: unresolved concept {chapter_id}: {name}"
)
ident = volume + ".goal." + digest(chapter_id + ":" + name)[:16]
alias = (
chapter["contract"]
.get("explorer_graph", {})
.get("labels", {})
.get(name, name)
)
nodes.setdefault(
ident,
dict(
id=ident,
label=alias,
summary=chapter["contract"]["claims"][name],
claim=chapter["contract"]["claims"][name],
concept=name,
chapter=chapter_id,
volume=int(volume[-1]),
url=chapter["url"],
kind="teaching-concept",
resources=companion_resources(root, chapter, name),
),
)
members.add(ident)
return ident
for chapter in chapters.values():
if chapter["volume"] != volume:
continue
authored = chapter["contract"].get("explorer_graph", {})
if not isinstance(authored.get("resources", {}), dict):
raise ConceptGraphError(f"{chapter['id']}: resources must reference owned concepts")
for goal in authored.get("resources", {}):
companion_resources(root, chapter, goal)
if authored and authored.get("schema_version") != 1:
raise ConceptGraphError(
f"{chapter['id']}: unsupported explorer_graph schema"
)
for relation in authored.get("relationships", []):
_required(
relation,
("source", "predicate", "explanation", "conditions", "status"),
chapter["id"],
)
if relation["status"] not in ("draft", "reviewed"):
raise ConceptGraphError(
"Volume relationship status must be draft or reviewed"
)
target = relation.get("target", {})
_required(target, ("chapter", "concept"), chapter["id"])
source_id = resolve(chapter["id"], relation["source"])
target_id = resolve(volume + "/" + target["chapter"], target["concept"])
evidence = relation.get("evidence", [])
if not evidence:
raise ConceptGraphError("Volume relationship needs claim evidence")
for item in evidence:
_required(item, ("chapter", "concept", "quote"), chapter["id"])
owner = chapters.get(volume + "/" + item["chapter"])
claim = (
owner["contract"].get("claims", {}).get(item["concept"], "")
if owner
else ""
)
if not claim or " ".join(item["quote"].split()) not in " ".join(
claim.split()
):
raise ConceptGraphError(
f"{chapter['id']}: volume relationship evidence claim changed"
)
ident = (
volume
+ ".connection."
+ digest(source_id + relation["predicate"] + target_id)[:16]
)
if ident in edges:
raise ConceptGraphError(f"Duplicate volume relationship: {ident}")
edges[ident] = {
**relation,
"id": ident,
"source": source_id,
"target": target_id,
"label": relation["predicate"],
"chapter": chapter["id"],
"evidence_url": chapter["url"],
"evidence_kind": "chapter-claim",
}
relations.append(ident)
if not relations:
continue
ident = volume + ".overview"
title = {
"vol1": "How do data, algorithms, and machines constrain one another?",
"vol2": "How does a fleet turn capacity into useful work?",
"vol3": "How does an agent turn proposals into accepted outcomes?",
"vol4": "How does a physical system turn proposals into safe actions?",
}[volume]
# Membership follows canonical chapter and concept order, not edge traversal.
ordered = [
i
for c in chapters.values()
for name in c["owned"]
if (i := volume + ".goal." + digest(c["id"] + ":" + name)[:16]) in members
]
owner = next(c for c in chapters.values() if c["volume"] == volume)
maps[ident] = dict(
id=ident,
title=title,
focus_question=title,
scope="argument" if any(m.get("scope") == "volume" and m["chapter"].startswith(volume + "/") for m in maps.values()) else "volume",
volume=volume,
chapter=owner["id"],
description="Follow concepts across chapter boundaries. Chapter selection highlights a region of this same map.",
concepts=ordered,
relationships=relations,
)
for chapter in chapters.values():
if chapter["volume"] == volume and chapter["owned"]:
if maps[ident]["scope"] == "volume":
chapter["maps"].insert(0, ident)
def map_data(graph: dict, ident: str) -> dict:
view = graph["maps"][ident]
nodes = []
for node_id in view["concepts"]:
node = graph["nodes"][node_id]
# Explicit teaches ownership joins overview arguments to finer questions.
# This is a refinement/navigation link, never inferred equivalence.
refinements = [
{"id": candidate["id"], "title": candidate["title"]}
for candidate in graph["maps"].values()
if candidate.get("scope", "focused") == "focused" and candidate["id"] != ident
and any(
graph["nodes"][i]["chapter"] == node["chapter"]
and graph["nodes"][i]["concept"] == node["concept"]
for i in candidate["concepts"]
)
]
nodes.append({
**node, "detail_maps": refinements,
"teaching_goal": node["chapter"] + ":" + digest(node["concept"])[:16],
})
return {
**view,
"nodes": nodes,
"coverage": map_coverage(graph, view),
"edges": [graph["edges"][i] for i in view["relationships"]],
}
def map_coverage(graph: dict, view: dict) -> dict:
"""Report editorial-goal representation, never claim field completeness."""
volume = view["chapter"].split("/")[0]
represented = {(graph["nodes"][i]["chapter"], graph["nodes"][i]["concept"]) for i in view["concepts"]}
chapters = []
for chapter in graph["chapters"].values():
if chapter["volume"] != volume or not chapter["owned"]:
continue
covered = [name for name in chapter["owned"] if (chapter["id"], name) in represented]
chapters.append(dict(
id=chapter["id"], title=chapter["title"], represented=covered,
outside_view=[name for name in chapter["owned"] if name not in covered],
primary_outside_view=[name for name in chapter["contract"].get("primary_concepts", []) if name not in covered],
))
return dict(
teaching_goals=sum(len(c["owned"]) for c in graph["chapters"].values() if c["volume"] == volume),
represented_goals=len(represented),
teaching_chapters=len(chapters),
represented_chapters=sum(bool(c["represented"]) for c in chapters),
chapters=chapters,
)
def svg_map(data: dict, *, pack_components: bool = False, emphasize_first: bool = True, pad_edge_labels: bool = False, horizontal: bool = False) -> tuple[str, bytes]:
"""Place labels and arrows together, retaining stable IDs for interaction."""
quote = lambda value: json.dumps(value, ensure_ascii=False)
lines = [
"digraph concepts {",
'graph [rankdir=TB, bgcolor="white", pad="0.35", nodesep="0.45", ranksep="0.85", splines=polyline, outputorder=edgesfirst];',
'node [shape=box, style="rounded,filled", fillcolor="#f7f7f7", color="#88949d", penwidth=1.15, fontname="Helvetica Neue", fontsize=16, margin="0.18,0.12"];',
'edge [fontname="Helvetica Neue", fontsize=12, color="#7a858e", fontcolor="#47535d", arrowsize=0.65];',
]
if horizontal:
lines.append("graph [rankdir=LR];")
if pack_components:
lines.append('graph [pack=true, packmode="array_u3"];')
for i, node in enumerate(data["nodes"]):
label = quote("\n".join(textwrap.wrap(node["label"], 23)))
extra = ', fillcolor="#fcebed", color="#a51c30", penwidth=1.8' if i == 0 and emphasize_first else ""
lines.append(
f"{quote(node['id'])} [id={quote('concept-' + node['id'])}, label={label}, tooltip={quote(node['summary'])}{extra}];"
)
for edge in data["edges"]:
if pad_edge_labels:
words = "
".join(html.escape(line) for line in textwrap.wrap(edge["label"], 28))
label = '<
| ' + words + ' |