"""R49 D1 acceptance gate for a built corpus. The design doc's D1 acceptance is "clean UTF-8, chapter-segmented, zero boilerplate lines, stable tokenization". Each is checked here as something that can actually go RED -- a gate that cannot fail is the third failure mode this target has already recorded, and it is not repeated here. python verify_corpus.py [--tokenizer PATH] """ from __future__ import annotations import argparse, collections, json, re, sys, unicodedata from pathlib import Path #: ⚠ Anchored to line start, and that is not cosmetic. The first draft matched #: `Produced by` anywhere and went RED on four hits that were all Charlotte #: Bronte's own prose -- "a chilling effect produced by his steady announcement", #: "how such a result was produced by such means". A hard rule on a phrase with a #: common non-boilerplate sense manufactures failures; same shape as the drift #: detector that fired on the adjective "minor" and stopped work three times. #: Gutenberg credits always begin a line, so require that. BOILER = [r"^.*PROJECT GUTENBERG.*$", r"^.*gutenberg\.org.*$", r"^\s*Produced by\b", r"^\s*E-text prepared by\b", r"^\s*Transcribed from\b", r"^\s*Distributed Proofread", r"^\*\*\*\s*(?:START|END) OF"] ap = argparse.ArgumentParser() ap.add_argument("corpus") ap.add_argument("--tokenizer", default=None) a = ap.parse_args() root = Path(a.corpus) man = json.loads((root / "manifest.json").read_text()) alpha = json.loads((root / "corpus_alphabet.json").read_text()) records = [] for w in man["works"]: for line in (root / w["path"]).read_text(encoding="utf-8").splitlines(): records.append(json.loads(line)) text = "\n\n".join(r["text"] for r in records) fails = [] def check(name, ok, detail=""): print(f" [{'PASS' if ok else 'FAIL'}] {name}{(' -- ' + detail) if detail else ''}") if not ok: fails.append(name) print(f"== {len(records)} chapters, {sum(r['words'] for r in records):,} words, {len(text):,} chars\n") # 1. boilerplate hits = {p: len(re.findall(p, text, re.I | re.M)) for p in BOILER} bad = {p: n for p, n in hits.items() if n} check("zero Gutenberg boilerplate", not bad, f"found {bad}" if bad else "7 patterns, 0 hits") # 2. structure per_work = collections.Counter(r["work"] for r in records) seq_ok = all( [r["chapter"] for r in records if r["work"] == w] == list(range(1, per_work[w] + 1)) for w in per_work) check("chapters number 1..N with no gaps", seq_ok, ", ".join(f"{w}:{n}" for w, n in per_work.items())) check("no empty chapters", all(r["words"] > 100 for r in records), f"min {min(r['words'] for r in records)} words") # 3. typography consistency AFTER normalisation -- the reason normalisation exists counts = collections.Counter(text) straight = counts['"'] + counts["'"] dbl_hyphen = len(re.findall(r"(?