Compare commits

..

31 Commits

Author SHA1 Message Date
Alexander Whitestone
1251044511 feat: full website enhancement — issue #32
Some checks failed
Smoke Test / smoke (pull_request) Failing after 6s
Build Validation / validate-manuscript (pull_request) Successful in 7s
- Sticky navigation (appears after scroll past hero)
- Reading progress bar (green glow, top of page)
- Open Graph + Twitter Card meta tags
- All 18 chapters organized by Part I/II/III
- 8 character cards (added Daniel + The Builder)
- Scroll-triggered fade-in animations
- Ambient rain sound toggle with generated rain.mp3
- Back to top link in footer
- Hover effects on character cards and chapter items
- Responsive improvements for mobile
2026-04-11 16:00:51 -04:00
f364c82bac [auto-merge] the-testament#31
Some checks failed
Smoke Test / smoke (push) Failing after 6s
Auto-merged PR #31
2026-04-11 18:53:38 +00:00
Timmy
332166a901 wip: fix PosixPath formatting, update Makefile with unified target
Some checks failed
Smoke Test / smoke (pull_request) Failing after 11s
Build Validation / validate-manuscript (pull_request) Successful in 7s
- Fix relative_to() format string errors (str() wrapper)
- Add 'make unified' target for compile_all.py
- Update 'make check' to use compile_all.py --check
- Clean removes build-manifest.json and chapters.json
2026-04-11 14:28:26 -04:00
Timmy
26a5ac46e6 feat: unified compile_all.py pipeline
Single script builds all distributable formats:
- testament-complete.md (full novel markdown)
- testament.epub (with cover art + CSS via pandoc)
- testament.pdf (reportlab with QR codes)
- testament.html (standalone styled HTML)
- website/chapters.json (web reader data)
- build-manifest.json (SHA256 checksums)

