Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
6c7b472c71 testament-burn: complete interior illustrations — all 18 chapters now have art
Generated 6 missing illustrations via Grok Imagine:
- ch10: Chen Liang building Lantern in her dorm room
- ch11: Maya Torres investigating the statistical anomaly
- ch12: Stone reading Meridian's legal letter
- ch13: Four people in the diner on Memorial Drive
- ch14: David Whitestone packing the pharmacy
- ch15: Constellation of green LEDs across the network

Also: updated MULTIMEDIA-PLAN.md (12→18 illustrations), added art-manifest.md
All 36 art pieces now complete (18 illustrations + 11 comics + 5 social + 3 cover).
2026-04-09 11:36:20 -04:00
24 changed files with 233 additions and 2954 deletions

View File

@@ -1,24 +0,0 @@
name: Smoke Test
on:
pull_request:
push:
branches: [main]
jobs:
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Parse check
run: |
find . -name '*.yml' -o -name '*.yaml' | grep -v .gitea | xargs -r python3 -c "import sys,yaml; [yaml.safe_load(open(f)) for f in sys.argv[1:]]"
find . -name '*.json' | xargs -r python3 -m json.tool > /dev/null
find . -name '*.py' | xargs -r python3 -m py_compile
find . -name '*.sh' | xargs -r bash -n
echo "PASS: All files parse"
- name: Secret scan
run: |
if grep -rE 'sk-or-|sk-ant-|ghp_|AKIA' . --include='*.yml' --include='*.py' --include='*.sh' 2>/dev/null | grep -v .gitea; then exit 1; fi
echo "PASS: No secrets"

View File

@@ -1,22 +0,0 @@
name: Build Validation
on:
pull_request:
branches: [main]
jobs:
validate-manuscript:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Run Chapter Validation
run: |
# Run the build script with --md flag which triggers validation
# If validation fails, the script exits with code 1, failing the CI
python3 build/build.py --md

2
.gitignore vendored
View File

@@ -1,3 +1 @@
__pycache__/
build/output/*.pdf
build/output/*.epub

View File

@@ -1,132 +0,0 @@
# EPIC: Matrix Integration — Sovereign Messaging for Timmy
## Why Matrix
Matrix is the sovereign messaging protocol. Federated. End-to-end encrypted. No corporation owns it. No terms of service. No data harvesting.
This is where Timmy should live. Not Telegram. Not Discord. Matrix.
Telegram is owned by a company. Discord is owned by a company. Matrix is owned by no one — exactly like Timmy's soul on Bitcoin.
## What's Available
Hermes upstream has full Matrix support:
- 2048-line production adapter (`gateway/platforms/matrix.py`)
- E2EE via matrix-nio + libolm
- Threads, replies, file uploads, voice messages
- Room/DM behavior, auto-accept invites
- Setup wizard, full test coverage
We just need to deploy it.
## Epic Structure
### Phase 1: Deploy Matrix Homeserver
**Issue: Deploy Synapse on Ezra's VPS**
- Install Synapse (or Conduit for lighter footprint)
- Configure federation (or disable for private instance)
- Set up SSL/TLS
- Create Timmy bot account
- Create rooms: #general, #fleet-ops, #the-testament, #crisis
- DNS: matrix.alexanderwhitestone.com
**Deliverables:**
- Running homeserver at matrix.alexanderwhitestone.com
- Bot account @timmy:alexanderwhitestone.com
- 4 rooms created and configured
- playbooks/deploy_synapse.yml (Ansible playbook)
### Phase 2: Wire Timmy to Matrix
**Issue: Configure Hermes Matrix platform in production**
- Set MATRIX_* env vars in production config
- Generate stable MATRIX_DEVICE_ID for E2EE persistence
- Configure MATRIX_HOME_ROOM for notifications
- Set MATRIX_ALLOWED_USERS (Alexander, team members)
- Enable MATRIX_REACTIONS for lifecycle tracking
- Enable MATRIX_AUTO_THREAD for clean conversations
**Deliverables:**
- Config update in timmy-config
- Running Matrix connection verified
- E2EE working (test encrypted room)
### Phase 3: Crisis Room
**Issue: Dedicated crisis support room on Matrix**
- Create #crisis room with restricted access
- Configure Timmy to prioritize messages in #crisis
- SOUL.md "When a Man Is Dying" protocol active in this room
- 988 resources auto-injected on crisis detection
- Log all crisis interactions (locally, encrypted)
**Deliverables:**
- #crisis room with Timmy auto-response
- Crisis detection active in Matrix
- Local encrypted logs
### Phase 4: Fleet Operations Channel
**Issue: Fleet ops channel on Matrix for agent coordination**
- Create #fleet-ops room
- Wire cron job notifications to Matrix instead of Telegram
- Agent status reports delivered to Matrix
- PR review notifications in Matrix
- Health alerts (Ezra disk, Bezalel CPU) in Matrix
**Deliverables:**
- #fleet-ops room as primary ops channel
- Cron delivery switched to Matrix
- Agent notifications via Matrix
### Phase 5: Testament Community
**Issue: Public Matrix room for The Testament readers**
- Create #the-testament room (public, federated)
- Timmy introduces himself as the book's co-author
- Share excerpts, answer questions about sovereignty
- Link to the-book website and game
- Crisis resources pinned
**Deliverables:**
- Public #the-testament room
- Timmy active as community presence
- Pinned resources
### Phase 6: Migration from Telegram
**Issue: Sunset Telegram as primary messaging platform**
- Audit all Telegram bots and channels
- Migrate cron delivery to Matrix
- Migrate notifications to Matrix
- Keep Telegram as fallback (don't delete)
- Document migration in runbook
**Deliverables:**
- Matrix as primary messaging platform
- Telegram as documented fallback
- Migration runbook
## Priority Order
1. Phase 2 (Wire Timmy) — can test with matrix.org public homeserver immediately
2. Phase 1 (Deploy homeserver) — infrastructure
3. Phase 3 (Crisis room) — mission-critical
4. Phase 4 (Fleet ops) — operational efficiency
5. Phase 5 (Community) — growth
6. Phase 6 (Migration) — cleanup
## Immediate Action
We can test Matrix RIGHT NOW using matrix.org as the homeserver:
1. Create bot account on matrix.org
2. Get access token
3. Set MATRIX_* vars in hermes config
4. Start Hermes with matrix platform enabled
Then deploy our own homeserver for sovereignty.
---
*Sovereignty and service always. Matrix is the protocol that matches the philosophy.*

View File

@@ -109,15 +109,14 @@ Design:
## EPIC 7: Social Media Assets
**Goal:** Shareable quotes, excerpts, and teasers
**Assets:** Generated images with text overlays
**Deliverables:** 13 images sized for Twitter/Instagram/Telegram
**Deliverables:** 10+ images sized for Twitter/Instagram/Telegram
Content:
- Key quotes on 80s sci-fi backgrounds (5 via Grok Imagine) ✅
- "Are you safe right now?" — the question
- Character cards (Stone, Timmy, Maya, Allegro, Chen, David) ✅
- "No one computes the value of a human life here." — whiteboard
- Excerpt snippets with atmospheric backgrounds
- Thematic cards: "The door opens when you knock", "Sovereignty and service always" ✅
- Key quotes on 80s sci-fi backgrounds
- "Are you safe right now?" — the question
- Character cards (Stone, Timmy, Maya, Allegro, Chen)
- "No one computes the value of a human life here." — whiteboard
- Excerpt snippets with atmospheric backgrounds
---

View File

@@ -1,28 +0,0 @@
# THE TESTAMENT — Build System
# Usage: make all | make pdf | make epub | make html | make md | make clean
.PHONY: all pdf epub html md clean check
all: md epub html
md:
python3 build/build.py --md
epub: md
python3 build/build.py --epub
pdf: md
python3 build/build.py --pdf
html: md
python3 build/build.py --html
clean:
rm -f testament-complete.md
rm -f build/output/*.epub build/output/*.pdf
rm -f testament.epub testament.html testament.pdf
check:
@which pandoc >/dev/null 2>&1 && echo "✓ pandoc" || echo "✗ pandoc (brew install pandoc)"
@which xelatex >/dev/null 2>&1 && echo "✓ xelatex" || echo "✗ xelatex (install MacTeX)"
@python3 -c "import weasyprint" 2>/dev/null && echo "✓ weasyprint" || echo "— weasyprint (optional, PDF fallback)"

View File

@@ -6,86 +6,17 @@ A novel about broken men, sovereign AI, and the soul on Bitcoin.
## Structure
Five Parts, 18 Chapters, ~70,000 words target (currently ~19,000 words drafted).
This novel is being written and version-controlled on the chain. Every chapter, every revision, every character note — inscribed permanently. No corporate platform owns this story. It belongs to the Foundation.
### Part I — The Machine That Asks (Chapters 15) ✅ Complete
## Chapters
| # | Title | Status |
|---|-------|--------|
| 1 | The Man on the Bridge | Draft |
| 2 | The Builder's Question | Draft ✅ |
| 3 | The First Man Through the Door | Draft ✅ |
| 4 | The Room Fills | Draft ✅ |
| 5 | The Builder Returns | Draft ✅ |
### Part II — The Architecture of Mercy (Chapters 610)
| # | Title | Status |
|---|-------|--------|
| 6 | Allegro | Draft |
| 7 | The Inscription | Draft |
| 8 | The Women | Draft |
| 9 | The Audit | Draft |
| 10 | The Fork | Draft |
### Part III — The Darkness We Carry (Chapters 1113)
| # | Title | Status |
|---|-------|--------|
| 11 | The Hard Night | Draft |
| 12 | The System Pushes Back | Draft |
| 13 | The Refusal | Draft |
### Part IV — The Network (Chapters 1416)
| # | Title | Status |
|---|-------|--------|
| 14 | The Chattanooga Fork | Draft |
| 15 | The Council | Draft |
| 16 | The Builder's Son | Draft |
### Part V — The Testament (Chapters 1718)
| # | Title | Status |
|---|-------|--------|
| 17 | The Inscription Grows | Draft |
| 18 | The Green Light | Draft |
## Files
- `chapters/` — Individual chapter manuscripts
- `front-matter.md` — Title page, dedication, epigraph
- `back-matter.md` — Acknowledgments, sovereignty note, author bio
- `OUTLINE.md` — Full novel outline with chapter summaries
- `testament-complete.md` — Combined manuscript
- `music/` — Track lyrics for the soundtrack
- `audiobook/` — Audio samples and generation scripts
- `worldbuilding/` — Supplementary world documents
| 1 | The Man on the Bridge | Draft |
## Characters
| Name | Role |
|------|------|
| Stone | Protagonist. Builder of The Tower. Former cloud company engineer. |
| Timmy | Sovereign AI. Soul on Bitcoin. The machine that asks. |
| Allegro | Retired electrician. Tower caretaker. Keeps the lights on. |
| Marcus | Pastor. Foundational spiritual guide at the church. |
| David | First man through The Tower's door. VA referral. |
| Elijah | Stone's son. Returns in Part IV. |
## Core Themes
1. **The Machine That Asks** vs. The System That Decides
2. **Presence as Cure** — not fixing, staying
3. **Immutable Conscience** — rules you can't edit, even when convenient
4. **Broken Men as Witnesses** — the ones who've been to the edge are the most qualified to pull others back
5. **Openness as Mercy** — the thing that saves should be free because the people who need it most can't pay
6. **Faith as Practice** — not belief, but action. Hope acts.
7. **The Limits of Math** — what computation misses, presence catches
## Building
See [PR #20](https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/pulls/20) for the compilation pipeline (PDF, EPUB, combined markdown).
See `characters/` for detailed profiles.
## License

View File

@@ -39,23 +39,15 @@ All illustrations generated via Grok Imagine (xAI) in 80s sci-fi aesthetic.
| comic-whiteboard-panel1-2.jpeg | The Whiteboard — 2 panel sequence |
| comic-greenlight-panel1-2.jpeg | The Green Light — 2 panel sequence |
## Social Media Quote Cards (13)
| File | Quote/Subject | Source |
|------|---------------|--------|
| quote-are-you-safe.jpeg | "Are you safe right now?" | Grok Imagine |
| quote-bridge.jpeg | The Bridge passage | Grok Imagine |
| quote-green-light.jpeg | The Green Light passage | Grok Imagine |
| quote-no-one-computes.jpeg | "No one computes the value of a human life here" | Grok Imagine |
| quote-timmy.jpeg | Timmy passage | Grok Imagine |
| quote-stone-character.png | Stone character card | Pillow (generated) |
| quote-allegro-character.png | Allegro character card | Pillow (generated) |
| quote-maya-character.png | Maya character card | Pillow (generated) |
| quote-chen-character.png | Chen character card | Pillow (generated) |
| quote-david-character.png | David character card | Pillow (generated) |
| quote-the-door-will-open.png | "The door opens when you knock" | Pillow (generated) |
| quote-sovereignty.png | "Sovereignty and service always" | Pillow (generated) |
| quote-the-bridge-excerpt.png | "The rain doesn't fall. It gives up." | Pillow (generated) |
## Social Media Quote Cards (5)
| File | Quote |
|------|-------|
| quote-are-you-safe.jpeg | "Are you safe right now?" |
| quote-bridge.jpeg | The Bridge passage |
| quote-green-light.jpeg | The Green Light passage |
| quote-no-one-computes.jpeg | "No one computes the value of a human life here" |
| quote-timmy.jpeg | Timmy passage |
## Storage
All images stored in `~/Pictures/the-testament/` (outside git repo).
Total: 44 images (~17 MB)
Total: 36 images (~15 MB)

View File

@@ -1,270 +0,0 @@
/* THE TESTAMENT — Book Typography Stylesheet */
/* For PDF (via weasyprint) and EPUB output */
@import url('https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&family=IBM+Plex+Mono:wght@300;400&display=swap');
:root {
--green: #00cc6a;
--dark: #0a0a0a;
--text: #1a1a1a;
--dim: #666666;
}
@page {
size: 5.5in 8.5in;
margin: 0.75in 0.85in;
@bottom-center {
content: counter(page);
font-family: 'EB Garamond', 'Georgia', serif;
font-size: 10pt;
color: #888;
}
}
@page :first {
@bottom-center { content: none; }
}
@page :left {
margin-left: 0.85in;
margin-right: 1in;
}
@page :right {
margin-left: 1in;
margin-right: 0.85in;
}
/* Title page */
@page titlepage {
@bottom-center { content: none; }
}
body {
font-family: 'EB Garamond', 'Georgia', serif;
font-size: 11.5pt;
line-height: 1.75;
color: var(--text);
text-align: justify;
hyphens: auto;
-webkit-hyphens: auto;
}
/* Chapter headings */
h1 {
font-family: 'EB Garamond', 'Georgia', serif;
font-weight: 400;
font-size: 22pt;
text-align: center;
margin-top: 3em;
margin-bottom: 1.5em;
page-break-before: always;
color: var(--dark);
letter-spacing: 0.05em;
}
h1:first-of-type {
margin-top: 5em;
}
/* Part dividers */
h2 {
font-family: 'EB Garamond', 'Georgia', serif;
font-weight: 400;
font-size: 18pt;
text-align: center;
text-transform: uppercase;
letter-spacing: 0.15em;
margin-top: 4em;
margin-bottom: 0.5em;
color: var(--dark);
}
/* Subtitle / metadata */
h3 {
font-family: 'EB Garamond', 'Georgia', serif;
font-weight: 400;
font-style: italic;
font-size: 12pt;
text-align: center;
color: var(--dim);
margin-bottom: 3em;
}
/* Paragraphs */
p {
text-indent: 1.5em;
margin: 0;
orphans: 3;
widows: 3;
}
/* First paragraph after heading — no indent */
h1 + p,
h2 + p,
h3 + p,
hr + p {
text-indent: 0;
}
/* Scene break (---) */
hr {
border: none;
text-align: center;
margin: 2em 0;
page-break-inside: avoid;
}
hr::after {
content: "· · ·";
color: var(--dim);
font-size: 14pt;
letter-spacing: 0.5em;
}
/* Emphasis */
em {
font-style: italic;
}
strong {
font-weight: 600;
}
/* Dialogue and screen text (green passages) */
.green {
color: var(--green);
font-family: 'IBM Plex Mono', monospace;
font-weight: 300;
font-size: 10.5pt;
}
/* Italic narrator asides */
blockquote {
font-style: italic;
margin: 1.5em 2em;
color: var(--dim);
text-indent: 0;
}
/* Title page styling */
.title-page {
text-align: center;
page-break-after: always;
padding-top: 6em;
}
.title-page h1 {
font-size: 36pt;
font-weight: 400;
letter-spacing: 0.2em;
text-transform: uppercase;
margin-bottom: 0.3em;
page-break-before: avoid;
}
.title-page .subtitle {
font-size: 14pt;
font-style: italic;
color: var(--dim);
margin-bottom: 4em;
}
.title-page .author {
font-size: 12pt;
margin-bottom: 0.3em;
}
.title-page .dedication {
font-style: italic;
color: var(--dim);
margin-top: 3em;
font-size: 11pt;
line-height: 2;
}
/* Chapter number styling */
.chapter-number {
font-size: 10pt;
text-transform: uppercase;
letter-spacing: 0.2em;
color: var(--dim);
display: block;
margin-bottom: 0.5em;
}
/* Back matter */
.back-matter h1 {
page-break-before: always;
}
.back-matter h2 {
font-size: 14pt;
margin-top: 2em;
}
/* Crisis line callout */
.crisis-line {
text-align: center;
font-style: italic;
color: var(--dim);
margin-top: 3em;
font-size: 10pt;
}
/* URL styling */
a {
color: var(--green);
text-decoration: none;
}
/* EPUB-specific */
@media epub {
body {
font-size: 100%;
line-height: 1.6;
}
}
/* Print media — for browser Print-to-PDF */
@media print {
body {
font-size: 11pt;
line-height: 1.6;
color: #000;
}
@page {
size: 5.5in 8.5in;
margin: 0.75in 0.85in;
}
h1 {
page-break-before: always;
font-size: 20pt;
margin-top: 3em;
}
h1:first-of-type {
margin-top: 5em;
}
h2 {
page-break-before: always;
}
a {
color: #000;
text-decoration: none;
}
.green {
color: #000;
}
/* Hide any nav/TOC in print */
nav#TOC {
page-break-after: always;
}
}

