arborist/tests/test_textbook_tex.py
russell@unturf.com 514e07d7c2
textbooks: TeX-source ingest closes pillars I + IV (Hilbert + Boole)
Two foundational PD textbooks ship from Project Gutenberg as
LaTeX source only — no clean HTML edition. Pandoc fails on PG's
custom preamble macros; a focused regex-based stripper is the
right amount of machinery for the well-known PG TeX format.

What landed
===========
- arborist/sources/textbook_tex.py — TextbookTexSource +
  strip_tex pipeline. Drops preamble + line comments + structural
  envs (tabular, figure, thebibliography, scshape, …); keeps the
  argument of structural-but-content-bearing single-arg commands
  (textbf, emph, section, chapter, paragraph, PG's custom \\rfa);
  drops zero-arg + brace-arg structural commands (noindent,
  thispagestyle, setcounter, label, index, …); substitutes
  symbol-level macros (\\to → →, \\neg → ¬, \\forall → ∀, \\S → §,
  Greek letters, etc.).

- arborist/cli.py — `--source textbook_tex` accepts --url,
  --bundle, or --urls-from. Reuses the existing fetch + chunker
  + Merkle commit + audit pipeline; idempotent at the database
  layer (same TeX → same prose → same document_root).

- bench/scripts/textbooks_manifest.py — gains `tex-targets`
  subcommand emitting tab-separated `<tex_url>\t<id>` rows for
  manifest entries with a `tex_url` field.

- Makefile targets:
    textbooks-tex         — ingest every entry with a tex_url
    textbook-hilbert      — convenience for PG #17384
    textbook-boole        — convenience for PG #15114
  Each writes to $(CRAWL_SHARDS_DIR)/textbook_<id>.db, idempotent
  on re-run.

- tests/test_textbook_tex.py — 20 unit tests covering preamble +
  postmatter stripping, line comments, env drops (tabular, figure),
  single-arg keepers (\\textbf, \\emph, \\section, \\rfa),
  symbol-level macro subs (10 paramerized cases), structural-cmd
  drops, whitespace cleanup, idempotence on already-stripped text.

End-to-end verification
=======================
Smoke test on PG #17384 + #15114:
  Hilbert Foundations of Geometry: 1 doc / 65 chunks (192K of
    plain prose). FTS5 finds "axiom of parallels" → real chapter
    content with axiom references intact (≡, §, math fragments).
  Boole Laws of Thought: 1 doc / 273 chunks (829K). FTS5 finds
    "law of contradiction" → "the principle of contradiction"
    passage from Chapter III of Boole's text.

Vital-books coverage now 6/7 pillars
====================================

  Pillar I   Logic        ✓ Levin + Aristotle Prior + Posterior + Boole
  Pillar II  Set Theory   ✓ Levin
  Pillar III Arithmetic   ✓ Levin
  Pillar IV  Geometry     ✓ Hilbert (PG TeX)
  Pillar V   Probability  ✗ Kolmogorov license analysis pending
  Pillar VI  Phys.        ✓ Newton Principia
  Pillar VII Combin.      ✓ Bogart + Keller-Trotter + Levin
  Pillar IX  λ-Calculus   ✗ Church + Turing 1936 papers pending

Test suite: 1574 passed / 28 skipped (was 1554 + 20 new TeX tests).

Out of scope: chunk-resolution + derivations.proof_blob warrant
promotion (#000031 follow-up; see also #000032).
2026-05-09 16:06:53 -04:00

215 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests for the TextbookTexSource (#000031 §5).
Pure unit tests over the strip_tex pipeline. Network ingest is
exercised via `make textbook-hilbert` / `make textbook-boole`; this
file stays offline.
"""
from __future__ import annotations
import pytest
from arborist.sources.textbook_tex import strip_tex
# --- preamble + postmatter stripping ---------------------------------
def test_strips_preamble_and_postmatter():
tex = r"""
% comment line
\documentclass{book}
\usepackage{amsmath}
\title{Test Book}
\begin{document}
This is the body.
\end{document}
% trailing junk
"""
out = strip_tex(tex)
assert "documentclass" not in out
assert "amsmath" not in out
assert "trailing junk" not in out
assert "This is the body." in out
def test_drops_line_comments():
tex = r"""
\begin{document}
First sentence. % this is a comment
Second sentence.
\end{document}
"""
out = strip_tex(tex)
assert "comment" not in out.lower()
assert "First sentence." in out
assert "Second sentence." in out
# --- environment dropping --------------------------------------------
def test_drops_tabular_environment():
tex = r"""
\begin{document}
Before table.
\begin{tabular}{lr}
foo & bar \\
\end{tabular}
After table.
\end{document}
"""
out = strip_tex(tex)
assert "Before table." in out
assert "After table." in out
assert "foo" not in out
assert "tabular" not in out
def test_drops_figure_environment():
tex = r"""
\begin{document}
Body 1.
\begin{figure}
\includegraphics{img.png}
\caption{Caption text}
\end{figure}
Body 2.
\end{document}
"""
out = strip_tex(tex)
assert "Body 1." in out
assert "Body 2." in out
assert "Caption text" not in out
# --- single-arg command stripping ------------------------------------
def test_keeps_arg_for_emphasis_commands():
tex = r"""
\begin{document}
This is \textbf{bold} and \emph{italic} text.
\end{document}
"""
out = strip_tex(tex)
assert "bold" in out
assert "italic" in out
assert "textbf" not in out
assert "emph" not in out
def test_keeps_arg_for_section_commands():
tex = r"""
\begin{document}
\section{Introduction}
\subsection{Background}
Body text.
\end{document}
"""
out = strip_tex(tex)
assert "Introduction" in out
assert "Background" in out
assert "Body text." in out
def test_keeps_pg_custom_commands():
"""PG uses \\rfa for roman fixed all-caps section headings."""
tex = r"""
\begin{document}
\rfa{CONTENTS}
Body.
\end{document}
"""
out = strip_tex(tex)
assert "CONTENTS" in out
assert "rfa" not in out
# --- macro substitution ----------------------------------------------
@pytest.mark.parametrize(
"tex_in,expected",
[
(r"\S 5", "§ 5"),
(r"A \to B", "A → B"),
(r"\neg P", "¬ P"),
(r"P \lor Q", "P Q"),
(r"P \land Q", "P ∧ Q"),
(r"\forall x", "∀ x"),
(r"\exists y", "∃ y"),
(r"\alpha + \beta", "α + β"),
(r"\dots", ""),
(r"x \in S", "x ∈ S"),
],
)
def test_macro_substitutions(tex_in, expected):
tex = r"\begin{document}" + "\n" + tex_in + "\n" + r"\end{document}"
out = strip_tex(tex)
assert expected in out
# --- structural commands dropped -------------------------------------
def test_drops_structural_commands():
tex = r"""
\begin{document}
\noindent Body. \bigskip
\thispagestyle{empty}
\setcounter{page}{1}
\label{intro}
\index{topic}
More body.
\end{document}
"""
out = strip_tex(tex)
assert "Body." in out
assert "More body." in out
assert "noindent" not in out
assert "thispagestyle" not in out
assert "setcounter" not in out
assert "label" not in out
assert "index" not in out
# --- whitespace cleanup ----------------------------------------------
def test_collapses_multi_blank_lines():
tex = r"""
\begin{document}
First.
Second.
Third.
\end{document}
"""
out = strip_tex(tex)
# No more than 2 consecutive newlines (= 1 blank line)
assert "\n\n\n" not in out
# --- end-to-end stability --------------------------------------------
def test_idempotent_on_already_stripped_text():
"""Plain ASCII text without any LaTeX should pass through with
only whitespace normalization."""
tex = r"""
\begin{document}
This is plain text with no LaTeX commands at all.
Multiple sentences. No special characters.
\end{document}
"""
out = strip_tex(tex)
assert "This is plain text" in out
# Round-trip: stripping again should produce the same output.
out2 = strip_tex(r"\begin{document}" + out + r"\end{document}")
assert out2 == out