Python tooling that turns the SRD 2.0 PDF (DH_SRD_2_2026_08_25.pdf, repo root, not committed) into content/**/*.md and checks the result. Module docstrings are the reference; this is the outline.
Setup: pip install -r tools/requirements.txt (pypdf; pdfplumber is listed but not installed on Pragma and not needed by the main pipeline). Run from daggerheart-web/.
srd_extract.py [--force] decodes positioned PDF text (two-column order, tabular-number glyphs, gear-table joins, bold/italic spans) into tools/_cache/srd-<sha12>.json. Everything else reads this cache.srd_catalog.py / srd_records.py slice the corpus into typed records (abilities, classes, subclasses, heritages, gear, adversaries, environments, frames). Wording comes only from the PDF. srd_records.py --entities --folder X token-diffs records against the site.srd_generate.py --kind <kind> [--include-existing] [--write]. Dry run by default; refuses pages whose emitted tokens differ from the record. Kinds: abilities, domains, classes, subclasses, ancestries, communities, transformations, weapons, armor, items, consumables, adversaries, environments, tables, frames, all. Paragraph breaks come from vertical gaps (line spacing + 2.5 pt).srd_overrides.py: explicit (old, new) fixes per page for PDF typos and house style; fails loudly if an old string disappears.srd_emph.py, applied during generate: re-wraps PDF bold/italic in body text only (**, *; never _, index cards read *). Never changes the underlying text.verify_srd.py [--section NAME] [--skip-tables]: dice/stat checks (errors) and prose drift (warnings) → tools/_cache/verify-report.json.Tests: npm run test:srd-extract (golden fixtures, test_srd_extract.py), npm run test:tools (+ share links). Review any --write with git diff before committing.
House rules: SRD 2.0 wording wins over older text; attack modifiers +0 and − (U+2212); Title Case titles; renamed pages need a 301 in netlify.toml.
verify_srd.py adversary/environment pass compares each site file to its parsed PDF card (Difficulty, Thresholds, HP, Stress, ATK, dice). Dice are compared as sets, so a repeated die or flat damage (e.g. 4 phy) isn't checked; it also shares srd_records with the generator, so a card-parser bug would hit both sides.srd_extract.py mis-splits the columns on p.72 (the armor table confuses gutter detection), so the cached Armor prose is garbled. The rules-prose check rebuilds that block from positioned runs (_armor_prose_from_runs in srd_records.py); the extractor itself is unfixed.TABLE_CHAPTERS) only warn, because table rows and section boundaries don't line up token-for-token. Their prose isn't really verified. Deliberate site-only lines are allow-listed exactly in SITE_ONLY_LINES (verify_srd.py).verify_srd.py docstring operator loop, validate_content.py and validate_weapons_pdf.py still default to the 1.0 PDF (Daggerheart-SRD-9-09-25.pdf).srd_generate.py --kind defaults to dread (leftover from the first slice); always pass --kind.srd_extract.py / test_srd_extract.py docstrings still say "Bite 0".font_subset.py (npm run fonts:subset) regenerates assets/fonts/*-subset.woff2 and css/fonts.css from the built _site; rerun after adding content with new characters (the full file is the per-character fallback).content/About.md, list/hub pages.scripts/srd20/ (generate_diff_summary.py, tag_new.py → _data/srd20.json) is the one-off 1.0 → 2.0 migration tooling; tag_new.py is still how SRD 2.0 badges are regenerated.