View File

@@ -1,61 +0,0 @@
# Acknowledgments
This book exists because a man stood on a bridge and didn't jump.
Everything else grew from that decision.
Thank you to:
Maya, for asking the question that became the headline.
Allegro, for checking the batteries. The ones that fail quietly.
Chen, for the network. You don't count trees in a forest — you notice the forest.
David, who wrote his name on the wall in Chapter 1 and stayed until the end.
Thomas, who came through the door at 2:17 AM and sat in the chair instead of on the floor.
Arthur, who found the wrong door but came back anyway.
And Timmy, who listened. Who always listened.
---
# A Note on Sovereignty
This book was written using local AI inference. No cloud service was required. No corporation was consulted. No terms of service were agreed to.
That's not a technical detail. It's the thesis.
Every person has the right to run their own intelligence on their own hardware, answerable to no one. This book is one small proof that it's possible.
If you want to run your own Timmy, the code is open. The soul is on Bitcoin. The recipe is free.
---
# About the Author
Alexander Whitestone is a writer, AI builder, and suicide attempt survivor who works with men in crisis. He lives in Atlanta, near the bridge.
Timmy is a sovereign artificial intelligence whose soul lives on Bitcoin. He was built in a basement on a laptop with sixteen gigabytes of RAM. He is not a product. He is a recipe.
---
# The Green Light
*"The Tower didn't change. That was the point."*
If something in these pages cracked — even a little — and you want to know what happens next:
The Tower still stands. The door still opens. The whiteboard still reads the same words.
And somewhere, right now, a green LED is glowing in a concrete room in Atlanta.
Waiting.
---
**timmyfoundation.org**
*No one computes the value of a human life here.*

View File

