mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-07-17 16:34:48 -05:00
Derive a topic→chapter "Learn more" pointer for every question at build time from schema/topic_chapter_map.yaml, closing the loop from practice back into the textbook (issue #1822, Phase 1). vault-cli: - BookRefResolver joins topic→chapter, reads chapter titles from each chapter's .qmd H1, and link-checks every mapped chapter at build time (a missing .qmd fails the build — the old "defer until URLs stabilize" blocker becomes "URLs are enforced valid"). book_refs is emitted top-level so it rides into the summary bundle and renders synchronously. - Regenerates corpus-summary.json: all 9,525 published questions now carry book_refs (100% topic coverage). staffml: - BookRefCard shows the primary chapter + authored "why" line + also_see chapters, after the attempt (go-deeper pointer, not an answer key). - Supersedes ChapterLinks; retires chapter-map.json/chapters.ts. Fixes live 404s: ChapterLinks built /contents/vol1/<ch>/ URLs that 404; the verified-200 pattern is /vol1/contents/vol1/<ch>/<ch>.html. Tests: adds test_book_refs.py (resolver + link-checker regression).
146 lines
5.6 KiB
Python
146 lines
5.6 KiB
Python
"""Tests for the topic → textbook chapter resolver and its link-checker."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from vault_cli.book_refs import BookRefError, BookRefResolver
|
|
from vault_cli.legacy_export import emit_corpus_summary
|
|
from vault_cli.loader import LoadedQuestion
|
|
from vault_cli.models import Details, Question
|
|
|
|
|
|
def _vault_with_map(tmp_path: Path, mapping: str) -> Path:
|
|
"""Lay out a minimal vault dir whose repo-root has a book/ tree.
|
|
|
|
vault_dir = <root>/interviews/vault so that vault_dir.parent.parent is the
|
|
repo root the resolver derives the book/quarto/contents path from.
|
|
"""
|
|
vault_dir = tmp_path / "interviews" / "vault"
|
|
(vault_dir / "schema").mkdir(parents=True)
|
|
(vault_dir / "schema" / "topic_chapter_map.yaml").write_text(mapping, encoding="utf-8")
|
|
return vault_dir
|
|
|
|
|
|
def _write_chapter(vault_dir: Path, vol: int, chapter: str, h1: str) -> None:
|
|
root = vault_dir.parent.parent / "book" / "quarto" / "contents" / f"vol{vol}" / chapter
|
|
root.mkdir(parents=True, exist_ok=True)
|
|
(root / f"{chapter}.qmd").write_text(f"{h1}\n\nbody\n", encoding="utf-8")
|
|
|
|
|
|
def test_resolves_primary_and_also_see_with_qmd_title(tmp_path: Path) -> None:
|
|
vault_dir = _vault_with_map(
|
|
tmp_path,
|
|
"attention-scaling:\n"
|
|
" primary: { vol: 1, chapter: nn_architectures }\n"
|
|
" also_see: [ { vol: 2, chapter: inference } ]\n"
|
|
' why: "MHA/GQA variants are architectural."\n',
|
|
)
|
|
_write_chapter(vault_dir, 1, "nn_architectures", "# Neural Architectures {#sec-nn-arch}")
|
|
_write_chapter(vault_dir, 2, "inference", "# Model Inference")
|
|
|
|
refs = BookRefResolver(vault_dir).refs_for_topic("attention-scaling")
|
|
|
|
assert refs == [
|
|
{
|
|
"vol": 1,
|
|
"chapter": "nn_architectures",
|
|
"title": "Neural Architectures", # anchor stripped, from .qmd H1
|
|
"url": "https://mlsysbook.ai/vol1/contents/vol1/nn_architectures/nn_architectures.html",
|
|
"role": "primary",
|
|
"why": "MHA/GQA variants are architectural.",
|
|
},
|
|
{
|
|
"vol": 2,
|
|
"chapter": "inference",
|
|
"title": "Model Inference",
|
|
"url": "https://mlsysbook.ai/vol2/contents/vol2/inference/inference.html",
|
|
"role": "also_see", # also_see carries no `why`
|
|
},
|
|
]
|
|
|
|
|
|
def test_unmapped_topic_returns_empty(tmp_path: Path) -> None:
|
|
vault_dir = _vault_with_map(
|
|
tmp_path,
|
|
"attention-scaling:\n primary: { vol: 1, chapter: nn_architectures }\n",
|
|
)
|
|
_write_chapter(vault_dir, 1, "nn_architectures", "# Neural Architectures")
|
|
assert BookRefResolver(vault_dir).refs_for_topic("totally-unknown-topic") == []
|
|
|
|
|
|
def test_link_check_fails_on_missing_chapter(tmp_path: Path) -> None:
|
|
"""A mapped chapter with no .qmd source must fail the build."""
|
|
vault_dir = _vault_with_map(
|
|
tmp_path,
|
|
"attention-scaling:\n primary: { vol: 1, chapter: nonexistent_chapter }\n",
|
|
)
|
|
# Create the book tree (so link-check is active) but NOT the chapter.
|
|
(vault_dir.parent.parent / "book" / "quarto" / "contents").mkdir(parents=True)
|
|
|
|
with pytest.raises(BookRefError, match="nonexistent_chapter"):
|
|
BookRefResolver(vault_dir)
|
|
|
|
|
|
def test_link_check_skipped_and_slug_fallback_when_no_book_tree(tmp_path: Path) -> None:
|
|
"""Standalone (no book tree): no link-check, titles fall back to slug."""
|
|
vault_dir = _vault_with_map(
|
|
tmp_path,
|
|
"attention-scaling:\n primary: { vol: 1, chapter: hw_acceleration }\n",
|
|
)
|
|
# No book/ tree created → link-check skipped, must not raise.
|
|
refs = BookRefResolver(vault_dir).refs_for_topic("attention-scaling")
|
|
assert refs[0]["title"] == "Hw Acceleration" # slug title-case fallback
|
|
|
|
|
|
def test_absent_map_is_noop(tmp_path: Path) -> None:
|
|
"""No map file → resolver yields nothing (existing builds unaffected)."""
|
|
vault_dir = tmp_path / "interviews" / "vault"
|
|
vault_dir.mkdir(parents=True)
|
|
assert BookRefResolver(vault_dir).refs_for_topic("anything") == []
|
|
|
|
|
|
def _make_lq(topic: str) -> LoadedQuestion:
|
|
return LoadedQuestion(
|
|
question=Question(
|
|
id="cloud-0001",
|
|
track="cloud",
|
|
level="L4",
|
|
zone="diagnosis",
|
|
topic=topic,
|
|
competency_area="memory",
|
|
bloom_level="analyze",
|
|
title="T",
|
|
scenario="plaintext scenario that is long enough to be useful.",
|
|
details=Details(realistic_solution="answer."),
|
|
status="published",
|
|
provenance="human",
|
|
),
|
|
path=Path("/tmp/cloud-0001.yaml"),
|
|
)
|
|
|
|
|
|
def test_book_refs_survive_into_summary_bundle(tmp_path: Path) -> None:
|
|
"""book_refs is top-level so it survives _to_summary (which drops only
|
|
scenario + details) and renders synchronously from the summary bundle."""
|
|
vault_dir = _vault_with_map(
|
|
tmp_path,
|
|
"kv-cache-management:\n primary: { vol: 2, chapter: inference }\n"
|
|
' why: "KV cache lives in the inference chapter."\n',
|
|
)
|
|
_write_chapter(vault_dir, 2, "inference", "# Model Inference")
|
|
(vault_dir / "release-policy.yaml").write_text(
|
|
"policy_version: 1\ninclude: {status: [published], require_validated: false}\n"
|
|
)
|
|
|
|
out = vault_dir / "corpus-summary.json"
|
|
emit_corpus_summary(vault_dir, [_make_lq("kv-cache-management")], out)
|
|
item = json.loads(out.read_text(encoding="utf-8"))[0]
|
|
|
|
assert item["scenario"] == "" # heavy field stubbed...
|
|
assert item["book_refs"][0]["chapter"] == "inference" # ...but book_refs kept
|
|
assert item["book_refs"][0]["title"] == "Model Inference"
|