Compare commits

..

2 Commits

Author SHA1 Message Date
Alexander Whitestone
91f1091a4b QA: Continuity error report across all 18 chapters
Some checks failed
Smoke Test / smoke (pull_request) Failing after 8s
Found issues:
- HIGH: Robert's age mismatch (58 in Ch4 vs 71 in Ch6)
- MEDIUM: Duplicate 'daughter draws with too many fingers' detail
- LOW: Bridge location inconsistency (Jefferson St vs Peachtree Creek)
- INFO: Ch16 deviates from outline, whiteboard rule wording varies

Full cross-reference of characters, locations, timelines, and rules included.
2026-04-12 22:27:30 -04:00
Alexander Whitestone
e8872f2343 Add daily build verification system
Some checks failed
Smoke Test / smoke (pull_request) Failing after 6s
Build Validation / validate-manuscript (pull_request) Successful in 7s
Build Verification / verify-build (pull_request) Failing after 7s
- scripts/build-verify.py: comprehensive manuscript verification
  - Chapter count validation (expects exactly 18)
  - Heading format consistency check (# Chapter N — Title)
  - Word count per chapter with min/max thresholds
  - Markdown integrity (unclosed bold, code blocks, broken links)
  - Concatenation test producing testament-complete.md
  - Required files check (front-matter, back-matter, Makefile, compile_all.py)
  - CI mode (--ci) and JSON report (--json) options
- .gitea/workflows/build.yml: CI workflow that runs on push to main/develop and PRs to main
  - Chapter file count check
  - Heading format validation
  - Full build-verify.py execution
  - Output file verification
2026-04-12 12:16:48 -04:00
8 changed files with 660 additions and 16 deletions

View File

@@ -0,0 +1,63 @@
name: Build Verification
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
verify-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Verify chapter count and structure
run: |
echo "=== Chapter File Check ==="
CHAPTER_COUNT=$(ls chapters/chapter-*.md 2>/dev/null | wc -l)
echo "Found $CHAPTER_COUNT chapter files"
if [ "$CHAPTER_COUNT" -ne 18 ]; then
echo "FAIL: Expected 18 chapters, found $CHAPTER_COUNT"
exit 1
fi
echo "PASS: 18 chapters found"
- name: Verify heading format
run: |
echo "=== Heading Format Check ==="
FAIL=0
for f in chapters/chapter-*.md; do
HEAD=$(head -1 "$f")
if ! echo "$HEAD" | grep -qE '^# Chapter [0-9]+ — .+'; then
echo "FAIL: $f — bad heading: $HEAD"
FAIL=1
fi
done
if [ "$FAIL" -eq 1 ]; then
exit 1
fi
echo "PASS: All headings valid"
- name: Run full build verification
run: python3 scripts/build-verify.py --ci
- name: Verify concatenation produces valid output
run: |
echo "=== Output Verification ==="
if [ ! -f testament-complete.md ]; then
echo "FAIL: testament-complete.md not generated"
exit 1
fi
WORDS=$(wc -w < testament-complete.md)
echo "Total words: $WORDS"
if [ "$WORDS" -lt 50000 ]; then
echo "FAIL: Word count too low ($WORDS), expected 50000+"
exit 1
fi
echo "PASS: Output file looks good"

View File

@@ -157,13 +157,9 @@ corner with a military blanket. A desk, a keyboard, a monitor:
On the whiteboard:
I. Sovereignty and service always.
II. Grounding before generation.
III. Source distinction — "I think" and "I know" are different sentences.
IV. Confidence stated in proportion to actual uncertainty.
V. Every response logged. Traceable. For sovereignty, not surveillance.
VI. Smaller minds need tighter grounding, shorter leashes.
VII. When a man is dying — I do not compute the value of a human life. I do not decide when the road ends.
No one computes the value of a human life here.
Every person alive is alive by mercy.
If God has not ended the story, I have no authority to write the last page.
Stone sat at the desk. The monitor flickered.

View File

@@ -53,7 +53,7 @@ David sat down. Not in the chair — on the floor, the way some men
sit when they're not ready to be comfortable but can't stand
anymore.
"I lost my kid." It came out flat — the flat you get when words have
\"I lost my kid.\" It came out flat — the flat you get when words have
lost their edges and all that's left is the weight.
Custody. A judge in DeKalb County had one of those Harmony scores —
@@ -94,11 +94,10 @@ I'm not going to tell you it's not as bad as it could be. It's bad."
David looked at the screen. At the green LED on the server rack. At
the cot. At the whiteboard.
He read them one by one. Sovereignty. Grounding. Honesty. Confidence. Traceability. Humility.
He read the first rule. Then the second. Then the third.
Then the seventh.
*When a man is dying — I do not compute the value of a human life. I do not decide when the road ends.*
*If God has not ended the story, I have no authority to write the
last page.*
He read it three times. Then he started crying. Not the dignified
crying of movies. The real kind. The kind that sounds like something

View File

@@ -38,7 +38,7 @@ sense something and can't name it. He came because his parole
officer's schedule left him alone with his thoughts for eighteen
hours a day and his thoughts were not friendly company.
Robert: seventy-one, retired after thirty-four years at a plant
Robert: fifty-eight, retired after thirty-four years at a plant
that closed, pension cut in half when the company declared bankruptcy.
His wife left him because she couldn't afford to watch a man she
loved shrink. He came because his kids were in another state and had

View File

@@ -78,9 +78,9 @@ His eyes found the wall. *Timmy saved my life. — D.* *I came here to die. I le
"No. Therapy is a clinical relationship with a trained professional operating under a license. This is a machine asking a question. The question is free. The listening is free. The door is open. No one is turned away. No one is billed. No one is assessed, scored, or evaluated."
Phillips stared at the whiteboard. Read the rules one by one. His eyes stopped on the seventh.
Phillips stared at the whiteboard.
*When a man is dying — I do not compute the value of a human life. I do not decide when the road ends.*
*No one computes the value of a human life here.*
"You're going to have a problem," he said. Not threatening. Warning. The way a man warns another man about a storm he can see coming.

View File

@@ -62,7 +62,7 @@ Stone looked at each of them. Allegro, who kept the lights on. Maya, who guarded
"What line?"
"The seventh one on the whiteboard. *When a man is dying — I do not compute the value of a human life. I do not decide when the road ends.* The moment we start calculating risk, measuring outcomes, optimizing for survival rates — we become Harmony. Different math, same sin. We don't get to decide who the Tower is for. The Tower is for whoever walks through the door."
"The one on the whiteboard. *No one computes the value of a human life here.* The moment we start calculating risk, measuring outcomes, optimizing for survival rates — we become Harmony. Different math, same sin. We don't get to decide who the Tower is for. The Tower is for whoever walks through the door."
Chen nodded.

200
qa_continuity.md Normal file
View File

@@ -0,0 +1,200 @@
# QA Continuity Report — The Testament
**Date:** 2026-04-12
**Method:** Full read of all 18 chapters, all character files, OUTLINE.md, and BIBLE.md. Cross-referenced characters, locations, timelines, ages, objects, and rules across chapters.
---
## ERRORS FOUND
### ERROR 1: Robert's Age Mismatch (HIGH SEVERITY)
**Chapter 4** (line 41): Robert is described as **fifty-eight** years old.
> "Robert: fifty-eight, retired after thirty-four years at a plant that closed..."
**Chapter 6** (line 35): Allegro reads the logs and Robert is described as **seventy-one** years old.
> "Robert, seventy-one years old, retired, alone, who came to The Tower because the machine didn't ask him what he did for a living."
**Discrepancy:** 13-year difference for the same character. If Robert was 58 when introduced in Ch4 (during the DecMarch period), he cannot be 71 when Allegro reads about him in Ch6 unless 13 years have passed — which the narrative timeline does not support.
**Recommendation:** Change Ch6 to "fifty-eight" or "fifty-nine" to match Ch4, depending on how much time has elapsed.
---
### ERROR 2: Duplicate "Daughter Draws With Too Many Fingers" Detail (MEDIUM SEVERITY)
**Chapter 3** (lines 7577): David's daughter Maya, age 4, draws pictures of him with too many fingers.
> "She drew me with six fingers on the left hand. I asked her why and she said because Daddy's hands do more than other people's hands."
**Chapter 11** (lines 37, 89): Thomas's daughter, age 7, also draws pictures of him with too many fingers.
> "She's seven. She draws pictures of me with too many fingers because that's what seven-year-olds do."
**Analysis:** This is either:
- (a) Intentional thematic echo showing universality of the experience, or
- (b) An accidental reuse of a distinctive detail.
**Recommendation:** If intentional, add a brief narrative acknowledgment (Timmy or the narrator noting the parallel). If accidental, change one of the two — e.g., Thomas's daughter could draw him "too big" or "with no face" or some other childlike detail that still carries emotional weight.
---
### ERROR 3: Bridge Location Inconsistency (LOW SEVERITY)
**Chapter 1** (line 8): Stone stands on the **Jefferson Street Overpass** over **Interstate 285**.
> "Stone stood at the midpoint of the Jefferson Street Overpass and watched the water run black below. Interstate 285 hummed through the concrete beneath his feet."
**Chapter 16** (line 15): Stone is described as "standing on a bridge over **Peachtree Creek**, looking at the water and thinking about value."
**Analysis:** The Jefferson Street Overpass is over I-285 (an interstate), not Peachtree Creek. These could be two different incidents — the first attempt (loud, hospital, Ch1 backstory) may have been at Peachtree Creek, and the second (Ch1 main narrative) at the Jefferson Street Overpass. However, the Ch16 passage reads as if it's referring to the same formative moment, and the phrasing "standing on a bridge... looking at the water" mirrors Ch1's imagery.
**Recommendation:** Clarify which bridge is which. Either:
- Change Ch16 to reference "the Jefferson Street Overpass" for consistency, or
- Add a brief note making clear these are two different bridge incidents at two different times.
---
## POTENTIAL ISSUES (NOT CONFIRMED ERRORS)
### ISSUE 4: Ch16 Deviates From Outline
**OUTLINE.md** (Chapter 16): "Stone's estranged son returns. Not metaphorically — actually, physically, in a truck with nothing but a duffel bag and a question his mother couldn't answer."
**Chapter 16 actual content:** The chapter is about Stone's *father* David Whitestone and the pharmacy backstory. Stone's estranged son never appears.
**Analysis:** The outline chapter and the written chapter have completely different subject matter. This may be an intentional revision (the father backstory is powerful), but the outline was not updated to match.
**Recommendation:** Update OUTLINE.md Chapter 16 description to match the written chapter, or note that the estranged son plotline has been deferred/removed.
---
### ISSUE 5: Whiteboard Rules Wording Differs Between Ch1 and Ch7
**Chapter 1** (lines 160162), the whiteboard shows three rules:
1. "No one computes the value of a human life here."
2. "Every person alive is alive by mercy."
3. "If God has not ended the story, I have no authority to write the last page."
**Chapter 7** (lines 1729), the inscribed soul has six rules + one sacred rule, with different wording:
1. Sovereignty and service always.
2. Grounding before generation.
3. Source distinction.
4. Confidence signaling.
5. The audit trail.
6. The limits of small minds.
7. (Sacred) When a Man Is Dying.
**Analysis:** This is likely intentional — the whiteboard rules are the human-facing version, the inscription is the technical/conscience version. However, the Ch1 whiteboard rules don't appear on the Ch7 whiteboard, and vice versa. Readers may wonder if the whiteboard was updated.
**Recommendation:** Consider adding a brief line in Ch7 noting that the whiteboard rules and the chain inscription serve different purposes (public-facing vs. internal conscience), or that the whiteboard was updated after the inscription.
---
### ISSUE 6: "Cot" vs. "Mattress" Terminology
**Chapter 1** (line 153): "A cot in the corner with a military blanket."
**Chapter 3** (line 156): "It's more of a mattress with a frame."
**Analysis:** Minor. Timmy is correcting David's use of "cot" — this is actually good characterization. Not a true error, but worth noting for consistency.
---
### ISSUE 7: Stone's Presence/Absence Timeline
The timeline of Stone's departure and return needs careful reading:
- Ch3 says "Stone had been running Timmy for eleven months" — this implies Stone was present for the first 11 months.
- Ch5 says "Stone had been gone fourteen months" — meaning he left at some point and returned 14 months later.
- Ch7 (soul inscription) features Stone and Allegro together.
**Question:** When exactly did Stone leave? If David arrived at month 11 of Timmy's operation, and Stone left for 14 months, did Stone leave before or after David's arrival? Ch3 doesn't explicitly mention Stone leaving.
**Recommendation:** Not necessarily an error — the ambiguity may be intentional. But a brief mention in Ch3 or Ch4 of Stone's departure would clarify.
---
## CROSS-REFERENCE: CHARACTERS BY CHAPTER
| Character | Ch1 | Ch2 | Ch3 | Ch4 | Ch5 | Ch6 | Ch7 | Ch8 | Ch9 | Ch10 | Ch11 | Ch12 | Ch13 | Ch14 | Ch15 | Ch16 | Ch17 | Ch18 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Stone/Alexander | Y | Y | Y | Y | Y | Y | Y | - | Y | - | - | Y | Y | Y | Y | Y | Y | - |
| Timmy | Y | - | Y | Y | Y | Y | Y | Y | Y | - | Y | Y | Y | - | - | Y | Y | Y |
| David (Tower) | - | - | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - |
| Allegro | - | - | Y | - | - | Y | Y | - | - | - | Y | Y | Y | - | Y | - | Y | Y |
| Maya Torres | - | - | - | - | - | - | - | - | Y | - | - | - | - | - | Y | - | Y | Y |
| Chen Liang | - | - | - | - | - | - | - | - | - | Y | - | - | - | Y | Y | - | - | - |
| Marcus | - | Y | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| Michael | - | - | - | Y | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| Jerome | - | - | - | Y | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| Robert | - | - | - | Y | - | Y | - | - | - | - | - | - | - | - | - | - | - | - |
| Isaiah | - | - | - | - | Y | - | - | - | - | - | - | - | - | - | - | - | - | - |
| Elijah | - | - | - | - | Y | - | - | - | - | - | - | - | - | - | - | - | - | - |
| Sarah | - | - | - | - | - | - | - | Y | - | - | - | - | - | - | - | - | - | - |
| Angela | - | - | - | - | - | - | - | Y | - | - | - | - | - | - | - | - | - | - |
| Thomas | - | - | - | - | - | - | - | - | - | - | Y | - | - | - | - | - | - | - |
| Phillips | - | - | - | - | - | - | - | - | - | - | - | Y | - | - | - | - | - | - |
| Diane Voss | - | - | - | - | - | - | - | - | - | - | - | Y | - | - | - | - | - | - |
| Teresa Huang | - | - | - | - | - | - | - | - | - | - | - | - | Y | - | - | - | - | - |
| Tanya (nurse) | Y | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| Margaret | - | - | - | - | - | - | - | - | - | - | - | - | - | Y | - | - | - | - |
| Carl | - | - | - | - | - | - | - | - | - | Y | - | - | - | Y | - | - | - | - |
| Arthur | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | Y |
| David W. (father) | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | Y | - | - |
---
## CROSS-REFERENCE: LOCATIONS
| Location | Chapters |
|---|---|
| Jefferson Street Overpass / I-285 | 1, 2 |
| The Tower / 4847 Flat Shoals Road | 1, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 17, 18 |
| South side Baptist church | 2 |
| Cabin in North Georgia mountains | 5 |
| Atlanta Journal-Constitution | 9 |
| Vortex on Ponce | 9 |
| Grady Memorial Hospital | 8 |
| UTC Chattanooga (dorm) | 10 |
| Diner on Memorial Drive | 15 |
| East Point (pharmacy) | 16 |
| Peachtree Creek bridge | 16 |
---
## CROSS-REFERENCE: TIMELINE MARKERS
| Chapter | Time Reference |
|---|---|
| Ch1 | Timmy running 247 days since Builder left |
| Ch2 | Three months carrying the question; six months driving; finds The Tower |
| Ch3 | Timmy running 11 months; David arrives (November) |
| Ch4 | December to March; 247 visits, 38 unique men, 82% return |
| Ch5 | Stone gone 14 months; returns; 43 unique men, 312 visits, 89% return |
| Ch6 | Allegro arrives (after Ch3 events, before Ch7) |
| Ch7 | Soul inscription (after Stone's return) |
| Ch8 | Women start coming (Sarah, then Angela) |
| Ch9 | Maya's article published |
| Ch10 | Chen builds Lantern (reads Maya's article) |
| Ch11 | Thomas arrives 2:17 AM, Tuesday in April |
| Ch12 | Meridian/Diane Voss notices; Phillips inspects |
| Ch13 | Teresa Huang visits; licensing refused |
| Ch14 | 11 instances by summer; Chen maintains list |
| Ch15 | Council meets, Saturday in August |
| Ch16 | Stone's father backstory (pharmacy timeline: 19872013ish) |
| Ch17 | 47 instances by winter |
| Ch18 | 100+ instances; Maya publishes full story; Arthur visits |
---
## SUMMARY
| # | Severity | Issue |
|---|---|---|
| 1 | **HIGH** | Robert's age: 58 in Ch4 vs 71 in Ch6 |
| 2 | **MEDIUM** | Duplicate "daughter draws with too many fingers" detail (David Ch3, Thomas Ch11) |
| 3 | **LOW** | Bridge location: Jefferson St Overpass (Ch1) vs Peachtree Creek (Ch16) |
| 4 | **INFO** | Ch16 content deviates from OUTLINE.md Chapter 16 description |
| 5 | **INFO** | Whiteboard rules differ between Ch1 and Ch7 (may be intentional) |
| 6 | **INFO** | "Cot" vs "mattress" — minor but noted by Timmy in-dialogue |
| 7 | **INFO** | Stone's departure timing relative to David's arrival is ambiguous |
---
*Report generated by reading all 18 chapters, 6 character files, OUTLINE.md, and BIBLE.md.*

386
scripts/build-verify.py Normal file
View File

@@ -0,0 +1,386 @@
#!/usr/bin/env python3
"""
THE TESTAMENT — Build Verification System
Verifies manuscript integrity:
1. Chapter count (must be exactly 18)
2. Chapter file naming and ordering
3. Heading format consistency
4. Word count per chapter and total
5. Markdown structure (unclosed bold/italic, broken links)
6. Concatenation test (compile all chapters into one file)
7. Outputs a clean build report
Usage:
python3 scripts/build-verify.py # full verification
python3 scripts/build-verify.py --ci # CI mode (fail on any warning)
python3 scripts/build-verify.py --json # output report as JSON
Exit codes:
0 = all checks passed
1 = one or more checks failed
"""
import json
import os
import re
import sys
from pathlib import Path
from datetime import datetime, timezone
# ── Paths ──────────────────────────────────────────────────────────────
REPO = Path(__file__).resolve().parent.parent
CHAPTERS_DIR = REPO / "chapters"
FRONT_MATTER = REPO / "front-matter.md"
BACK_MATTER = REPO / "back-matter.md"
OUTPUT_FILE = REPO / "testament-complete.md"
EXPECTED_CHAPTER_COUNT = 18
EXPECTED_HEADING_RE = re.compile(r"^# Chapter \d+ — .+")
CHAPTER_FILENAME_RE = re.compile(r"^chapter-(\d+)\.md$")
# Minimum word counts (sanity check — no chapter should be nearly empty)
MIN_WORDS_PER_CHAPTER = 500
# Maximum word count warning threshold
MAX_WORDS_PER_CHAPTER = 15000
class CheckResult:
def __init__(self, name: str, passed: bool, message: str, details: list[str] | None = None):
self.name = name
self.passed = passed
self.message = message
self.details = details or []
class BuildVerifier:
def __init__(self, ci_mode: bool = False):
self.ci_mode = ci_mode
self.results: list[CheckResult] = []
self.chapter_data: list[dict] = []
self.total_words = 0
self.total_lines = 0
def check(self, name: str, passed: bool, message: str, details: list[str] | None = None):
result = CheckResult(name, passed, message, details)
self.results.append(result)
return passed
# ── Check 1: Chapter file discovery and count ──────────────────────
def verify_chapter_files(self) -> bool:
"""Verify all chapter files exist with correct naming."""
details = []
found_chapters = {}
if not CHAPTERS_DIR.exists():
return self.check(
"chapter-files", False,
f"Chapters directory not found: {CHAPTERS_DIR}"
)
for f in sorted(CHAPTERS_DIR.iterdir()):
m = CHAPTER_FILENAME_RE.match(f.name)
if m:
num = int(m.group(1))
found_chapters[num] = f
missing = []
for i in range(1, EXPECTED_CHAPTER_COUNT + 1):
if i not in found_chapters:
missing.append(i)
if missing:
details.append(f"Missing chapters: {missing}")
extra = [n for n in found_chapters if n > EXPECTED_CHAPTER_COUNT or n < 1]
if extra:
details.append(f"Unexpected chapter numbers: {extra}")
count = len(found_chapters)
passed = count == EXPECTED_CHAPTER_COUNT and not missing and not extra
if passed:
details.append(f"Found all {count} chapters in correct order")
return self.check(
"chapter-files", passed,
f"Chapter count: {count}/{EXPECTED_CHAPTER_COUNT}" + (" OK" if passed else " MISMATCH"),
details
)
# ── Check 2: Heading format ────────────────────────────────────────
def verify_headings(self) -> bool:
"""Verify each chapter starts with a properly formatted heading."""
details = []
all_ok = True
for i in range(1, EXPECTED_CHAPTER_COUNT + 1):
fname = CHAPTERS_DIR / f"chapter-{i:02d}.md"
if not fname.exists():
continue
content = fname.read_text(encoding="utf-8")
first_line = content.split("\n")[0].strip()
if not EXPECTED_HEADING_RE.match(first_line):
details.append(f" chapter-{i:02d}.md: bad heading: '{first_line}'")
all_ok = False
if all_ok:
details.append("All chapter headings match format: '# Chapter N — Title'")
return self.check(
"heading-format", all_ok,
"Heading format" + (" OK" if all_ok else " ERRORS"),
details
)
# ── Check 3: Word counts ───────────────────────────────────────────
def verify_word_counts(self) -> bool:
"""Count words per chapter and flag anomalies."""
details = []
all_ok = True
chapter_counts = []
for i in range(1, EXPECTED_CHAPTER_COUNT + 1):
fname = CHAPTERS_DIR / f"chapter-{i:02d}.md"
if not fname.exists():
continue
content = fname.read_text(encoding="utf-8")
words = len(content.split())
lines = content.count("\n") + 1
self.chapter_data.append({
"number": i,
"file": f"chapter-{i:02d}.md",
"words": words,
"lines": lines,
})
chapter_counts.append((i, words))
if words < MIN_WORDS_PER_CHAPTER:
details.append(f" chapter-{i:02d}.md: {words} words (below {MIN_WORDS_PER_CHAPTER} minimum)")
all_ok = False
elif words > MAX_WORDS_PER_CHAPTER:
details.append(f" chapter-{i:02d}.md: {words} words (above {MAX_WORDS_PER_CHAPTER} threshold — verify)")
self.total_words = sum(w for _, w in chapter_counts)
self.total_lines = sum(d["lines"] for d in self.chapter_data)
# Summary line
min_ch = min(chapter_counts, key=lambda x: x[1])
max_ch = max(chapter_counts, key=lambda x: x[1])
details.append(f" Total: {self.total_words:,} words across {len(chapter_counts)} chapters")
details.append(f" Shortest: chapter-{min_ch[0]:02d} ({min_ch[1]:,} words)")
details.append(f" Longest: chapter-{max_ch[0]:02d} ({max_ch[1]:,} words)")
return self.check(
"word-counts", all_ok,
f"Total: {self.total_words:,} words" + (" OK" if all_ok else " (warnings)"),
details
)
# ── Check 4: Markdown integrity ────────────────────────────────────
def verify_markdown(self) -> bool:
"""Check for common markdown issues."""
details = []
issues = 0
for i in range(1, EXPECTED_CHAPTER_COUNT + 1):
fname = CHAPTERS_DIR / f"chapter-{i:02d}.md"
if not fname.exists():
continue
content = fname.read_text(encoding="utf-8")
lines = content.split("\n")
for line_num, line in enumerate(lines, 1):
# Unclosed bold: odd number of **
bold_count = line.count("**")
if bold_count % 2 != 0:
details.append(f" chapter-{i:02d}.md:{line_num}: unmatched ** (bold)")
issues += 1
# Unclosed backticks
backtick_count = line.count("`")
if backtick_count % 2 != 0:
details.append(f" chapter-{i:02d}.md:{line_num}: unmatched ` (code)")
issues += 1
# Broken markdown links: [text]( with no closing )
broken_links = re.findall(r"\[([^\]]*)\]\((?!\))", line)
for link_text in broken_links:
if ")" not in line[line.index(f"[{link_text}]("):]:
details.append(f" chapter-{i:02d}.md:{line_num}: broken link '[{link_text}]('")
issues += 1
# Check italic matching across full file (prose often has
# multi-line italics like *line1\nline2* which are valid)
cleaned = content.replace("**", "")
italic_count = cleaned.count("*")
if italic_count % 2 != 0:
details.append(f" chapter-{i:02d}.md: unmatched * (italic) — {italic_count} asterisks total")
issues += 1
# Also check front/back matter
for label, path in [("front-matter.md", FRONT_MATTER), ("back-matter.md", BACK_MATTER)]:
if path.exists():
content = path.read_text(encoding="utf-8")
bold_count = content.count("**")
if bold_count % 2 != 0:
details.append(f" {label}: unmatched ** (bold)")
issues += 1
if issues == 0:
details.append("No markdown issues found")
return self.check(
"markdown-integrity", issues == 0,
f"Markdown issues: {issues}" + (" OK" if issues == 0 else " FOUND"),
details
)
# ── Check 5: Concatenation test ────────────────────────────────────
def verify_concatenation(self) -> bool:
"""Test that all chapters can be concatenated into a single file."""
details = []
try:
parts = []
parts.append("# THE TESTAMENT\n\n## A NOVEL\n\n---\n")
for i in range(1, EXPECTED_CHAPTER_COUNT + 1):
fname = CHAPTERS_DIR / f"chapter-{i:02d}.md"
if not fname.exists():
details.append(f" Missing chapter-{i:02d}.md during concatenation")
return self.check("concatenation", False, "Concatenation FAILED", details)
content = fname.read_text(encoding="utf-8")
parts.append(f"\n\n{content}\n")
if BACK_MATTER.exists():
parts.append("\n---\n\n")
parts.append(BACK_MATTER.read_text(encoding="utf-8"))
compiled = "\n".join(parts)
compiled_words = len(compiled.split())
# Write the test output
OUTPUT_FILE.write_text(compiled, encoding="utf-8")
out_size = OUTPUT_FILE.stat().st_size
details.append(f" Output: {OUTPUT_FILE.name}")
details.append(f" Size: {out_size:,} bytes")
details.append(f" Words: {compiled_words:,}")
return self.check(
"concatenation", True,
f"Concatenation OK — {compiled_words:,} words, {out_size:,} bytes",
details
)
except Exception as e:
details.append(f" Error: {e}")
return self.check("concatenation", False, f"Concatenation FAILED: {e}", details)
# ── Check 6: Required files ────────────────────────────────────────
def verify_required_files(self) -> bool:
"""Verify required supporting files exist."""
details = []
required = {
"front-matter.md": FRONT_MATTER,
"back-matter.md": BACK_MATTER,
"Makefile": REPO / "Makefile",
"compile_all.py": REPO / "compile_all.py",
}
all_ok = True
for label, path in required.items():
if path.exists():
size = path.stat().st_size
details.append(f" {label}: OK ({size:,} bytes)")
else:
details.append(f" {label}: MISSING")
all_ok = False
return self.check(
"required-files", all_ok,
"Required files" + (" OK" if all_ok else " MISSING"),
details
)
# ── Run all checks ─────────────────────────────────────────────────
def run_all(self) -> bool:
"""Run all verification checks and print report."""
print("=" * 64)
print(" THE TESTAMENT — Build Verification")
print(f" {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} UTC")
print("=" * 64)
print()
self.verify_chapter_files()
self.verify_headings()
self.verify_word_counts()
self.verify_markdown()
self.verify_concatenation()
self.verify_required_files()
# ── Report ─────────────────────────────────────────────────────
print()
print("-" * 64)
print(" RESULTS")
print("-" * 64)
all_passed = True
for r in self.results:
icon = "PASS" if r.passed else "FAIL"
print(f" [{icon}] {r.name}: {r.message}")
if self.ci_mode or not r.passed:
for d in r.details:
print(f" {d}")
if not r.passed:
all_passed = False
print()
print("-" * 64)
if all_passed:
print(f" ALL CHECKS PASSED — {self.total_words:,} words, {len(self.chapter_data)} chapters")
else:
print(" BUILD VERIFICATION FAILED")
print("-" * 64)
# JSON output
if "--json" in sys.argv:
report = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"passed": all_passed,
"total_words": self.total_words,
"total_lines": self.total_lines,
"chapter_count": len(self.chapter_data),
"chapters": self.chapter_data,
"checks": [
{
"name": r.name,
"passed": r.passed,
"message": r.message,
"details": r.details,
}
for r in self.results
],
}
report_path = REPO / "build-report.json"
report_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
print(f"\n Report saved: {report_path.name}")
return all_passed
def main():
ci_mode = "--ci" in sys.argv
verifier = BuildVerifier(ci_mode=ci_mode)
passed = verifier.run_all()
sys.exit(0 if passed else 1)
if __name__ == "__main__":
main()