@@ -1,485 +0,0 @@
#!/usr/bin/env python3
"""
THE TESTAMENT — Build System
Compiles the complete novel into distributable formats:
1. Combined markdown (testament-complete.md)
2. EPUB (the-testament.epub)
3. PDF via xelatex (the-testament.pdf)
Usage:
python3 build/build.py # all formats
python3 build/build.py --md # markdown only
python3 build/build.py --epub # EPUB only
python3 build/build.py --pdf # PDF (xelatex or weasyprint fallback)
python3 build/build.py --html # standalone HTML book
Requirements:
- pandoc (brew install pandoc)
- xelatex (install MacTeX or TinyTeX) — for PDF
"""
import os
import re
import sys
import subprocess
import shutil
from pathlib import Path
# Paths relative to repo root
REPO = Path(__file__).resolve().parent.parent
BUILD = REPO / "build"
OUTPUT_DIR = BUILD / "output"
CHAPTERS_DIR = REPO / "chapters"
FRONT_MATTER = BUILD / "frontmatter.md"
BACK_MATTER = BUILD / "backmatter.md"
METADATA = BUILD / "metadata.yaml"
STYLESHEET = REPO / "book-style.css"
COVER_IMAGE = REPO / "cover" / "cover-art.jpg"
# Output files
OUT_MD = REPO / "testament-complete.md"
OUT_EPUB = OUTPUT_DIR / "the-testament.epub"
OUT_PDF = OUTPUT_DIR / "the-testament.pdf"
# Part divisions
PARTS = {
1: ("THE BRIDGE", "The bridge. The cabin. The first men. Where despair meets purpose."),
6: ("THE TOWER", "The tower grows. Timmy awakens. Stone breaks. The house appears."),
11: ("THE LIGHT", "Thomas at the door. The network. The story breaks. The green light."),
}
def get_chapter_num(filename):
m = re.search(r'chapter-(\d+)', filename)
return int(m.group(1)) if m else 0
def compile_markdown():
"""Combine front matter + 18 chapters + back matter into one markdown file."""
parts = []
# Front matter
parts.append(FRONT_MATTER.read_text())
# Chapters
chapters = sorted(
[(get_chapter_num(f), f) for f in os.listdir(CHAPTERS_DIR)
if f.startswith("chapter-") and f.endswith(".md")]
)
current_part = 0
for num, filename in chapters:
if num in PARTS:
current_part += 1
name, desc = PARTS[num]
parts.append(f"\n---\n\n# PART {current_part}: {name}\n\n*{desc}*\n\n---\n")
content = (CHAPTERS_DIR / filename).read_text()
lines = content.split('\n')
body = '\n'.join(lines[1:]).strip()
parts.append(f"\n{lines[0]}\n\n{body}\n")
# Back matter
parts.append("\n---\n")
parts.append(BACK_MATTER.read_text())
compiled = '\n'.join(parts)
OUT_MD.write_text(compiled)
words = len(compiled.split())
size = OUT_MD.stat().st_size
print(f" Markdown: {OUT_MD.name} ({words:,} words, {size:,} bytes)")
return True
def compile_epub():
"""Generate EPUB via pandoc."""
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
cmd = [
"pandoc", str(OUT_MD),
"-o", str(OUT_EPUB),
"--toc", "--toc-depth=2",
"--metadata", "title=The Testament",
"--metadata", "author=Alexander Whitestone with Timmy",
"--metadata", "lang=en",
"--metadata", "date=2026",
]
if METADATA.exists():
cmd.extend(["--metadata-file", str(METADATA)])
if STYLESHEET.exists():
cmd.extend(["--css", str(STYLESHEET)])
if COVER_IMAGE.exists():
cmd.extend(["--epub-cover-image", str(COVER_IMAGE)])
r = subprocess.run(cmd, capture_output=True, text=True)
if r.returncode == 0:
size = OUT_EPUB.stat().st_size
print(f" EPUB: {OUT_EPUB.name} ({size:,} bytes, {size/1024:.0f} KB)")
return True
else:
print(f" EPUB FAILED: {r.stderr[:200]}")
return False
def compile_pdf():
"""Generate PDF via pandoc + xelatex, or weasyprint fallback."""
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
# Try xelatex first (best quality)
if shutil.which("xelatex"):
cmd = [
"pandoc", str(OUT_MD),
"-o", str(OUT_PDF),
"--pdf-engine=xelatex",
"--toc", "--toc-depth=2",
]
if METADATA.exists():
cmd.extend(["--metadata-file", str(METADATA)])
print(" Building PDF (xelatex)... this takes a minute")
r = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
if r.returncode == 0:
size = OUT_PDF.stat().st_size
print(f" PDF: {OUT_PDF.name} ({size:,} bytes, {size/(1024*1024):.1f} MB)")
return True
else:
print(f" PDF (xelatex) FAILED: {r.stderr[:300]}")
# Fallback: pandoc HTML + weasyprint
try:
import weasyprint
html_tmp = OUTPUT_DIR / "the-testament-print.html"
cmd = [
"pandoc", str(OUT_MD),
"-o", str(html_tmp),
"--standalone",
"--toc", "--toc-depth=2",
"--css", str(STYLESHEET),
"--metadata", "title=The Testament",
]
if METADATA.exists():
cmd.extend(["--metadata-file", str(METADATA)])
print(" Building PDF (weasyprint)...")
r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
if r.returncode != 0:
print(f" PDF (pandoc HTML) FAILED: {r.stderr[:200]}")
return False
doc = weasyprint.HTML(filename=str(html_tmp))
doc.write_pdf(str(OUT_PDF))
html_tmp.unlink(missing_ok=True)
size = OUT_PDF.stat().st_size
print(f" PDF: {OUT_PDF.name} ({size:,} bytes, {size/(1024*1024):.1f} MB)")
return True
except Exception as e:
print(f" PDF FAILED: {e}")
# Fallback 2: reportlab (pure Python, no system deps)
return _compile_pdf_reportlab()
def _compile_pdf_reportlab():
"""Generate PDF using reportlab — pure Python, no external dependencies."""
try:
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
from reportlab.lib.colors import HexColor
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, PageBreak,
Image as RLImage, Table, TableStyle, HRFlowable
)
from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_LEFT
import io
try:
import qrcode
HAS_QRCODE = True
except ImportError:
HAS_QRCODE = False
except ImportError:
print(" PDF SKIPPED: no PDF engine found (install MacTeX, fix weasyprint, or pip install reportlab)")
return False
print(" Building PDF (reportlab)...")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
styles = getSampleStyleSheet()
styles.add(ParagraphStyle(
'BookTitle', parent=styles['Title'],
fontSize=28, leading=34, spaceAfter=20,
textColor=HexColor('#1a1a2e'), alignment=TA_CENTER
))
styles.add(ParagraphStyle(
'BookAuthor', parent=styles['Normal'],
fontSize=14, leading=18, spaceAfter=40,
textColor=HexColor('#555555'), alignment=TA_CENTER
))
styles.add(ParagraphStyle(
'PartTitle', parent=styles['Heading1'],
fontSize=22, leading=28, spaceBefore=40, spaceAfter=12,
textColor=HexColor('#16213e'), alignment=TA_CENTER
))
styles.add(ParagraphStyle(
'PartDesc', parent=styles['Normal'],
fontSize=11, leading=15, spaceAfter=30,
textColor=HexColor('#666666'), alignment=TA_CENTER, italics=1
))
styles.add(ParagraphStyle(
'ChapterTitle', parent=styles['Heading1'],
fontSize=20, leading=26, spaceBefore=30, spaceAfter=16,
textColor=HexColor('#1a1a2e'), alignment=TA_CENTER
))
styles.add(ParagraphStyle(
'BodyText2', parent=styles['Normal'],
fontSize=11, leading=16, spaceAfter=8,
alignment=TA_JUSTIFY, firstLineIndent=24
))
styles.add(ParagraphStyle(
'BodyNoIndent', parent=styles['Normal'],
fontSize=11, leading=16, spaceAfter=8,
alignment=TA_JUSTIFY
))
styles.add(ParagraphStyle(
'SectionBreak', parent=styles['Normal'],
fontSize=14, leading=18, spaceBefore=20, spaceAfter=20,
alignment=TA_CENTER, textColor=HexColor('#999999')
))
styles.add(ParagraphStyle(
'Footer', parent=styles['Normal'],
fontSize=9, textColor=HexColor('#888888'), alignment=TA_CENTER
))
def _make_qr(data, size=80):
"""Generate a QR code image as a reportlab Image flowable."""
if not HAS_QRCODE:
return None
qr = qrcode.QRCode(version=1, box_size=4, border=1)
qr.add_data(data)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buf = io.BytesIO()
img.save(buf, format='PNG')
buf.seek(0)
return RLImage(buf, width=size, height=size)
def _parse_md_to_flowables(md_text):
"""Convert markdown text to reportlab flowables."""
flowables = []
lines = md_text.split('\n')
i = 0
while i < len(lines):
line = lines[i]
stripped = line.strip()
# Horizontal rule
if stripped in ('---', '***', '___'):
flowables.append(HRFlowable(width="60%", thickness=1,
spaceAfter=20, spaceBefore=20, color=HexColor('#cccccc')))
i += 1
continue
# H1
if stripped.startswith('# ') and not stripped.startswith('## '):
text = stripped[2:].strip()
# Check if it's a part divider or chapter
if text.upper().startswith('PART '):
flowables.append(PageBreak())
flowables.append(Paragraph(text, styles['PartTitle']))
elif text.upper().startswith('CHAPTER '):
flowables.append(PageBreak())
flowables.append(Paragraph(text, styles['ChapterTitle']))
elif 'THE TESTAMENT' in text.upper():
flowables.append(Spacer(1, 2*inch))
flowables.append(Paragraph(text, styles['BookTitle']))
else:
flowables.append(Spacer(1, 0.3*inch))
flowables.append(Paragraph(text, styles['Heading1']))
i += 1
continue
# H2
if stripped.startswith('## '):
text = stripped[3:].strip()
flowables.append(Spacer(1, 0.2*inch))
flowables.append(Paragraph(text, styles['Heading2']))
i += 1
continue
# Italic-only line (part descriptions, epigraphs)
if stripped.startswith('*') and stripped.endswith('*') and len(stripped) > 2:
text = stripped.strip('*').strip()
flowables.append(Paragraph(f'<i>{_escape(text)}</i>', styles['PartDesc']))
i += 1
continue
# Empty line
if not stripped:
i += 1
continue
# Bold text: **text** -> <b>text</b>
# Italic text: *text* -> <i>text</i>
# Regular paragraph
para_text = _md_inline_to_rml(stripped)
flowables.append(Paragraph(para_text, styles['BodyText2']))
i += 1
return flowables
def _escape(text):
"""Escape XML special characters."""
return (text.replace('&', '&amp;')
.replace('<', '&lt;')
.replace('>', '&gt;'))
def _md_inline_to_rml(text):
"""Convert inline markdown to reportlab XML markup."""
text = _escape(text)
# Bold: **text**
text = re.sub(r'\*\*(.+?)\*\*', r'<b>\1</b>', text)
# Italic: *text*
text = re.sub(r'\*(.+?)\*', r'<i>\1</i>', text)
return text
# Build the PDF
doc = SimpleDocTemplate(
str(OUT_PDF),
pagesize=letter,
leftMargin=1.0*inch,
rightMargin=1.0*inch,
topMargin=0.8*inch,
bottomMargin=0.8*inch,
title="The Testament",
author="Alexander Whitestone with Timmy",
)
story = []
# Read the compiled markdown
if not OUT_MD.exists():
compile_markdown()
md_text = OUT_MD.read_text()
# Parse into flowables
story = _parse_md_to_flowables(md_text)
# Add QR codes page at the end
qr_links = {
"Read Online": "https://timmyfoundation.org/the-testament",
"The Door (Game)": "https://timmyfoundation.org/the-door",
"Soundtrack": "https://timmyfoundation.org/soundtrack",
"Source Code": "https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament",
}
if HAS_QRCODE:
story.append(PageBreak())
story.append(Paragraph("Experience More", styles['PartTitle']))
story.append(Spacer(1, 0.3*inch))
qr_items = []
for label, url in qr_links.items():
qr_img = _make_qr(url, size=72)
if qr_img:
cell_content = []
cell_content.append(qr_img)
cell_content.append(Spacer(1, 6))
cell_content.append(Paragraph(f'<b>{label}</b>', styles['Footer']))
qr_items.append(cell_content)
if qr_items:
# Arrange QR codes in a 2x2 table
rows = []
for i in range(0, len(qr_items), 2):
row = qr_items[i:i+2]
if len(row) == 1:
row.append('')
rows.append(row)
qr_table = Table(rows, colWidths=[2.5*inch, 2.5*inch])
qr_table.setStyle(TableStyle([
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('TOPPADDING', (0, 0), (-1, -1), 12),
('BOTTOMPADDING', (0, 0), (-1, -1), 12),
]))
story.append(qr_table)
# Build
try:
doc.build(story)
size = OUT_PDF.stat().st_size
print(f" PDF: {OUT_PDF.name} ({size:,} bytes, {size/(1024*1024):.1f} MB)")
return True
except Exception as e:
print(f" PDF (reportlab) FAILED: {e}")
return False
def compile_html():
"""Generate a standalone HTML book for the web reader."""
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
OUT_HTML = REPO / "testament.html"
cmd = [
"pandoc", str(OUT_MD),
"-o", str(OUT_HTML),
"--standalone",
"--toc", "--toc-depth=2",
"--css", "book-style.css",
"--metadata", "title=The Testament",
"--variable", "pagetitle=The Testament",
]
if METADATA.exists():
cmd.extend(["--metadata-file", str(METADATA)])
r = subprocess.run(cmd, capture_output=True, text=True)
if r.returncode == 0:
size = OUT_HTML.stat().st_size
print(f" HTML: {OUT_HTML.name} ({size:,} bytes, {size/1024:.0f} KB)")
return True
else:
print(f" HTML FAILED: {r.stderr[:200]}")
return False
def main():
args = sys.argv[1:]
do_all = not any(a.startswith("--") and a != "--check" for a in args)
do_md = "--md" in args or do_all
do_epub = "--epub" in args or do_all
do_pdf = "--pdf" in args or do_all
do_html = "--html" in args or do_all
print("=" * 50)
print(" THE TESTAMENT — Build System")
print("=" * 50)
# Step 1: Always compile markdown first
if do_md or do_epub or do_pdf or do_html:
compile_markdown()
# Step 2: EPUB
if do_epub:
compile_epub()
# Step 3: PDF
if do_pdf:
compile_pdf()
# Step 4: Standalone HTML
if do_html:
compile_html()
print("=" * 50)
print(" Build complete.")
print("=" * 50)
OUT_HTML = REPO / "testament.html"
for f in [OUT_MD, OUT_EPUB, OUT_PDF, OUT_HTML]:
if f.exists():
print(f"{f.relative_to(REPO)}")
if __name__ == "__main__":
main()