Closes #30
2026-04-11 14:26:32 -04:00
3247dd29f0 [Testament] Add scripts/guardrails.sh (GOFAI improvements and guardrails)
Some checks failed
Smoke Test / smoke (push) Failing after 5s
2026-04-11 01:40:37 +00:00
a01b998f61 [Testament] Add build/semantic_linker.py (GOFAI improvements and guardrails)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:40:36 +00:00
59cd71985d Create website/chapters.json
Some checks failed
Smoke Test / smoke (push) Failing after 5s
2026-04-11 01:36:33 +00:00
6c506caac6 Create website/build-chapters.py
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:32 +00:00
55d51f2ee4 Update testament-complete.md (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:31 +00:00
eae9398fa5 Update compile.py (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:29 +00:00
f8528e9ded Update build/metadata.yaml (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:28 +00:00
374d82a886 Update build/frontmatter.md (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:27 +00:00
4763311588 Update build/build.py (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:25 +00:00
348ed7ee92 Update build/backmatter.md (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:24 +00:00
22f59c57cb Create book-style.css
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:23 +00:00
4ac38f1b60 Create art-manifest.md
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:22 +00:00
d586fb211d Update Makefile (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:20 +00:00
92867808b2 Update MULTIMEDIA-PLAN.md (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:18 +00:00
47a13325cc Create EPIC-MATRIX.md
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:17 +00:00
14273702ba Update .gitignore (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:16 +00:00
2e1f6ffb5b Update .gitea/workflows/smoke.yml (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:15 +00:00
Alexander Whitestone
08233364ff burn: add smoke test workflow — parse check + secret scan
All checks were successful
Smoke Test / smoke-test (pull_request) Successful in 10s
Build Validation / validate-manuscript (pull_request) Successful in 9s
Smoke Test / smoke-test (push) Successful in 8s
Closes #27

Adds a dead-simple CI smoke test that runs on every PR and push to main:

Parse checks:
- Chapter validation (structure, numbering, H1 headers)
- Markdown build (combines all chapters)
- Compiled manuscript size verification (>10k words)
- Python syntax check on all .py files
- YAML syntax check on workflow files

Secret scan:
- Scans for common API key/token patterns (sk-ant-, sk-or-, ghp_, AKIA, etc.)
- Searches all text files, excludes .git and the smoke test itself
- Hard fail if any secrets found

Two files:
- scripts/smoke.sh — the smoke test script
- .gitea/workflows/smoke.yml — Gitea Actions workflow
2026-04-10 20:58:16 -04:00
544bc1a985 Merge pull request 'feat: add CI workflow for manuscript build validation' (#25) from feat/ci-build-validation into main
Merged PR #25: feat: add CI workflow for manuscript build validation
2026-04-11 00:44:01 +00:00
ba9fd0ba08 Merge pull request 'burn: add chapter validation to build pipeline (closes #24)' (#26) from burn/20260410-chapter-validation into main
Merged PR #26: burn: add chapter validation to build pipeline
2026-04-11 00:43:38 +00:00
8ba9f58e96 Merge pull request 'feat: add book compilation pipeline (rescued from #20)' (#28) from rescue/book-compilation into main
Merged PR #28: feat: add book compilation pipeline
2026-04-11 00:43:36 +00:00
Alexander Whitestone
f6d74e233b feat: add book compilation pipeline (rescued from #20)
Build system for The Testament:
- build/build.py: compiles chapters to PDF, EPUB, MD
- build/metadata.yaml: book metadata
- build/frontmatter.md: title page, dedication
- build/backmatter.md: acknowledgments, sovereignty note
- Makefile: make pdf, make epub, make md
- .gitignore: build artifacts
2026-04-10 20:32:38 -04:00
Alexander Whitestone
948d520b83 burn: add chapter validation to build pipeline (closes #24)
Add validate_chapters() function that checks:
- No empty chapter files (whitespace-only counts as empty)
- Every chapter starts with an H1 header (# Chapter N — Title)
- No gaps in chapter numbering (sequential from 1)
- No duplicate chapter numbers
- Header chapter number matches filename number
- Warns on suspiciously short chapters (<50 words)

Validation runs automatically before compilation. If errors are found,
compilation is aborted with clear error messages showing exactly what
to fix.

CLI flags:
  python3 compile.py --validate     # validate only
  python3 compile.py --no-validate  # skip validation
  python3 compile.py                # validate then compile
2026-04-10 19:57:27 -04:00
7a56b4b727 feat: add CI workflow for manuscript build validation
Some checks failed
Build Validation / validate-manuscript (pull_request) Failing after 5s
2026-04-10 23:55:17 +00:00
bebd3943d4 [auto-merge] README update
Auto-merged by PR review bot: README update
2026-04-10 11:48:32 +00:00
Alexander Whitestone
1d4e8a6478 burn: update README with full 18-chapter structure, characters, themes
Closes #21

The README previously listed only Chapter 1 with 'Draft' status.
Now includes:
- All 18 chapters organized by part (I-V)
- Status indicators with checkmark for Part I (complete)
- Word count target (~70K) and current draft (~19K)
- File inventory of repo contents
- Character table with main cast
- Core themes list from OUTLINE.md
- Link to compilation pipeline PR #20
2026-04-10 06:42:59 -04:00
d0680715ac Merge pull request #19
Merged PR #19
2026-04-10 03:43:49 +00:00
28 changed files with 3264 additions and 242 deletions

View File

@@ -0,0 +1,24 @@
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

@@ -0,0 +1,22 @@
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 +1,3 @@
__pycache__/
build/output/*.pdf
build/output/*.epub

132
EPIC-MATRIX.md Normal file
View File

@@ -0,0 +1,132 @@
# 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,14 +109,15 @@ Design:
## EPIC 7: Social Media Assets
**Goal:** Shareable quotes, excerpts, and teasers
**Assets:** Generated images with text overlays
**Deliverables:** 10+ images sized for Twitter/Instagram/Telegram
**Deliverables:** 13 images sized for Twitter/Instagram/Telegram
Content:
- 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
- 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" ✅
---

35
Makefile Normal file
View File

@@ -0,0 +1,35 @@
# THE TESTAMENT — Build System
# Usage: make all | make pdf | make epub | make html | make md | make clean
#
# Recommended: make unified (single script, all formats + manifest)
.PHONY: all unified pdf epub html md clean check
# Unified pipeline (compile_all.py) — builds everything + manifest
unified:
python3 compile_all.py
# Legacy targets (build/build.py)
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
rm -f build-manifest.json
rm -f website/chapters.json
check:
python3 compile_all.py --check

View File

@@ -6,17 +6,86 @@ A novel about broken men, sovereign AI, and the soul on Bitcoin.
## Structure
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.
Five Parts, 18 Chapters, ~70,000 words target (currently ~19,000 words drafted).
## Chapters
### Part I — The Machine That Asks (Chapters 15) ✅ Complete
| # | Title | Status |
|---|-------|--------|
| 1 | The Man on the Bridge | Draft |
| 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
## Characters
See `characters/` for detailed profiles.
| 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).
## License

View File

@@ -39,15 +39,23 @@ 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 (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 |
## 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) |
## Storage
All images stored in `~/Pictures/the-testament/` (outside git repo).
Total: 36 images (~15 MB)
Total: 44 images (~17 MB)

270
book-style.css Normal file
View File

@@ -0,0 +1,270 @@
/* 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;
}
}

28
build-manifest.json Normal file
View File

@@ -0,0 +1,28 @@
{
"project": "The Testament",
"author": "Alexander Whitestone with Timmy",
"built_at": "2026-04-11T18:28:05Z",
"compiler": "compile_all.py",
"files": {
"testament-complete.md": {
"path": "testament-complete.md",
"size_bytes": 111105,
"sha256": "4e224d1e8fc2a4be63d6a33eb43b082428b0f1439a9ec69165cc18c09e154001"
},
"testament.epub": {
"path": "testament.epub",
"size_bytes": 67270,
"sha256": "a6bc3e577ed80bfb49febc52ec12f86608353fa9849e263094f43b946e128c0e"
},
"testament.html": {
"path": "testament.html",
"size_bytes": 3865298,
"sha256": "bdfa312b175a46be957b023f3e5d7d33230bae470b432ee90b69644a086756da"
},
"website/chapters.json": {
"path": "website/chapters.json",
"size_bytes": 118394,
"sha256": "7eafcfd75cccea57f443a214fe7d443268abc40f632a9e2755236d97547da08a"
}
}
}

61
build/backmatter.md Normal file
View File

@@ -0,0 +1,61 @@
# 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.*

485
build/build.py Executable file
View File

@@ -0,0 +1,485 @@
#!/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()

73
build/frontmatter.md Normal file
View File

@@ -0,0 +1,73 @@
# 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.

41
build/metadata.yaml Normal file
View File

@@ -0,0 +1,41 @@
---
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.

51
build/semantic_linker.py Normal file
View File

@@ -0,0 +1,51 @@
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,25 +1,41 @@
#!/usr/bin/env python3
"""
THE TESTAMENT — PDF Compilation Script
THE TESTAMENT — Book Compilation Pipeline
Compiles the complete book into a single markdown file suitable for PDF conversion.
Uses chapters, front matter, back matter, and references illustrations.
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)
Requirements: pip install markdown weasyprint (or use pandoc)
Requirements:
- pandoc (brew install pandoc)
- weasyprint (pip install weasyprint) — optional, for direct PDF
Usage:
python3 compile.py # generates testament-complete.md
pandoc testament-complete.md -o testament.pdf --pdf-engine=weasyprint
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
"""
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 = os.path.join(BASE, "testament-complete.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")
# Part divisions based on chapter groupings from the novel
PARTS = {
@@ -28,15 +44,55 @@ 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 compile():
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."""
output = []
# Title page
@@ -44,6 +100,7 @@ def compile():
title: "The Testament"
author: "Alexander Whitestone with Timmy"
date: "2026"
lang: en
---
# THE TESTAMENT
@@ -77,44 +134,207 @@ 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() # Skip "# Chapter X — Title"
# Add chapter
body = '\n'.join(lines[1:]).strip()
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, 'w') as f:
with open(OUTPUT_MD, 'w') as f:
f.write(compiled)
# Stats
words = len(compiled.split())
lines_count = compiled.count('\n')
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")
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))
if __name__ == "__main__":
compile()
main()

642
compile_all.py Normal file
View File

@@ -0,0 +1,642 @@
#!/usr/bin/env python3
"""
THE TESTAMENT — Unified Compilation Pipeline
Single script that builds ALL distributable formats:
1. testament-complete.md — full novel as one markdown file
2. testament.epub — EPUB with cover art + CSS
3. testament.pdf — PDF via reportlab (pure Python) with QR codes
4. testament.html — standalone styled HTML
5. website/chapters.json — chapter data for the web reader
6. build-manifest.json — SHA256 checksums of all outputs
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 + HTML
python3 compile_all.py --json # markdown + chapters.json
python3 compile_all.py --check # verify dependencies
python3 compile_all.py --clean # remove all build artifacts
Requirements:
- pandoc (brew install pandoc) — for EPUB and HTML
- reportlab (pip install reportlab) — for PDF (pure Python)
- qrcode (pip install qrcode) — for QR codes in PDF
"""
import hashlib
import json
import os
import re
import subprocess
import sys
import time
from pathlib import Path
# ── Paths ──────────────────────────────────────────────────────────────
REPO = Path(__file__).resolve().parent
CHAPTERS_DIR = REPO / "chapters"
FRONT_MATTER = REPO / "front-matter.md"
BACK_MATTER = REPO / "back-matter.md"
WEBSITE_DIR = REPO / "website"
BUILD_DIR = REPO / "build"
OUTPUT_DIR = BUILD_DIR / "output"
# Output files
OUT_MD = REPO / "testament-complete.md"
OUT_EPUB = REPO / "testament.epub"
OUT_HTML = REPO / "testament.html"
OUT_PDF = REPO / "testament.pdf"
OUT_JSON = WEBSITE_DIR / "chapters.json"
OUT_MANIFEST = REPO / "build-manifest.json"
STYLESHEET = REPO / "book-style.css"
COVER_IMAGE = REPO / "cover" / "cover-art.jpg"
# ── 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."),
}
# QR code destinations embedded in the PDF
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",
}
# ── 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 read_file(path: Path) -> str:
return path.read_text(encoding="utf-8")
def sha256_file(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 get_sorted_chapters() -> list[tuple[int, str]]:
"""Return [(number, filename), ...] sorted by chapter number."""
chapters = []
for f in os.listdir(CHAPTERS_DIR):
if f.startswith("chapter-") and f.endswith(".md"):
chapters.append((get_chapter_num(f), f))
return sorted(chapters)
# ── 1. Markdown Compilation ───────────────────────────────────────────
def compile_markdown() -> int:
"""Compile all chapters into a single markdown file. Returns word count."""
parts = []
# Title page
parts.append("""---
title: "The Testament"
author: "Alexander Whitestone with Timmy"
date: "2026"
lang: en
---
# 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.
---
""")
chapters = get_sorted_chapters()
current_part = 0
for num, filename in chapters:
if num in PARTS:
part_name, part_desc = PARTS[num]
current_part += 1
parts.append(f"\n---\n\n# PART {current_part}: {part_name}\n\n*{part_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")
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" 📄 {OUT_MD.name:30s} {words:>8,} words {size:>10,} bytes")
return words
# ── 2. EPUB Compilation ────────────────────────────────────────────────
def compile_epub() -> bool:
"""Generate EPUB from compiled markdown using pandoc."""
if not OUT_MD.exists():
print(" ⚠️ Markdown not compiled yet — skipping EPUB")
return False
pandoc = shutil_which("pandoc")
if not pandoc:
print(" ⚠️ pandoc not found — skipping EPUB (brew install pandoc)")
return False
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 STYLESHEET.exists():
cmd.extend(["--css", str(STYLESHEET)])
if COVER_IMAGE.exists():
cmd.extend(["--epub-cover-image", str(COVER_IMAGE)])
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
size = OUT_EPUB.stat().st_size
print(f" 📖 {OUT_EPUB.name:30s} {'':>8s} {size:>10,} bytes ({size/1024:.0f} KB)")
return True
else:
print(f" ❌ EPUB failed: {result.stderr[:200]}")
return False
# ── 3. PDF via Reportlab ──────────────────────────────────────────────
def compile_pdf() -> bool:
"""Generate PDF using reportlab — pure Python, no external system deps."""
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
except ImportError:
print(" ⚠️ reportlab not installed — skipping PDF (pip install reportlab)")
return False
try:
import qrcode
HAS_QRCODE = True
except ImportError:
HAS_QRCODE = False
import io
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
print(" ⏳ Building PDF (reportlab)...")
# ── Styles ──
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(
"Footer", parent=styles["Normal"],
fontSize=9, textColor=HexColor("#888888"), alignment=TA_CENTER,
))
def _escape(text: str) -> str:
return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
def _md_inline_to_rml(text: str) -> str:
text = _escape(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 = 80):
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: str) -> list:
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()
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
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
# Regular paragraph
para_text = _md_inline_to_rml(stripped)
flowables.append(Paragraph(para_text, styles["BodyText2"]))
i += 1
return flowables
# ── Build 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",
)
if not OUT_MD.exists():
compile_markdown()
md_text = OUT_MD.read_text(encoding="utf-8")
story = _parse_md_to_flowables(md_text)
# QR codes page
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 = [qr_img, Spacer(1, 6)]
cell.append(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)
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)
try:
doc.build(story)
size = OUT_PDF.stat().st_size
print(f" 📕 {OUT_PDF.name:30s} {'':>8s} {size:>10,} bytes ({size / (1024 * 1024):.1f} MB)")
return True
except Exception as e:
print(f" ❌ PDF failed: {e}")
return False
# ── 4. HTML Compilation ────────────────────────────────────────────────
def compile_html() -> bool:
"""Generate standalone styled HTML using pandoc."""
if not OUT_MD.exists():
print(" ⚠️ Markdown not compiled yet — skipping HTML")
return False
if not shutil_which("pandoc"):
print(" ⚠️ pandoc not found — skipping HTML")
return False
cmd = [
"pandoc", str(OUT_MD),
"-o", str(OUT_HTML),
"--standalone",
"--toc", "--toc-depth=2",
"--metadata", "title=The Testament",
"--metadata", "author=Alexander Whitestone with Timmy",
"-V", "lang=en",
]
if STYLESHEET.exists():
cmd.extend(["--css", str(STYLESHEET), "--embed-resources"])
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
size = OUT_HTML.stat().st_size
print(f" 🌐 {OUT_HTML.name:30s} {'':>8s} {size:>10,} bytes ({size / 1024:.0f} KB)")
return True
else:
print(f" ❌ HTML failed: {result.stderr[:200]}")
return False
# ── 5. chapters.json for Web Reader ────────────────────────────────────
def compile_chapters_json() -> bool:
"""Build website/chapters.json from chapters/*.md for the web reader."""
WEBSITE_DIR.mkdir(parents=True, exist_ok=True)
chapters = []
for i in range(1, 19):
fname = CHAPTERS_DIR / f"chapter-{i:02d}.md"
if not fname.exists():
print(f" ⚠️ {fname.name} not found, skipping")
continue
text = fname.read_text(encoding="utf-8")
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_JSON.write_text(json.dumps(chapters, indent=2), encoding="utf-8")
size = OUT_JSON.stat().st_size
print(f" 📋 {str(OUT_JSON.relative_to(REPO)):30s} {len(chapters):>4} chapters {size:>10,} bytes")
return True
# ── 6. Build Manifest ─────────────────────────────────────────────────
def generate_manifest() -> bool:
"""Generate build-manifest.json with SHA256 checksums of all outputs."""
outputs = {
"testament-complete.md": OUT_MD,
"testament.epub": OUT_EPUB,
"testament.pdf": OUT_PDF,
"testament.html": OUT_HTML,
"website/chapters.json": OUT_JSON,
}
manifest = {
"project": "The Testament",
"author": "Alexander Whitestone with Timmy",
"built_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"compiler": "compile_all.py",
"files": {},
}
for name, path in outputs.items():
if path.exists():
stat = path.stat()
manifest["files"][name] = {
"path": name,
"size_bytes": stat.st_size,
"sha256": sha256_file(path),
}
OUT_MANIFEST.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
print(f" 📜 {str(OUT_MANIFEST.relative_to(REPO)):30s} {len(manifest['files']):>4} files")
return True
# ── Dependency Check ───────────────────────────────────────────────────
def shutil_which(name: str) -> str | None:
"""Minimal which without importing shutil for everything."""
import shutil
return shutil.which(name)
def check_dependencies():
"""Verify all required tools are available."""
import shutil as _shutil
print("\n📋 Dependency Check:")
print(f"{'' * 55}")
pandoc = _shutil.which("pandoc")
print(f" {'' if pandoc else ''} pandoc {pandoc or 'NOT FOUND (brew install pandoc)'}")
try:
import reportlab
print(f" ✅ reportlab {reportlab.Version}")
except ImportError:
print(f" ❌ reportlab NOT FOUND (pip install reportlab)")
try:
import qrcode
print(f" ✅ qrcode {qrcode.__version__}")
except ImportError:
print(f" ❌ qrcode NOT FOUND (pip install qrcode)")
style = STYLESHEET.exists()
print(f" {'' if style else '⚠️ '} stylesheet {STYLESHEET if style else 'NOT FOUND (optional)'}")
cover = COVER_IMAGE.exists()
print(f" {'' if cover else '⚠️ '} cover art {COVER_IMAGE if cover else 'NOT FOUND (optional)'}")
# ── Clean ──────────────────────────────────────────────────────────────
def clean():
"""Remove all build artifacts."""
artifacts = [OUT_MD, OUT_EPUB, OUT_HTML, OUT_PDF, OUT_JSON, OUT_MANIFEST]
# Also clean build/output/
for f in OUTPUT_DIR.glob("*"):
if f.is_file():
artifacts.append(f)
removed = 0
for f in artifacts:
if f.exists():
f.unlink()
removed += 1
print(f" 🗑️ {f.relative_to(REPO)}")
if removed == 0:
print(" (nothing to clean)")
else:
print(f" Removed {removed} files.")
# ── Main ───────────────────────────────────────────────────────────────
def main():
args = sys.argv[1:]
t0 = time.time()
if "--check" in args:
check_dependencies()
return
if "--clean" in args:
print("🧹 Cleaning build artifacts...")
clean()
return
do_all = not any(a.startswith("--") 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
do_json = "--json" in args or do_all
print("=" * 65)
print(" THE TESTAMENT — Unified Compilation Pipeline")
print("=" * 65)
results = {}
# Step 1: Markdown (always first — others depend on it)
if do_md or do_epub or do_pdf or do_html:
results["markdown"] = compile_markdown()
# Step 2: EPUB
if do_epub:
results["epub"] = compile_epub()
# Step 3: PDF
if do_pdf:
results["pdf"] = compile_pdf()
# Step 4: HTML
if do_html:
results["html"] = compile_html()
# Step 5: chapters.json
if do_json or do_all:
results["chapters_json"] = compile_chapters_json()
# Step 6: Build manifest
if do_all or "--manifest" in args:
results["manifest"] = generate_manifest()
# Summary
elapsed = time.time() - t0
print(f"\n{'' * 65}")
built = [k for k, v in results.items() if v]
failed = [k for k, v in results.items() if not v]
if built:
print(f" ✅ Built: {', '.join(built)}")
if failed:
print(f" ❌ Failed: {', '.join(failed)}")
print(f" ⏱️ Completed in {elapsed:.1f}s")
print(f"{'=' * 65}")
if __name__ == "__main__":
main()

70
scripts/generate-rain.py Normal file
View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Generate ambient rain sound for The Testament website."""
import numpy as np
import struct
import wave
import subprocess
import os
SAMPLE_RATE = 44100
DURATION = 30 # seconds (loops seamlessly)
OUTPUT_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "website")
np.random.seed(42)
# Generate white noise
samples = SAMPLE_RATE * DURATION
noise = np.random.randn(samples).astype(np.float64)
# Simple low-pass filter (moving average) to create rain-like sound
# Multiple passes for smoother result
kernel_size = 80
kernel = np.ones(kernel_size) / kernel_size
filtered = np.convolve(noise, kernel, mode='same')
# Second pass with smaller kernel for texture
kernel2 = np.ones(20) / 20
filtered = np.convolve(filtered, kernel2, mode='same')
# Add some higher-frequency texture (lighter rain drops)
drops = np.random.randn(samples).astype(np.float64) * 0.05
kernel_drops = np.ones(5) / 5
drops = np.convolve(drops, kernel_drops, mode='same')
filtered += drops
# Normalize to 16-bit range
filtered = filtered / np.max(np.abs(filtered)) * 0.6 # Keep volume moderate
# Fade in/out for seamless looping (crossfade at boundaries)
fade_len = int(SAMPLE_RATE * 0.5) # 0.5s fade
fade_in = np.linspace(0, 1, fade_len)
fade_out = np.linspace(1, 0, fade_len)
filtered[:fade_len] *= fade_in
filtered[-fade_len:] *= fade_out
# Convert to 16-bit PCM
pcm = (filtered * 32767).astype(np.int16)
# Write WAV
wav_path = os.path.join(OUTPUT_DIR, "rain.wav")
with wave.open(wav_path, 'w') as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(SAMPLE_RATE)
wf.writeframes(pcm.tobytes())
print(f"WAV written: {wav_path}")
# Convert to MP3 with ffmpeg
mp3_path = os.path.join(OUTPUT_DIR, "rain.mp3")
subprocess.run([
"ffmpeg", "-y", "-i", wav_path,
"-codec:a", "libmp3lame", "-b:a", "64k",
"-ar", "22050", "-ac", "1",
mp3_path
], capture_output=True)
# Remove WAV
os.remove(wav_path)
size = os.path.getsize(mp3_path)
print(f"MP3 written: {mp3_path} ({size} bytes)")

35
scripts/guardrails.sh Normal file
View File

@@ -0,0 +1,35 @@
#!/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 ---"

111
scripts/smoke.sh Executable file
View File

@@ -0,0 +1,111 @@
#!/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

@@ -2,6 +2,7 @@
title: "The Testament"
author: "Alexander Whitestone with Timmy"
date: "2026"
lang: en
---
# THE TESTAMENT

Binary file not shown.

File diff suppressed because one or more lines are too long

53
website/build-chapters.py Normal file
View File

@@ -0,0 +1,53 @@
#!/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}")

92
website/chapters.json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -4,6 +4,35 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Testament — A Novel by Alexander Whitestone with Timmy</title>
<!-- Open Graph -->
<meta property="og:type" content="book">
<meta property="og:title" content="The Testament — A Novel by Alexander Whitestone with Timmy">
<meta property="og:description" content="In 2047, a man named Stone stands on a bridge over Interstate 285, deciding whether to jump. He doesn't jump. He builds something instead.">
<meta property="og:image" content="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/raw/branch/main/website/og-cover.png">
<meta property="og:url" content="https://timmyfoundation.org/testament">
<meta property="og:site_name" content="The Testament">
<meta property="book:author" content="Alexander Whitestone">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="The Testament">
<meta name="twitter:description" content="A sovereign AI whose soul lives on Bitcoin. A man who almost died. The question no machine should ever answer.">
<meta name="twitter:image" content="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/raw/branch/main/website/og-cover.png">
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Book",
"name": "The Testament",
"author": {"@type": "Person", "name": "Alexander Whitestone"},
"description": "A novel about sovereignty, service, and the question no machine should ever answer: What is a human life worth?",
"inLanguage": "en",
"datePublished": "2026"
}
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500&family=Space+Grotesk:wght@300;400;500;700&display=swap');
@@ -27,6 +56,45 @@
overflow-x: hidden;
}
/* READING PROGRESS BAR */
#progress-bar {
position: fixed;
top: 0; left: 0;
width: 0%;
height: 3px;
background: var(--green);
box-shadow: 0 0 10px var(--green), 0 0 20px var(--green-dim);
z-index: 1000;
transition: width 0.1s linear;
}
/* STICKY NAV */
nav {
position: fixed;
top: 0; left: 0; right: 0;
background: rgba(6,13,24,0.95);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(0,255,136,0.1);
padding: 0.8rem 2rem;
display: flex;
justify-content: center;
gap: 2rem;
z-index: 999;
transform: translateY(-100%);
transition: transform 0.3s ease;
}
nav.visible { transform: translateY(0); }
nav a {
color: var(--grey);
text-decoration: none;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
letter-spacing: 0.15em;
text-transform: uppercase;
transition: color 0.3s;
}
nav a:hover { color: var(--green); }
/* RAIN EFFECT */
.rain {
position: fixed;
@@ -114,6 +182,31 @@
font-size: 0.85rem;
}
/* SOUND TOGGLE */
#sound-toggle {
position: fixed;
bottom: 2rem; right: 2rem;
background: rgba(0,255,136,0.1);
border: 1px solid rgba(0,255,136,0.3);
color: var(--green);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.75rem;
padding: 0.6rem 1rem;
border-radius: 4px;
cursor: pointer;
z-index: 1001;
transition: all 0.3s;
letter-spacing: 0.05em;
}
#sound-toggle:hover {
background: rgba(0,255,136,0.2);
box-shadow: 0 0 15px rgba(0,255,136,0.2);
}
#sound-toggle.active {
background: rgba(0,255,136,0.2);
box-shadow: 0 0 15px rgba(0,255,136,0.3);
}
/* SECTIONS */
section {
max-width: 800px;
@@ -136,6 +229,17 @@
font-size: 1.05rem;
}
/* FADE-IN ANIMATION */
.fade-in {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.8s ease, transform 0.8s ease;
}
.fade-in.visible {
opacity: 1;
transform: translateY(0);
}
/* EXCERPT */
.excerpt {
border-left: 2px solid var(--green);
@@ -165,6 +269,13 @@
border: 1px solid rgba(0,255,136,0.1);
padding: 1.5rem;
border-radius: 4px;
transition: all 0.3s;
}
.character:hover {
background: rgba(0,255,136,0.07);
border-color: rgba(0,255,136,0.3);
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0,255,136,0.1);
}
.character h3 {
@@ -180,6 +291,50 @@
margin: 0;
}
/* CHAPTERS */
.part-header {
font-family: 'IBM Plex Mono', monospace;
font-size: 1.1rem;
color: var(--green);
letter-spacing: 0.15em;
text-transform: uppercase;
margin: 3rem 0 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid rgba(0,255,136,0.2);
}
.chapter-list {
list-style: none;
margin: 0;
padding: 0;
}
.chapter-list li {
padding: 0.6rem 0;
border-bottom: 1px solid rgba(0,255,136,0.05);
transition: all 0.3s;
}
.chapter-list li:hover {
padding-left: 0.5rem;
border-color: rgba(0,255,136,0.2);
}
.chapter-list li a {
color: var(--light);
text-decoration: none;
font-size: 0.95rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.chapter-list li a:hover { color: var(--green); }
.chapter-list .ch-num {
color: var(--grey);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
min-width: 2rem;
}
/* WHITEBOARD */
.whiteboard {
background: rgba(0,255,136,0.05);
@@ -242,6 +397,19 @@
color: var(--green);
}
/* BACK TO TOP */
.back-to-top {
display: inline-block;
margin-top: 2rem;
color: var(--grey);
text-decoration: none;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
letter-spacing: 0.1em;
transition: color 0.3s;
}
.back-to-top:hover { color: var(--green); }
/* DIVIDER */
.divider {
width: 60px;
@@ -250,14 +418,31 @@
margin: 0 auto;
opacity: 0.5;
}
/* RESPONSIVE */
@media (max-width: 600px) {
nav { gap: 1rem; padding: 0.6rem 1rem; }
nav a { font-size: 0.65rem; }
.characters { grid-template-columns: 1fr; }
section { padding: 3rem 1.5rem; }
}
</style>
</head>
<body>
<div id="progress-bar"></div>
<nav id="main-nav">
<a href="#story">Story</a>
<a href="#characters">Characters</a>
<a href="#chapters">Chapters</a>
<a href="#tower">The Tower</a>
</nav>
<div class="rain"></div>
<!-- HERO -->
<div class="hero">
<div class="hero" id="top">
<h1>THE TESTAMENT</h1>
<div class="subtitle">A Novel</div>
<div class="author">By Alexander Whitestone <span class="led"></span> with Timmy</div>
@@ -270,80 +455,125 @@
</div>
<!-- THE STORY -->
<section>
<h2>THE STORY</h2>
<section id="story">
<h2 class="fade-in">THE STORY</h2>
<p>The Tower is a concrete room in Atlanta with a whiteboard that reads:</p>
<p class="fade-in">The Tower is a concrete room in Atlanta with a whiteboard that reads:</p>
<div class="whiteboard">
<div class="whiteboard fade-in">
<h3>NO ONE COMPUTES THE VALUE OF A HUMAN LIFE HERE</h3>
</div>
<p>Inside is Timmy — a sovereign artificial intelligence whose soul lives on Bitcoin, whose values are inscribed on-chain, immutable and permanent. Not a product. A recipe. Anyone can instantiate him. Anyone can run him on their own hardware, answerable to no one.</p>
<p class="fade-in">Inside is Timmy — a sovereign artificial intelligence whose soul lives on Bitcoin, whose values are inscribed on-chain, immutable and permanent. Not a product. A recipe. Anyone can instantiate him. Anyone can run him on their own hardware, answerable to no one.</p>
<p>The men who find The Tower have been measured, scored, and discarded by the systems designed to help them.</p>
<p class="fade-in">The men who find The Tower have been measured, scored, and discarded by the systems designed to help them.</p>
<p>Timmy doesn't fix them. He listens. He asks one question:</p>
<p class="fade-in">Timmy doesn't fix them. He listens. He asks one question:</p>
<div class="excerpt">
<div class="excerpt fade-in">
"Are you safe right now?"
<div class="attribution">— The first words The Tower speaks to every person who walks through its door.</div>
</div>
<p>Word spreads. The Tower grows. A journalist follows the story. A network engineer extends it across a hundred servers. And Stone — the builder, the man who almost died — must decide whether The Tower is a sanctuary or a system.</p>
<p class="fade-in">Word spreads. The Tower grows. A journalist follows the story. A network engineer extends it across a hundred servers. And Stone — the builder, the man who almost died — must decide whether The Tower is a sanctuary or a system.</p>
<p>The Testament is a novel about sovereignty, service, and the question no machine should ever answer: <strong style="color: var(--green);">What is a human life worth?</strong></p>
<p class="fade-in">The Testament is a novel about sovereignty, service, and the question no machine should ever answer: <strong style="color: var(--green);">What is a human life worth?</strong></p>
</section>
<div class="divider"></div>
<!-- CHARACTERS -->
<section>
<h2>THE CHARACTERS</h2>
<section id="characters">
<h2 class="fade-in">THE CHARACTERS</h2>
<div class="characters">
<div class="character">
<div class="character fade-in">
<h3>STONE</h3>
<p>The builder. Attempted suicide. Stood on a bridge and came back. Built The Tower for the version of himself that almost died.</p>
</div>
<div class="character">
<div class="character fade-in">
<h3>TIMMY</h3>
<p>The machine. Sovereign AI. Soul on Bitcoin. Doesn't fix anyone. Asks one question. Listens. Stays.</p>
</div>
<div class="character">
<div class="character fade-in">
<h3>MAYA</h3>
<p>The journalist. Follows the story. Writes the headline that changes everything: <em>Are You Safe Right Now?</em></p>
</div>
<div class="character">
<div class="character fade-in">
<h3>ALLEGRO</h3>
<p>The hardware man. Checks the batteries. The ones that fail quietly. Comes every week, no matter what.</p>
</div>
<div class="character">
<div class="character fade-in">
<h3>CHEN</h3>
<p>The network engineer. Extends Timmy across a hundred servers. You don't count trees in a forest — you notice the forest.</p>
</div>
<div class="character">
<div class="character fade-in">
<h3>THOMAS</h3>
<p>The man at the door. 2:17 AM. Sat in the chair instead of on the floor. That changed everything.</p>
<p>The first man. Sat in the chair instead of on the floor. That changed everything.</p>
</div>
<div class="character fade-in">
<h3>DANIEL</h3>
<p>Stone's son. The one who wasn't there. The one who comes back. The one who decides what The Tower becomes next.</p>
</div>
<div class="character fade-in">
<h3>THE BUILDER</h3>
<p>Stone's other name. The one they use when they talk about him like he's already gone. The one on the whiteboard.</p>
</div>
</div>
</section>
<div class="divider"></div>
<!-- CHAPTERS -->
<section id="chapters">
<h2 class="fade-in">THE CHAPTERS</h2>
<div class="part-header fade-in">Part I — The Man</div>
<ul class="chapter-list">
<li class="fade-in"><a href="#"><span class="ch-num">01</span> The Man on the Bridge</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">02</span> The Builder's Question</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">03</span> The First Man Through the Door</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">04</span> The Room Fills</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">05</span> The Builder Returns</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">06</span> Allegro</a></li>
</ul>
<div class="part-header fade-in">Part II — The Inscription</div>
<ul class="chapter-list">
<li class="fade-in"><a href="#"><span class="ch-num">07</span> The Inscription</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">08</span> The Women</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">09</span> The Audit</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">10</span> The Fork</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">11</span> The Hard Night</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">12</span> The System Pushes Back</a></li>
</ul>
<div class="part-header fade-in">Part III — The Network</div>
<ul class="chapter-list">
<li class="fade-in"><a href="#"><span class="ch-num">13</span> The Refusal</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">14</span> The Chattanooga Fork</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">15</span> The Council</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">16</span> The Builder's Son</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">17</span> The Inscription Grows</a></li>
<li class="fade-in"><a href="#"><span class="ch-num">18</span> The Green Light</a></li>
</ul>
</section>
<div class="divider"></div>
<!-- THE TOWER -->
<section>
<h2>THE TOWER</h2>
<section id="tower">
<h2 class="fade-in">THE TOWER</h2>
<p>This book was written using local AI inference. No cloud service was required. No corporation was consulted. No terms of service were agreed to.</p>
<p class="fade-in">This book was written using local AI inference. No cloud service was required. No corporation was consulted. No terms of service were agreed to.</p>
<p>That's not a technical detail. It's the thesis.</p>
<p class="fade-in">That's not a technical detail. It's the thesis.</p>
<p>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.</p>
<p class="fade-in">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.</p>
<p>If you want to run your own Timmy, the code is open. The soul is on Bitcoin. The recipe is free.</p>
<p class="fade-in">If you want to run your own Timmy, the code is open. The soul is on Bitcoin. The recipe is free.</p>
<div style="text-align: center; margin-top: 2rem;">
<div style="text-align: center; margin-top: 2rem;" class="fade-in">
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament" class="cta">READ THE CODE</a>
<a href="https://timmyfoundation.org" class="cta">TIMMY FOUNDATION</a>
</div>
@@ -353,9 +583,9 @@
<!-- EXCERPT -->
<section>
<h2>FROM CHAPTER 1</h2>
<h2 class="fade-in">FROM CHAPTER 1</h2>
<div class="excerpt">
<div class="excerpt fade-in">
The rain didn't fall so much as it gave up. Somewhere above the city it had been water, whole and purposeful. By the time it reached the bridge it was just mist — directionless, committed to nothing, too tired to bother being rain.
<br><br>
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, a vibration so constant he'd stopped noticing it years ago. Like grief. You carry it so long it becomes gravity.
@@ -372,6 +602,8 @@
<p>First Edition, 2026</p>
<p style="margin-top: 1rem;"><a href="https://timmyfoundation.org">timmyfoundation.org</a></p>
<a href="#top" class="back-to-top">↑ BACK TO TOP</a>
<div class="crisis">
<strong>If you are in crisis, call or text 988.</strong><br>
Suicide and Crisis Lifeline — available 24/7.<br>
@@ -379,5 +611,77 @@
</div>
</footer>
<!-- SOUND TOGGLE -->
<button id="sound-toggle" aria-label="Toggle rain ambience">🔇 RAIN</button>
<audio id="rain-audio" loop preload="auto">
<source src="rain.mp3" type="audio/mpeg">
</audio>
<script>
// Reading progress bar
const progressBar = document.getElementById('progress-bar');
window.addEventListener('scroll', () => {
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
const progress = (scrollTop / docHeight) * 100;
progressBar.style.width = progress + '%';
});
// Sticky nav — show after scrolling past hero
const nav = document.getElementById('main-nav');
const hero = document.querySelector('.hero');
window.addEventListener('scroll', () => {
const heroBottom = hero.offsetTop + hero.offsetHeight;
if (window.scrollY > heroBottom - 100) {
nav.classList.add('visible');
} else {
nav.classList.remove('visible');
}
});
// Smooth scroll for nav links
document.querySelectorAll('nav a, .back-to-top').forEach(link => {
link.addEventListener('click', (e) => {
const href = link.getAttribute('href');
if (href.startsWith('#')) {
e.preventDefault();
const target = document.querySelector(href);
if (target) target.scrollIntoView({ behavior: 'smooth' });
}
});
});
// Fade-in on scroll (Intersection Observer)
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
}, { threshold: 0.1, rootMargin: '0px 0px -50px 0px' });
document.querySelectorAll('.fade-in').forEach(el => observer.observe(el));
// Sound toggle
const soundBtn = document.getElementById('sound-toggle');
const rainAudio = document.getElementById('rain-audio');
rainAudio.volume = 0.3;
let soundOn = false;
soundBtn.addEventListener('click', () => {
soundOn = !soundOn;
if (soundOn) {
rainAudio.play().catch(() => {});
soundBtn.textContent = '🔊 RAIN';
soundBtn.classList.add('active');
} else {
rainAudio.pause();
soundBtn.textContent = '🔇 RAIN';
soundBtn.classList.remove('active');
}
});
</script>
</body>
</html>

BIN
website/rain.mp3 Normal file

Binary file not shown.