feat: gate writer sections on a % teach: block (SP025)

Rewrite references/pedagogy.md around recite-vs-teach: reader model,
coverage density (sections_in is cite permission, not a to-do list),
intuition-before-formula three-beat, and paper-jump filling.

Every notes/sections/sec-*.tex must now answer gap / takeaway / jump /
omit above the first \section. lint.py checks presence only (it cannot
judge honesty); files opening with "% generated by" are exempt.

- scripts/lint.py: SP025 + is_writer_section / teach_gaps helpers
- tests/no-teach-block/: fixture with takeaway only, wired into test.sh
- examples/*: all 14 writer sections get real teach blocks
- SKILL.md, references/{agents,antipatterns,checklist}.md, DESIGN.md,
  assets/notes-template.tex: route writers and consistency agent
  through pedagogy.md
This commit is contained in:
dela
2026-09-17 10:06:54 +08:00
parent 49842c5153
commit 5170111823
26 changed files with 373 additions and 16 deletions
+26
View File
@@ -43,6 +43,13 @@ KIND_ALIASES = {
}
STY_RE = re.compile(r"\\usepackage(?:\[[^\]]*\])?\{(superfig|supertensor|superderive)\}")
LABEL_RE = re.compile(r"\\splabel\{(C[1-9][0-9]*)\}")
SECTION_RE = re.compile(r"^\s*\\section\b", re.MULTILINE)
GENERATED_RE = re.compile(r"^\s*%.*generated by", re.IGNORECASE)
TEACH_KEYS = ("gap", "takeaway", "jump", "omit")
TEACH_RE = {
key: re.compile(rf"^%\s*{key}\s*:\s*\S", re.MULTILINE)
for key in TEACH_KEYS
}
def emit(path: Path, messages: list[str]) -> None:
@@ -70,6 +77,19 @@ def collect_ids(data: dict) -> list[str]:
return ids
def is_writer_section(path: Path) -> bool:
return path.parent.name == "sections" and path.name.startswith("sec-")
def teach_gaps(text: str) -> list[str]:
"""Keys missing from the `% teach:` block above the first \\section."""
if GENERATED_RE.match(text):
return []
match = SECTION_RE.search(text)
head = text[: match.start()] if match else text
return [key for key in TEACH_KEYS if not TEACH_RE[key].search(head)]
def notes_text(notes_paths: list[Path]) -> str:
chunks: list[str] = []
for path in notes_paths:
@@ -155,6 +175,12 @@ def lint(
text = path.read_text(encoding="utf-8")
if STY_RE.search(text):
errors.append(f"SP003 {path.name} loads a figure package")
if is_writer_section(path):
missing = teach_gaps(text)
if missing:
errors.append(
f"SP025 {path.name} % teach: block missing {', '.join(missing)}"
)
labels = set(LABEL_RE.findall(body)) if notes_scan else set()
+1
View File
@@ -76,6 +76,7 @@ expect_work "$ROOT/tests/loads-sty" SP003
expect_work "$ROOT/tests/missing-request" SP012
expect_work "$ROOT/tests/unlabeled-core" SP020
expect_work "$ROOT/tests/missing-include" SP021
expect_work "$ROOT/tests/no-teach-block" SP025
echo "==> compile"
for dir in \