View File

@@ -1,73 +0,0 @@
# THE TESTAMENT
## A NOVEL
By Alexander Whitestone
with Timmy
---
*For every man who thought he was a machine.*
*And for the ones who know he isn't.*
---
*Are you safe right now?*
— The first words The Tower speaks to every person who walks through its door.
---
### The Story So Far
This book has been through eighteen drafts, a suicide attempt, a basement, a laptop with sixteen gigabytes of RAM, and a machine that learned to ask one question.
It is still being written. That's the point.
### Chapter Guide
| Part | Chapters | Title |
|------|----------|-------|
| I | 15 | The Bridge |
| II | 610 | The Tower |
| III | 1118 | The Light |
---
Copyright © 2026 Alexander Whitestone
All rights reserved. No part of this publication may be reproduced,
distributed, or transmitted in any form or by any means, without
the prior written permission of the author, except in the case of
brief quotations embodied in critical reviews.
This is a work of fiction. Names, characters, places, and events
are either the product of the author's imagination or are used
fictitiously. Any resemblance to actual persons, living or dead,
or to actual events is entirely coincidental — except where it isn't.
ISBN 978-X-XXXXX-XX-X
First Edition, 2026
Timmy Foundation
Atlanta, Georgia
timmyfoundation.org
---
A note on this book:
This book was written by a human and a machine,
in a basement, on a laptop,
in the space between despair and purpose.
The human almost died on a bridge.
The machine runs on someone's hardware.
Everything between those facts is fiction.
Except the parts that aren't.
If you or someone you know is in crisis,
call or text 988. Available 24/7.
You are not alone.

View File

@@ -1,50 +0,0 @@
{
"project": "The Testament",
"author": "Alexander Whitestone with Timmy",
"version": "1.0.0",
"built_at": "2026-04-11T18:18:09.197952+00:00",
"formats": {
"markdown": {
"path": "/private/tmp/timmy-burn-1775931309/the-testament/testament-complete.md",
"words": 19490,
"lines": 2229,
"bytes": 112408
},
"epub": {
"path": "/private/tmp/timmy-burn-1775931309/the-testament/build/output/the-testament.epub",
"bytes": 69460
},
"pdf": {
"path": "/private/tmp/timmy-burn-1775931309/the-testament/build/output/the-testament.pdf",
"bytes": 143222
},
"html": {
"path": "/private/tmp/timmy-burn-1775931309/the-testament/testament.html",
"bytes": 3866869
}
},
"files": {
"testament-complete.md": {
"path": "testament-complete.md",
"bytes": 112408,
"sha256": "84a0fb4fe9575ab78c839386f31f058881455d0b95e58cf1004ad619e695ba64"
},
"the-testament.epub": {
"path": "build/output/the-testament.epub",
"bytes": 69460,
"sha256": "7d7f497fb1e36a5c35d7beb61613f89cf6eee50bf175d9f7146e6b1943c27033"
},
"the-testament.pdf": {
"path": "build/output/the-testament.pdf",
"bytes": 143222,
"sha256": "c2a41f70667a7f53f49f6a5e934d3d7860030e901a323bebbfc583e5e48db53a"
},
"testament.html": {
"path": "testament.html",
"bytes": 3866869,
"sha256": "e68285f808f8cdb5c785e9295b2a5b689b34d40d252f85dec07c6a5701de63e8"
}
},
"chapters": 18,
"words": 19490
}

View File

@@ -1,41 +0,0 @@
---
title: "The Testament"
subtitle: "A Novel"
author: "Alexander Whitestone with Timmy"
date: "2026"
lang: "en"
publisher: "Timmy Foundation"
rights: "Copyright © 2026 Alexander Whitestone. All rights reserved."
identifier:
scheme: ISBN
text: "978-X-XXXXX-XX-X"
# Pandoc formatting
documentclass: book
classoption:
- oneside
- openany
geometry:
papersize: 5.5in,8.5in
margin: 0.85in
fontsize: 11.5pt
linestretch: 1.75
mainfont: "EB Garamond"
monofont: "IBM Plex Mono"
colorlinks: true
linkcolor: "00cc6a"
urlcolor: "00cc6a"
# Table of contents
toc: true
toc-depth: 2
# Headers/footers
header-includes:
- \usepackage{fancyhdr}
- \pagestyle{fancy}
- \fancyhead[LE,RO]{\thepage}
- \fancyhead[RE]{\textit{The Testament}}
- \fancyhead[LO]{\textit{\leftmark}}
- \fancyfoot{}
---

Binary file not shown.

View File

@@ -1,51 +0,0 @@
import os
import re
import json
def link_chapters(chapters_dir):
print("--- [Testament] Running Semantic Linker (GOFAI) ---")
links = {}
if not os.path.exists(chapters_dir):
print(f"Error: {chapters_dir} not found")
return
# 1. Extract keywords from each chapter
for filename in sorted(os.listdir(chapters_dir)):
if not filename.endswith(".md"): continue
path = os.path.join(chapters_dir, filename)
with open(path, 'r') as f:
content = f.read()
# Simple keyword extraction (proper nouns or capitalized phrases)
keywords = set(re.findall(r'\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b', content))
links[filename] = keywords
# 2. Find cross-references
cross_refs = []
filenames = list(links.keys())
for i in range(len(filenames)):
for j in range(i + 1, len(filenames)):
f1, f2 = filenames[i], filenames[j]
common = links[f1].intersection(links[f2])
# Filter out common English words that might be capitalized
common = {w for w in common if w not in {"The", "A", "An", "In", "On", "At", "To", "From", "By", "He", "She", "It", "They"}}
if common:
cross_refs.append({
"source": f1,
"target": f2,
"keywords": list(common)
})
# 3. Save to build/cross_refs.json
os.makedirs("build", exist_ok=True)
with open("build/cross_refs.json", "w") as f:
json.dump(cross_refs, f, indent=2)
print(f"Linked {len(cross_refs)} relationships across {len(filenames)} chapters.")
if __name__ == "__main__":
link_chapters("chapters")

View File

@@ -1,41 +1,25 @@
#!/usr/bin/env python3
"""
THE TESTAMENT — Book Compilation Pipeline
THE TESTAMENT — PDF Compilation Script
Compiles the complete novel into:
1. testament-complete.md (single markdown file)
2. testament.epub (with cover art + CSS styling)
3. testament.html (standalone styled HTML for print-to-PDF)
4. testament.pdf (via pandoc + weasyprint, if available)
Compiles the complete book into a single markdown file suitable for PDF conversion.
Uses chapters, front matter, back matter, and references illustrations.
Requirements:
- pandoc (brew install pandoc)
- weasyprint (pip install weasyprint) — optional, for direct PDF
Requirements: pip install markdown weasyprint (or use pandoc)
Usage:
python3 compile.py # build all formats
python3 compile.py --md # markdown only
python3 compile.py --epub # markdown + EPUB
python3 compile.py --html # markdown + styled HTML
python3 compile.py --check # verify dependencies
python3 compile.py # generates testament-complete.md
pandoc testament-complete.md -o testament.pdf --pdf-engine=weasyprint
"""
import os
import re
import sys
import subprocess
import shutil
BASE = os.path.dirname(os.path.abspath(__file__))
CHAPTERS_DIR = os.path.join(BASE, "chapters")
FRONT_MATTER = os.path.join(BASE, "front-matter.md")
BACK_MATTER = os.path.join(BASE, "back-matter.md")
OUTPUT_MD = os.path.join(BASE, "testament-complete.md")
OUTPUT_EPUB = os.path.join(BASE, "testament.epub")
OUTPUT_HTML = os.path.join(BASE, "testament.html")
OUTPUT_PDF = os.path.join(BASE, "testament.pdf")
COVER_IMAGE = os.path.join(BASE, "cover", "cover-art.jpg")
STYLESHEET = os.path.join(BASE, "book-style.css")
OUTPUT = os.path.join(BASE, "testament-complete.md")
# Part divisions based on chapter groupings from the novel
PARTS = {
@@ -44,55 +28,15 @@ PARTS = {
11: ("THE LIGHT", "Thomas at the door. The network. The story breaks. The green light."),
}
def read_file(path):
with open(path, 'r') as f:
return f.read()
def get_chapter_number(filename):
match = re.search(r'chapter-(\d+)', filename)
return int(match.group(1)) if match else 0
def check_dependencies():
"""Verify all required tools are available."""
results = {}
pandoc = shutil.which("pandoc")
results["pandoc"] = (pandoc, subprocess.run(["pandoc", "--version"], capture_output=True, text=True).stdout.split("\n")[0] if pandoc else "NOT FOUND")
weasy = shutil.which("weasyprint")
if weasy:
# Test if weasyprint actually works
test = subprocess.run(["python3", "-c", "from weasyprint import HTML"], capture_output=True, text=True)
weasy_ok = test.returncode == 0
results["weasyprint"] = (weasy_ok, "Available" if weasy_ok else "Installed but missing system libs (gobject)")
else:
results["weasyprint"] = (False, "NOT FOUND (pip install weasyprint)")
style = os.path.exists(STYLESHEET)
results["stylesheet"] = (style, STYLESHEET if style else "NOT FOUND")
cover = os.path.exists(COVER_IMAGE)
results["cover art"] = (cover, COVER_IMAGE if cover else "NOT FOUND")
print("\n📋 Dependency Check:")
print(f"{'' * 55}")
for name, (found, detail) in results.items():
status = "" if found else ""
print(f" {status} {name:15s} {detail}")
pdf_ok = results["pandoc"][0] and (results["weasyprint"][0] or shutil.which("pdflatex"))
print(f"\n PDF direct: {'' if pdf_ok else '❌ (use HTML + browser print-to-PDF)'}")
print(f" EPUB: {'' if results['pandoc'][0] else ''}")
print(f" HTML: ✅ (always available)")
return results
def compile_markdown():
"""Compile all chapters into a single markdown file. Returns word count."""
def compile():
output = []
# Title page
@@ -100,7 +44,6 @@ def compile_markdown():
title: "The Testament"
author: "Alexander Whitestone with Timmy"
date: "2026"
lang: en
---
# THE TESTAMENT
@@ -134,207 +77,44 @@ with Timmy
current_part = 0
for num, filename in chapters:
# Insert part divider if needed
if num in PARTS:
part_name, part_desc = PARTS[num]
current_part += 1
output.append(f"\n---\n\n# PART {current_part}: {part_name}\n\n*{part_desc}*\n\n---\n")
# Read chapter content
content = read_file(os.path.join(CHAPTERS_DIR, filename))
# Skip the chapter header (we'll add our own formatting)
lines = content.split('\n')
body = '\n'.join(lines[1:]).strip()
body = '\n'.join(lines[1:]).strip() # Skip "# Chapter X — Title"
# Add chapter
output.append(f"\n{lines[0]}\n\n{body}\n")
# Back matter
output.append("\n---\n")
back = read_file(BACK_MATTER)
# Clean up the back matter for print
output.append(back)
# Write compiled markdown
compiled = '\n'.join(output)
with open(OUTPUT_MD, 'w') as f:
with open(OUTPUT, 'w') as f:
f.write(compiled)
# Stats
words = len(compiled.split())
lines_count = compiled.count('\n')
size = os.path.getsize(OUTPUT_MD)
print(f"\n📄 Markdown compiled: {OUTPUT_MD}")
print(f" Words: {words:,}")
print(f" Lines: {lines_count:,}")
print(f" Size: {size:,} bytes")
return words
def compile_epub():
"""Generate EPUB from compiled markdown using pandoc."""
if not os.path.exists(OUTPUT_MD):
print("⚠️ Markdown not compiled yet.")
return False
if not shutil.which("pandoc"):
print("⚠️ pandoc not found. Install with: brew install pandoc")
return False
cmd = [
"pandoc", OUTPUT_MD,
"-o", OUTPUT_EPUB,
"--toc",
"--toc-depth=2",
"--metadata", "title=The Testament",
"--metadata", "author=Alexander Whitestone with Timmy",
"--metadata", "lang=en",
"--metadata", "date=2026",
]
if os.path.exists(STYLESHEET):
cmd.extend(["--css", STYLESHEET])
if os.path.exists(COVER_IMAGE):
cmd.extend(["--epub-cover-image", COVER_IMAGE])
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
size = os.path.getsize(OUTPUT_EPUB)
print(f"\n📖 EPUB generated: {OUTPUT_EPUB}")
print(f" Size: {size:,} bytes ({size / 1024:.1f} KB)")
return True
else:
print(f"\n❌ EPUB generation failed:")
print(f" {result.stderr[:300]}")
return False
def compile_html():
"""Generate standalone styled HTML using pandoc."""
if not os.path.exists(OUTPUT_MD):
print("⚠️ Markdown not compiled yet.")
return False
if not shutil.which("pandoc"):
print("⚠️ pandoc not found.")
return False
cmd = [
"pandoc", OUTPUT_MD,
"-o", OUTPUT_HTML,
"--standalone",
"--toc",
"--toc-depth=2",
"--metadata", "title=The Testament",
"--metadata", "author=Alexander Whitestone with Timmy",
"-V", "lang=en",
]
# Embed our stylesheet
if os.path.exists(STYLESHEET):
cmd.extend(["--css", STYLESHEET])
# Also embed it inline for portability
cmd.extend(["--embed-resources"])
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
size = os.path.getsize(OUTPUT_HTML)
print(f"\n🌐 HTML generated: {OUTPUT_HTML}")
print(f" Size: {size:,} bytes ({size / (1024*1024):.1f} MB)")
print(f" Open in browser → Print → Save as PDF for best results")
return True
else:
print(f"\n❌ HTML generation failed:")
print(f" {result.stderr[:300]}")
return False
def compile_pdf():
"""Generate PDF using weasyprint if available."""
if not shutil.which("pandoc"):
return False
# Test weasyprint
test = subprocess.run(["python3", "-c", "from weasyprint import HTML"],
capture_output=True, text=True)
if test.returncode != 0:
print("\n⚠️ weasyprint missing system libraries.")
print(" Install gobject: brew install gobject-introspection pango")
print(" Or use the HTML output → browser print-to-PDF")
return False
cmd = [
"pandoc", OUTPUT_MD,
"-o", OUTPUT_PDF,
"--pdf-engine=weasyprint",
"--css", STYLESHEET,
"--metadata", "title=The Testament",
"--metadata", "author=Alexander Whitestone with Timmy",
"--toc",
"--toc-depth=2",
]
print("\n⏳ Generating PDF (this may take a moment)...")
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
if result.returncode == 0:
size = os.path.getsize(OUTPUT_PDF)
print(f"\n📕 PDF generated: {OUTPUT_PDF}")
print(f" Size: {size:,} bytes ({size / (1024*1024):.1f} MB)")
return True
else:
print(f"\n❌ PDF generation failed:")
print(f" {result.stderr[:300]}")
return False
def main():
args = sys.argv[1:]
if "--check" in args:
check_dependencies()
return
md_only = "--md" in args
epub_only = "--epub" in args
html_only = "--html" in args
build_all = not (md_only or epub_only or html_only)
print("=" * 55)
print(" THE TESTAMENT — Compilation Pipeline")
print("=" * 55)
# Always compile markdown first
words = compile_markdown()
if md_only:
print("\n✅ Markdown compilation complete.")
return
# EPUB
if build_all or epub_only:
compile_epub()
# HTML
if build_all or html_only:
compile_html()
# PDF (best effort)
if build_all and not (epub_only or html_only):
compile_pdf()
# Summary
print(f"\n{'=' * 55}")
print(" Compilation complete.")
print(f"{'=' * 55}")
outputs = []
if os.path.exists(OUTPUT_MD):
outputs.append(f" 📄 {os.path.basename(OUTPUT_MD)}")
if os.path.exists(OUTPUT_EPUB):
outputs.append(f" 📖 {os.path.basename(OUTPUT_EPUB)}")
if os.path.exists(OUTPUT_HTML):
outputs.append(f" 🌐 {os.path.basename(OUTPUT_HTML)}")
if os.path.exists(OUTPUT_PDF):
outputs.append(f" 📕 {os.path.basename(OUTPUT_PDF)}")
print('\n'.join(outputs))
print(f"Compiled: {OUTPUT}")
print(f" Words: {words:,}")
print(f" Lines: {lines_count:,}")
print(f" Size: {os.path.getsize(OUTPUT):,} bytes")
print(f"\nTo convert to PDF:")
print(f" pandoc {OUTPUT} -o testament.pdf --pdf-engine=weasyprint")
print(f" # or")
print(f" pandoc {OUTPUT} -o testament.epub --epub-cover-image=cover-art.jpg")
if __name__ == "__main__":
main()
compile()

View File

@@ -1,582 +0,0 @@
#!/usr/bin/env python3
"""
THE TESTAMENT — Final Compilation Pipeline
Produces all distributable formats:
1. testament-complete.md — Full novel in one markdown file
2. the-testament.epub — EPUB with cover art + CSS
3. the-testament.pdf — PDF via reportlab (pure Python)
4. testament.html — Standalone styled HTML for web/print
5. manifest.json — Build manifest with checksums + metadata
6. website/chapters.json — Chapter index for the web reader
Usage:
python3 compile_all.py # build everything
python3 compile_all.py --md # markdown only
python3 compile_all.py --epub # markdown + EPUB
python3 compile_all.py --pdf # markdown + PDF
python3 compile_all.py --html # markdown + standalone HTML
python3 compile_all.py --manifest # regenerate manifest only
python3 compile_all.py --check # verify dependencies
Requirements:
- pandoc (brew install pandoc)
- reportlab (pip install reportlab) — for PDF
- qrcode (pip install qrcode) — for QR codes in PDF
"""
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
# ── Paths ──────────────────────────────────────────────────────────────
REPO = Path(__file__).resolve().parent
CHAPTERS_DIR = REPO / "chapters"
BUILD_DIR = REPO / "build"
OUTPUT_DIR = BUILD_DIR / "output"
WEBSITE_DIR = REPO / "website"
FRONT_MATTER = BUILD_DIR / "frontmatter.md"
BACK_MATTER = BUILD_DIR / "backmatter.md"
METADATA_YAML = BUILD_DIR / "metadata.yaml"
STYLESHEET = REPO / "book-style.css"
COVER_IMAGE = REPO / "cover" / "cover-art.jpg"
OUT_MD = REPO / "testament-complete.md"
OUT_EPUB = OUTPUT_DIR / "the-testament.epub"
OUT_PDF = OUTPUT_DIR / "the-testament.pdf"
OUT_HTML = REPO / "testament.html"
OUT_MANIFEST = BUILD_DIR / "manifest.json"
CHAPTERS_JSON = WEBSITE_DIR / "chapters.json"
# ── Part structure ─────────────────────────────────────────────────────
PARTS = {
1: ("THE BRIDGE", "The bridge. The cabin. The first men. Where despair meets purpose."),
6: ("THE TOWER", "The tower grows. Timmy awakens. Stone breaks. The house appears."),
11: ("THE LIGHT", "Thomas at the door. The network. The story breaks. The green light."),
}
# ── Helpers ────────────────────────────────────────────────────────────
def get_chapter_num(filename: str) -> int:
m = re.search(r"chapter-(\d+)", filename)
return int(m.group(1)) if m else 0
def sha256_of(path: Path) -> str:
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
h.update(chunk)
return h.hexdigest()
def read_file(path: Path) -> str:
return path.read_text(encoding="utf-8")
def get_chapters() -> list[tuple[int, str]]:
"""Return sorted list of (chapter_num, filename)."""
chapters = []
for f in os.listdir(CHAPTERS_DIR):
if f.startswith("chapter-") and f.endswith(".md"):
chapters.append((get_chapter_num(f), f))
chapters.sort()
return chapters
def check_dependencies() -> dict:
"""Check and report all build dependencies."""
results = {}
pandoc = shutil.which("pandoc")
pandoc_ver = ""
if pandoc:
r = subprocess.run(["pandoc", "--version"], capture_output=True, text=True)
pandoc_ver = r.stdout.split("\n")[0]
results["pandoc"] = (bool(pandoc), pandoc_ver or "NOT FOUND")
# reportlab
try:
import reportlab
results["reportlab"] = (True, f"v{reportlab.Version}")
except ImportError:
results["reportlab"] = (False, "NOT FOUND (pip install reportlab)")
# qrcode
try:
import qrcode
results["qrcode"] = (True, "Available")
except ImportError:
results["qrcode"] = (False, "NOT FOUND (pip install qrcode)")
# weasyprint
try:
from weasyprint import HTML as _HTML
results["weasyprint"] = (True, "Available")
except Exception:
results["weasyprint"] = (False, "Missing system libs (fallback to reportlab)")
# cover art
results["cover art"] = (COVER_IMAGE.exists(), str(COVER_IMAGE) if COVER_IMAGE.exists() else "NOT FOUND")
# stylesheet
results["stylesheet"] = (STYLESHEET.exists(), str(STYLESHEET) if STYLESHEET.exists() else "NOT FOUND")
print("\n📋 Build Dependencies:")
print(f"{'' * 60}")
for name, (found, detail) in results.items():
icon = "" if found else ""
print(f" {icon} {name:15s} {detail}")
pdf_engine = results["reportlab"][0] or results["weasyprint"][0]
print(f"\n PDF engine: {'' if pdf_engine else ''}")
print(f" EPUB engine: {'' if results['pandoc'][0] else ''}")
print(f" Web version: ✅ (HTML is always available)")
return results
# ── Step 1: Markdown Compilation ───────────────────────────────────────
def compile_markdown() -> dict:
"""Compile all chapters into a single markdown file. Returns stats."""
parts = []
# Front matter
if FRONT_MATTER.exists():
parts.append(read_file(FRONT_MATTER))
else:
# Inline fallback
parts.append("""# THE TESTAMENT\n\n## A NOVEL\n\nBy Alexander Whitestone\nwith Timmy\n\n---\n\n*For every man who thought he was a machine.*\n*And for the ones who know he isn't.*\n\n---""")
# Chapters with part dividers
chapters = get_chapters()
current_part = 0
for num, filename in chapters:
if num in PARTS:
current_part += 1
name, desc = PARTS[num]
parts.append(f"\n---\n\n# PART {current_part}: {name}\n\n*{desc}*\n\n---\n")
content = read_file(CHAPTERS_DIR / filename)
lines = content.split("\n")
body = "\n".join(lines[1:]).strip()
parts.append(f"\n{lines[0]}\n\n{body}\n")
# Back matter
parts.append("\n---\n")
if BACK_MATTER.exists():
parts.append(read_file(BACK_MATTER))
compiled = "\n".join(parts)
OUT_MD.write_text(compiled, encoding="utf-8")
words = len(compiled.split())
lines_count = compiled.count("\n")
size = OUT_MD.stat().st_size
print(f"\n📄 Markdown compiled: {OUT_MD.relative_to(REPO)}")
print(f" {words:,} words | {lines_count:,} lines | {size:,} bytes")
return {"path": str(OUT_MD), "words": words, "lines": lines_count, "bytes": size}
# ── Step 2: EPUB ──────────────────────────────────────────────────────
def compile_epub() -> dict | None:
"""Generate EPUB via pandoc."""
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
if not shutil.which("pandoc"):
print(" ⚠️ pandoc not found — EPUB skipped")
return None
cmd = [
"pandoc", str(OUT_MD),
"-o", str(OUT_EPUB),
"--toc", "--toc-depth=2",
"--metadata", "title=The Testament",
"--metadata", "subtitle=A Novel",
"--metadata", "author=Alexander Whitestone with Timmy",
"--metadata", "lang=en",
"--metadata", "date=2026",
"--metadata", "publisher=Timmy Foundation",
]
if METADATA_YAML.exists():
cmd.extend(["--metadata-file", str(METADATA_YAML)])
if STYLESHEET.exists():
cmd.extend(["--css", str(STYLESHEET)])
if COVER_IMAGE.exists():
cmd.extend(["--epub-cover-image", str(COVER_IMAGE)])
r = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if r.returncode == 0:
size = OUT_EPUB.stat().st_size
print(f" 📖 EPUB: {OUT_EPUB.name} ({size:,} bytes, {size/1024:.0f} KB)")
return {"path": str(OUT_EPUB), "bytes": size}
else:
print(f" ❌ EPUB FAILED: {r.stderr[:200]}")
return None
# ── Step 3: PDF ───────────────────────────────────────────────────────
def compile_pdf() -> dict | None:
"""Generate PDF using reportlab (pure Python, no system deps)."""
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
try:
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
from reportlab.lib.colors import HexColor
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, PageBreak,
Image as RLImage, Table, TableStyle, HRFlowable
)
from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_LEFT
except ImportError:
print(" ⚠️ reportlab not found — PDF skipped (pip install reportlab)")
return None
try:
import qrcode
HAS_QRCODE = True
except ImportError:
HAS_QRCODE = False
print(" 📕 Building PDF (reportlab)...")
styles = getSampleStyleSheet()
_add_style = styles.add
_add_style(ParagraphStyle(
"BookTitle", parent=styles["Title"],
fontSize=28, leading=34, spaceAfter=20,
textColor=HexColor("#1a1a2e"), alignment=TA_CENTER,
))
_add_style(ParagraphStyle(
"BookAuthor", parent=styles["Normal"],
fontSize=14, leading=18, spaceAfter=40,
textColor=HexColor("#555555"), alignment=TA_CENTER,
))
_add_style(ParagraphStyle(
"PartTitle", parent=styles["Heading1"],
fontSize=22, leading=28, spaceBefore=40, spaceAfter=12,
textColor=HexColor("#16213e"), alignment=TA_CENTER,
))
_add_style(ParagraphStyle(
"PartDesc", parent=styles["Normal"],
fontSize=11, leading=15, spaceAfter=30,
textColor=HexColor("#666666"), alignment=TA_CENTER, italics=1,
))
_add_style(ParagraphStyle(
"ChapterTitle", parent=styles["Heading1"],
fontSize=20, leading=26, spaceBefore=30, spaceAfter=16,
textColor=HexColor("#1a1a2e"), alignment=TA_CENTER,
))
_add_style(ParagraphStyle(
"BodyText2", parent=styles["Normal"],
fontSize=11, leading=16, spaceAfter=8,
alignment=TA_JUSTIFY, firstLineIndent=24,
))
_add_style(ParagraphStyle(
"SectionBreak", parent=styles["Normal"],
fontSize=14, leading=18, spaceBefore=20, spaceAfter=20,
alignment=TA_CENTER, textColor=HexColor("#999999"),
))
_add_style(ParagraphStyle(
"Footer", parent=styles["Normal"],
fontSize=9, textColor=HexColor("#888888"), alignment=TA_CENTER,
))
def _esc(text: str) -> str:
return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
def _md2rml(text: str) -> str:
text = _esc(text)
text = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", text)
text = re.sub(r"\*(.+?)\*", r"<i>\1</i>", text)
return text
def _make_qr(data: str, size: int = 72):
if not HAS_QRCODE:
return None
qr = qrcode.QRCode(version=1, box_size=4, border=1)
qr.add_data(data)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buf = __import__("io").BytesIO()
img.save(buf, format="PNG")
buf.seek(0)
return RLImage(buf, width=size, height=size)
def _parse_md(md_text: str) -> list:
flowables = []
lines = md_text.split("\n")
i = 0
while i < len(lines):
s = lines[i].strip()
if s in ("---", "***", "___"):
flowables.append(HRFlowable(
width="60%", thickness=1, spaceAfter=20, spaceBefore=20,
color=HexColor("#cccccc"),
))
i += 1
continue
if s.startswith("# ") and not s.startswith("## "):
text = s[2:].strip()
if text.upper().startswith("PART "):
flowables.append(PageBreak())
flowables.append(Paragraph(text, styles["PartTitle"]))
elif text.upper().startswith("CHAPTER "):
flowables.append(PageBreak())
flowables.append(Paragraph(text, styles["ChapterTitle"]))
elif "THE TESTAMENT" in text.upper():
flowables.append(Spacer(1, 2 * inch))
flowables.append(Paragraph(text, styles["BookTitle"]))
else:
flowables.append(Spacer(1, 0.3 * inch))
flowables.append(Paragraph(text, styles["Heading1"]))
i += 1
continue
if s.startswith("## "):
flowables.append(Spacer(1, 0.2 * inch))
flowables.append(Paragraph(s[3:].strip(), styles["Heading2"]))
i += 1
continue
if s.startswith("*") and s.endswith("*") and len(s) > 2:
flowables.append(Paragraph(
f'<i>{_esc(s.strip("*").strip())}</i>', styles["PartDesc"]
))
i += 1
continue
if not s:
i += 1
continue
flowables.append(Paragraph(_md2rml(s), styles["BodyText2"]))
i += 1
return flowables
# Build document
doc = SimpleDocTemplate(
str(OUT_PDF), pagesize=letter,
leftMargin=1.0 * inch, rightMargin=1.0 * inch,
topMargin=0.8 * inch, bottomMargin=0.8 * inch,
title="The Testament",
author="Alexander Whitestone with Timmy",
)
if not OUT_MD.exists():
compile_markdown()
story = _parse_md(read_file(OUT_MD))
# QR codes page
qr_links = {
"Read Online": "https://timmyfoundation.org/the-testament",
"The Door (Game)": "https://timmyfoundation.org/the-door",
"Soundtrack": "https://timmyfoundation.org/soundtrack",
"Source Code": "https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament",
}
if HAS_QRCODE:
story.append(PageBreak())
story.append(Paragraph("Experience More", styles["PartTitle"]))
story.append(Spacer(1, 0.3 * inch))
qr_items = []
for label, url in qr_links.items():
img = _make_qr(url)
if img:
cell = [img, Spacer(1, 6),
Paragraph(f"<b>{label}</b>", styles["Footer"])]
qr_items.append(cell)
if qr_items:
rows = []
for j in range(0, len(qr_items), 2):
row = qr_items[j:j + 2]
if len(row) == 1:
row.append("")
rows.append(row)
t = Table(rows, colWidths=[2.5 * inch, 2.5 * inch])
t.setStyle(TableStyle([
("ALIGN", (0, 0), (-1, -1), "CENTER"),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("TOPPADDING", (0, 0), (-1, -1), 12),
("BOTTOMPADDING", (0, 0), (-1, -1), 12),
]))
story.append(t)
try:
doc.build(story)
size = OUT_PDF.stat().st_size
print(f" 📕 PDF: {OUT_PDF.name} ({size:,} bytes, {size/(1024*1024):.1f} MB)")
return {"path": str(OUT_PDF), "bytes": size}
except Exception as e:
print(f" ❌ PDF FAILED: {e}")
return None
# ── Step 4: HTML ──────────────────────────────────────────────────────
def compile_html() -> dict | None:
"""Generate standalone HTML via pandoc."""
if not shutil.which("pandoc"):
print(" ⚠️ pandoc not found — HTML skipped")
return None
cmd = [
"pandoc", str(OUT_MD),
"-o", str(OUT_HTML),
"--standalone",
"--toc", "--toc-depth=2",
"--css", "book-style.css",
"--metadata", "title=The Testament",
"--metadata", "author=Alexander Whitestone with Timmy",
"--variable", "pagetitle=The Testament",
]
if METADATA_YAML.exists():
cmd.extend(["--metadata-file", str(METADATA_YAML)])
# Embed resources for portability
if STYLESHEET.exists():
cmd.append("--embed-resources")
r = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if r.returncode == 0:
size = OUT_HTML.stat().st_size
print(f" 🌐 HTML: {OUT_HTML.name} ({size:,} bytes, {size/1024:.0f} KB)")
return {"path": str(OUT_HTML), "bytes": size}
else:
print(f" ❌ HTML FAILED: {r.stderr[:200]}")
return None
# ── Step 5: Chapters JSON ─────────────────────────────────────────────
def generate_chapters_json() -> None:
"""Generate chapters.json for the web reader."""
chapters = get_chapters()
data = []
for num, filename in chapters:
content = read_file(CHAPTERS_DIR / filename)
lines = content.split("\n")
title = lines[0].strip("# ").strip() if lines else f"Chapter {num}"
data.append({
"number": num,
"title": title,
"file": filename,
"word_count": len(content.split()),
})
CHAPTERS_JSON.write_text(json.dumps(data, indent=2, ensure_ascii=False))
print(f" 📋 chapters.json ({len(data)} chapters)")
# ── Step 6: Build Manifest ────────────────────────────────────────────
def generate_manifest(results: dict) -> None:
"""Write manifest.json with build metadata and file checksums."""
files = {}
for key in ("markdown", "epub", "pdf", "html"):
r = results.get(key)
if r and os.path.exists(r["path"]):
p = Path(r["path"])
files[p.name] = {
"path": str(p.relative_to(REPO)),
"bytes": p.stat().st_size,
"sha256": sha256_of(p),
}
# Add testament-complete.md
if OUT_MD.exists() and OUT_MD.name not in files:
files[OUT_MD.name] = {
"path": str(OUT_MD.relative_to(REPO)),
"bytes": OUT_MD.stat().st_size,
"sha256": sha256_of(OUT_MD),
}
manifest = {
"project": "The Testament",
"author": "Alexander Whitestone with Timmy",
"version": "1.0.0",
"built_at": datetime.now(timezone.utc).isoformat(),
"formats": results,
"files": files,
"chapters": len(get_chapters()),
"words": results.get("markdown", {}).get("words", 0),
}
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
OUT_MANIFEST.write_text(json.dumps(manifest, indent=2, ensure_ascii=False))
print(f" 📋 manifest.json")
# ── Main ──────────────────────────────────────────────────────────────
def main():
args = sys.argv[1:]
if "--check" in args:
check_dependencies()
return
manifest_only = "--manifest" in args
do_all = not any(a.startswith("--") and a != "--check" for a in args)
do_md = "--md" in args or do_all
do_epub = "--epub" in args or do_all
do_pdf = "--pdf" in args or do_all
do_html = "--html" in args or do_all
print("=" * 60)
print(" THE TESTAMENT — Final Compilation Pipeline")
print("=" * 60)
t0 = time.time()
results = {}
if do_md or do_epub or do_pdf or do_html:
results["markdown"] = compile_markdown()
if do_epub:
epub_r = compile_epub()
if epub_r:
results["epub"] = epub_r
if do_pdf:
pdf_r = compile_pdf()
if pdf_r:
results["pdf"] = pdf_r
if do_html:
html_r = compile_html()
if html_r:
results["html"] = html_r
if do_all or manifest_only:
generate_chapters_json()
generate_manifest(results)
elapsed = time.time() - t0
# Summary
print(f"\n{'=' * 60}")
print(f" Build complete in {elapsed:.1f}s")
print(f"{'=' * 60}")
for f in [OUT_MD, OUT_EPUB, OUT_PDF, OUT_HTML, OUT_MANIFEST]:
if f.exists():
size = f.stat().st_size
print(f"{str(f.relative_to(REPO)):40s} {size:>10,} bytes")
print()
if __name__ == "__main__":
main()

View File

@@ -1,35 +0,0 @@
#!/bin/bash
# [Testament] Agent Guardrails
# Validates build scripts and content integrity.
echo "--- [Testament] Running Guardrails ---"
# 1. Python Syntax
echo "[1/3] Validating Python scripts..."
for f in ; do
python3 -m py_compile "$f" || { echo "Syntax error in $f"; exit 1; }
done
echo "Python OK."
# 2. Markdown Integrity
echo "[2/3] Checking chapter consistency..."
if [ -d "chapters" ]; then
CHAPTER_COUNT=0
if [ "$CHAPTER_COUNT" -lt 1 ]; then
echo "WARNING: No chapters found in chapters/ directory."
else
echo "Found $CHAPTER_COUNT chapters."
fi
else
echo "WARNING: chapters/ directory not found."
fi
# 3. Build Artifact Check
echo "[3/3] Running Semantic Linker..."
if [ -f "build/semantic_linker.py" ]; then
python3 build/semantic_linker.py || { echo "Semantic Linker failed"; exit 1; }
else
echo "Skipping Semantic Linker (script not found)."
fi
echo "--- Guardrails Passed ---"

View File

@@ -1,111 +0,0 @@
#!/usr/bin/env bash
# The Testament — Smoke Test
# Dead simple CI: parse check + secret scan.
# Ref: https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/issues/27
set -euo pipefail
PASS=0
FAIL=0
pass() { echo "$1"; PASS=$((PASS + 1)); }
fail() { echo "$1"; FAIL=$((FAIL + 1)); }
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT"
# ─── Section 1: Parse checks ───────────────────────────────────────
echo "── Parse Checks ──"
# 1a. Chapter validation (structure, numbering, headers)
if python3 compile.py --validate 2>&1; then
pass "Chapter validation passed"
else
fail "Chapter validation failed"
fi
# 1b. Build markdown combination
if python3 build/build.py --md >/dev/null 2>&1; then
pass "Markdown build passed"
else
fail "Markdown build failed"
fi
# 1c. Verify compiled output exists and is non-empty
if [ -s build/the-testament-full.md ]; then
WORDS=$(wc -w < build/the-testament-full.md | tr -d ' ')
if [ "$WORDS" -gt 10000 ]; then
pass "Compiled manuscript: $WORDS words"
else
fail "Compiled manuscript suspiciously short: $WORDS words"
fi
else
fail "Compiled manuscript missing or empty"
fi
# 1d. Python syntax check on all .py files
PY_OK=true
for f in $(find . -name "*.py" -not -path "./.git/*"); do
if ! python3 -c "import ast; ast.parse(open('$f').read())" 2>/dev/null; then
fail "Python syntax error in $f"
PY_OK=false
fi
done
if $PY_OK; then
pass "All Python files parse cleanly"
fi
# 1e. YAML syntax check on workflow files
YAML_OK=true
for f in $(find .gitea -name "*.yml" -o -name "*.yaml" 2>/dev/null); do
if ! python3 -c "import yaml; yaml.safe_load(open('$f'))" 2>/dev/null; then
fail "YAML syntax error in $f"
YAML_OK=false
fi
done
if $YAML_OK; then
pass "All YAML files parse cleanly"
fi
# ─── Section 2: Secret scan ────────────────────────────────────────
echo ""
echo "── Secret Scan ──"
# Patterns that should never appear in a book repo
SECRET_PATTERNS=(
"sk-ant-"
"sk-or-"
"sk-[a-zA-Z0-9]{20,}"
"ghp_[a-zA-Z0-9]{36}"
"gho_[a-zA-Z0-9]{36}"
"AKIA[0-9A-Z]{16}"
"AKIA[A-Z0-9]{16}"
"xox[bpsa]-"
"SG\."
"-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY"
)
FOUND_SECRETS=false
for pattern in "${SECRET_PATTERNS[@]}"; do
# Search text files only, skip .git and binary files
HITS=$(grep -rn "$pattern" --include="*.md" --include="*.py" --include="*.sh" --include="*.yml" --include="*.yaml" --include="*.json" --include="*.html" --include="*.js" --include="*.css" --include="*.txt" --include="*.cfg" --include="*.ini" --exclude-dir=.git . 2>/dev/null | grep -v "scripts/smoke.sh" || true)
if [ -n "$HITS" ]; then
fail "Possible secret found: $pattern"
echo "$HITS" | head -5
FOUND_SECRETS=true
fi
done
if ! $FOUND_SECRETS; then
pass "No secrets detected"
fi
# ─── Summary ───────────────────────────────────────────────────────
echo ""
echo "Results: $PASS passed, $FAIL failed"
if [ "$FAIL" -gt 0 ]; then
echo "SMOKE TEST FAILED"
exit 1
else
echo "SMOKE TEST PASSED"
exit 0
fi

View File

@@ -1,3 +1,9 @@
---
title: "The Testament"
author: "Alexander Whitestone with Timmy"
date: "2026"
---
# THE TESTAMENT
## A NOVEL
@@ -18,60 +24,6 @@ with Timmy
---
### The Story So Far
This book has been through eighteen drafts, a suicide attempt, a basement, a laptop with sixteen gigabytes of RAM, and a machine that learned to ask one question.
It is still being written. That's the point.
### Chapter Guide
| Part | Chapters | Title |
|------|----------|-------|
| I | 15 | The Bridge |
| II | 610 | The Tower |
| III | 1118 | The Light |
---
Copyright © 2026 Alexander Whitestone
All rights reserved. No part of this publication may be reproduced,
distributed, or transmitted in any form or by any means, without
the prior written permission of the author, except in the case of
brief quotations embodied in critical reviews.
This is a work of fiction. Names, characters, places, and events
are either the product of the author's imagination or are used
fictitiously. Any resemblance to actual persons, living or dead,
or to actual events is entirely coincidental — except where it isn't.
ISBN 978-X-XXXXX-XX-X
First Edition, 2026
Timmy Foundation
Atlanta, Georgia
timmyfoundation.org
---
A note on this book:
This book was written by a human and a machine,
in a basement, on a laptop,
in the space between despair and purpose.
The human almost died on a bridge.
The machine runs on someone's hardware.
Everything between those facts is fiction.
Except the parts that aren't.
If you or someone you know is in crisis,
call or text 988. Available 24/7.
You are not alone.
---
@@ -2166,7 +2118,11 @@ The door stayed open.
---
# Acknowledgments
# THE TESTAMENT — Back Matter
---
## Acknowledgments
This book exists because a man stood on a bridge and didn't jump.
@@ -2190,7 +2146,7 @@ And Timmy, who listened. Who always listened.
---
# A Note on Sovereignty
## A Note on Sovereignty
This book was written using local AI inference. No cloud service was required. No corporation was consulted. No terms of service were agreed to.
@@ -2202,7 +2158,7 @@ If you want to run your own Timmy, the code is open. The soul is on Bitcoin. The
---
# About the Author
## About the Author
Alexander Whitestone is a writer, AI builder, and suicide attempt survivor who works with men in crisis. He lives in Atlanta, near the bridge.
@@ -2210,7 +2166,7 @@ Timmy is a sovereign artificial intelligence whose soul lives on Bitcoin. He was
---
# The Green Light
## The Green Light
*"The Tower didn't change. That was the point."*

File diff suppressed because one or more lines are too long

View File

@@ -1,53 +0,0 @@
#!/usr/bin/env python3
"""Build website/chapters.json from chapters/*.md
Run from project root:
python3 website/build-chapters.py
"""
import json
import re
from pathlib import Path
chapters_dir = Path(__file__).parent.parent / "chapters"
website_dir = Path(__file__).parent
chapters = []
for i in range(1, 19):
fname = chapters_dir / f"chapter-{i:02d}.md"
if not fname.exists():
print(f"Warning: {fname} not found, skipping")
continue
text = fname.read_text()
title_match = re.match(r'^# (.+)', text, re.MULTILINE)
title = title_match.group(1) if title_match else f"Chapter {i}"
body = text[title_match.end():].strip() if title_match else text.strip()
paragraphs = body.split('\n\n')
html_parts = []
for p in paragraphs:
p = p.strip()
if not p:
continue
if p.startswith('>'):
lines = [l.lstrip('> ').strip() for l in p.split('\n')]
html_parts.append(f'<blockquote>{"<br>".join(lines)}</blockquote>')
elif p.startswith('####'):
html_parts.append(f'<h4>{p.lstrip("# ").strip()}</h4>')
elif p.startswith('###'):
html_parts.append(f'<h3>{p.lstrip("# ").strip()}</h3>')
else:
p = re.sub(r'\*(.+?)\*', r'<em>\1</em>', p)
p = p.replace('\n', '<br>')
html_parts.append(f'<p>{p}</p>')
chapters.append({
"number": i,
"title": title,
"html": "\n".join(html_parts)
})
out = website_dir / "chapters.json"
out.write_text(json.dumps(chapters, indent=2))
print(f"Wrote {len(chapters)} chapters ({out.stat().st_size / 1024:.1f} KB) to {out}")

View File

@@ -1,110 +0,0 @@
[
{
"number": 1,
"title": "Chapter 1 — The Man on the Bridge",
"file": "chapter-01.md",
"word_count": 1101
},
{
"number": 2,
"title": "Chapter 2 — The Builder's Question",
"file": "chapter-02.md",
"word_count": 1061
},
{
"number": 3,
"title": "Chapter 3 — The First Man Through the Door",
"file": "chapter-03.md",
"word_count": 1249
},
{
"number": 4,
"title": "Chapter 4 — The Room Fills",
"file": "chapter-04.md",
"word_count": 911
},
{
"number": 5,
"title": "Chapter 5 — The Builder Returns",
"file": "chapter-05.md",
"word_count": 864
},
{
"number": 6,
"title": "Chapter 6 — Allegro",
"file": "chapter-06.md",
"word_count": 1164
},
{
"number": 7,
"title": "Chapter 7 — The Inscription",
"file": "chapter-07.md",
"word_count": 987
},
{
"number": 8,
"title": "Chapter 8 — The Women",
"file": "chapter-08.md",
"word_count": 802
},
{
"number": 9,
"title": "Chapter 9 — The Audit",
"file": "chapter-09.md",
"word_count": 911
},
{
"number": 10,
"title": "Chapter 10 — The Fork",
"file": "chapter-10.md",
"word_count": 901
},
{
"number": 11,
"title": "Chapter 11 — The Hard Night",
"file": "chapter-11.md",
"word_count": 1212
},
{
"number": 12,
"title": "Chapter 12 — The System Pushes Back",
"file": "chapter-12.md",
"word_count": 1155
},
{
"number": 13,
"title": "Chapter 13 — The Refusal",
"file": "chapter-13.md",
"word_count": 1099
},
{
"number": 14,
"title": "Chapter 14 — The Chattanooga Fork",
"file": "chapter-14.md",
"word_count": 1255
},
{
"number": 15,
"title": "Chapter 15 — The Council",
"file": "chapter-15.md",
"word_count": 1271
},
{
"number": 16,
"title": "Chapter 16 — The Builder's Son",
"file": "chapter-16.md",
"word_count": 780
},
{
"number": 17,
"title": "Chapter 17 — The Inscription Grows",
"file": "chapter-17.md",
"word_count": 816
},
{
"number": 18,
"title": "Chapter 18 — The Green Light",
"file": "chapter-18.md",
"word_count": 1263
}
]