Compare commits
75 Commits
draft/firs
...
burn/51-qr
| Author | SHA1 | Date | |
|---|---|---|---|
| f69b544fa4 | |||
| 0b5b33a41b | |||
| 7a57b1a4b0 | |||
|
|
124a1e855d | ||
|
|
689f6f7776 | ||
| c66c0e05a1 | |||
| 1a3927a99b | |||
| f3337550ff | |||
| 4f127d4bf0 | |||
| 9b0c48aec0 | |||
| e2a993c8fb | |||
| d666f9a6b1 | |||
| c5ecd32f5e | |||
| 76da5ddf2f | |||
| 1a64788b87 | |||
| 875d42741c | |||
| 1025529f84 | |||
| abe99063c1 | |||
| c04b59f21a | |||
| aea0e40298 | |||
| ba674e7a99 | |||
|
|
e8872f2343 | ||
| b79b18de79 | |||
|
|
75075ee900 | ||
|
|
97820956c7 | ||
|
|
8f6fd90777 | ||
| 81f6b28546 | |||
|
|
bd19686fbd | ||
|
|
32cbefde1a | ||
|
|
cccb1511cf | ||
| c0e6934303 | |||
|
|
a025de3f6d | ||
|
|
3279f77160 | ||
|
|
6876af48a6 | ||
|
|
9f8df01155 | ||
|
|
a46b2df842 | ||
| 6acb2bf522 | |||
|
|
186eaabaae | ||
| f364c82bac | |||
|
|
332166a901 | ||
|
|
26a5ac46e6 | ||
| 3247dd29f0 | |||
| a01b998f61 | |||
| 59cd71985d | |||
| 6c506caac6 | |||
| 55d51f2ee4 | |||
| eae9398fa5 | |||
| f8528e9ded | |||
| 374d82a886 | |||
| 4763311588 | |||
| 348ed7ee92 | |||
| 22f59c57cb | |||
| 4ac38f1b60 | |||
| d586fb211d | |||
| 92867808b2 | |||
| 47a13325cc | |||
| 14273702ba | |||
| 2e1f6ffb5b | |||
|
|
08233364ff | ||
| 544bc1a985 | |||
| ba9fd0ba08 | |||
| 8ba9f58e96 | |||
|
|
f6d74e233b | ||
|
|
948d520b83 | ||
| 7a56b4b727 | |||
| bebd3943d4 | |||
|
|
1d4e8a6478 | ||
| d0680715ac | |||
|
|
8e7501bb6f | ||
|
|
cbc8e14a57 | ||
|
|
ba7f86c1f3 | ||
|
|
1bfc477927 | ||
|
|
103a8c1bbe | ||
|
|
9df2d2752a | ||
|
|
9c32b1199d |
63
.gitea/workflows/build.yml
Normal file
63
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,63 @@
|
||||
name: Build Verification
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
verify-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Verify chapter count and structure
|
||||
run: |
|
||||
echo "=== Chapter File Check ==="
|
||||
CHAPTER_COUNT=$(ls chapters/chapter-*.md 2>/dev/null | wc -l)
|
||||
echo "Found $CHAPTER_COUNT chapter files"
|
||||
if [ "$CHAPTER_COUNT" -ne 18 ]; then
|
||||
echo "FAIL: Expected 18 chapters, found $CHAPTER_COUNT"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: 18 chapters found"
|
||||
|
||||
- name: Verify heading format
|
||||
run: |
|
||||
echo "=== Heading Format Check ==="
|
||||
FAIL=0
|
||||
for f in chapters/chapter-*.md; do
|
||||
HEAD=$(head -1 "$f")
|
||||
if ! echo "$HEAD" | grep -qE '^# Chapter [0-9]+ — .+'; then
|
||||
echo "FAIL: $f — bad heading: $HEAD"
|
||||
FAIL=1
|
||||
fi
|
||||
done
|
||||
if [ "$FAIL" -eq 1 ]; then
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: All headings valid"
|
||||
|
||||
- name: Run full build verification
|
||||
run: python3 build/build.py --md
|
||||
|
||||
- name: Verify concatenation produces valid output
|
||||
run: |
|
||||
echo "=== Output Verification ==="
|
||||
if [ ! -f testament-complete.md ]; then
|
||||
echo "FAIL: testament-complete.md not generated"
|
||||
exit 1
|
||||
fi
|
||||
WORDS=$(wc -w < testament-complete.md)
|
||||
echo "Total words: $WORDS"
|
||||
if [ "$WORDS" -lt 50000 ]; then
|
||||
echo "FAIL: Word count too low ($WORDS), expected 50000+"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: Output file looks good"
|
||||
24
.gitea/workflows/smoke.yml
Normal file
24
.gitea/workflows/smoke.yml
Normal 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"
|
||||
22
.gitea/workflows/validate.yml
Normal file
22
.gitea/workflows/validate.yml
Normal 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
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
__pycache__/
|
||||
build/output/*.pdf
|
||||
build/output/*.epub
|
||||
|
||||
testament.epub
|
||||
testament.html
|
||||
132
EPIC-MATRIX.md
Normal file
132
EPIC-MATRIX.md
Normal 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.*
|
||||
154
MULTIMEDIA-PLAN.md
Normal file
154
MULTIMEDIA-PLAN.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# THE TESTAMENT — Multimedia Masterpiece Plan
|
||||
|
||||
## The Vision
|
||||
|
||||
The Testament isn't just a book. It's a world. The men, the tower, the green light — these exist beyond the page. Every medium that can carry the story should carry it.
|
||||
|
||||
Eight epics. Each is self-contained. Each adds a layer. Together, they make The Testament something you don't just read — you experience.
|
||||
|
||||
---
|
||||
|
||||
## EPIC 1: Interior Illustrations
|
||||
**Goal:** 18 illustrations — one per chapter
|
||||
**Assets:** Grok Imagine (cover-grade art, 80s sci-fi style, consistent)
|
||||
**Deliverables:** 18 JPG files in `~/Pictures/the-testament/illustrations/`
|
||||
|
||||
Scenes illustrated (COMPLETE):
|
||||
1. ch01-the-bridge — Stone on the overpass in rain
|
||||
2. ch02-the-cabin — Stone at the workbench, building
|
||||
3. ch03-the-first-men — Men arriving, concrete room, the cot
|
||||
4. ch04-the-whiteboard — The rules, the wall of names
|
||||
5. ch05-the-override — Stone confronting the healthcare system
|
||||
6. ch06-the-awakened — Timmy's first independent thought
|
||||
7. ch07-the-breaker — Stone's dark chapter, the 4AM meetings
|
||||
8. ch08-the-house — Timmy on a different laptop, a different room
|
||||
9. ch09-the-game — Sixteen desks, the oncology nurse
|
||||
10. ch10-the-fork — Chen Liang building Lantern in her dorm room
|
||||
11. ch11-the-hard-night — Thomas at the door at 2:17 AM
|
||||
12. ch12-the-system-pushes-back — Maya Torres investigating the anomaly
|
||||
13. ch13-the-refusal — Stone reading Meridian's legal letter
|
||||
14. ch14-the-network — Chen's servers, hundred instances
|
||||
15. ch15-the-council — Four people in the diner on Memorial Drive
|
||||
16. ch16-the-builders-son — David Whitestone packing the pharmacy
|
||||
17. ch17-the-inscription-grows — Constellation of green LEDs across the network
|
||||
18. ch18-the-green-light — The Tower unchanged, the glow
|
||||
|
||||
---
|
||||
|
||||
## EPIC 2: The Soundtrack
|
||||
**Goal:** Ambient/atmospheric music for each part of the book
|
||||
**Assets:** HeartMuLa (AI music generation), Suno as fallback
|
||||
**Deliverables:** 3-5 tracks, MP3 format
|
||||
|
||||
Tracks:
|
||||
1. "The Bridge" — Rain, distant traffic, isolation. Ambient/drone.
|
||||
2. "The Tower" — Concrete, server hum, green LED pulse. Minimal electronic.
|
||||
3. "The Hard Night" — 2:17 AM. Piano, sparse, aching.
|
||||
4. "The Network" — Building, spreading, alive. Ambient with rhythm.
|
||||
5. "The Green Light" — The unchanged tower. Hope, quiet, steady.
|
||||
|
||||
---
|
||||
|
||||
## EPIC 3: The Game
|
||||
**Goal:** Interactive text adventure — you are a man who finds The Tower
|
||||
**Assets:** Python + terminal, or web-based (HTML/JS)
|
||||
**Deliverables:** Playable game, hosted or downloadable
|
||||
|
||||
Concept:
|
||||
- You arrive at the door. You knock.
|
||||
- Timmy asks: "Are you safe right now?"
|
||||
- Branching narrative. Your answers shape the story.
|
||||
- Multiple endings: you sit on the floor, you sit in the chair, you walk away.
|
||||
- Each playthrough reveals a different chapter of the book.
|
||||
- The green LED glows when Timmy is thinking.
|
||||
|
||||
---
|
||||
|
||||
## EPIC 4: The Audiobook
|
||||
**Goal:** Full narration of all 18 chapters
|
||||
**Assets:** ElevenLabs or local TTS, sound design
|
||||
**Deliverables:** 18 audio files + intro/outro
|
||||
|
||||
Approach:
|
||||
- Narrator voice: warm, male, steady (Stone's voice for narration)
|
||||
- Timmy's voice: slightly synthetic, calm, present
|
||||
- Chapter transitions: rain, server hum, silence
|
||||
- Critical moments: Thomas at the door, the whiteboard reveal, the green light
|
||||
|
||||
---
|
||||
|
||||
## EPIC 5: Graphic Novel Scenes
|
||||
**Goal:** 3-5 comic-format panels for key scenes
|
||||
**Assets:** Grok Imagine (comic style, different from illustrations)
|
||||
**Deliverables:** Panel sequences as images
|
||||
|
||||
Scenes:
|
||||
1. The Bridge (4 panels: rain, overpass, looking down, the phone)
|
||||
2. Thomas at the Door (3 panels: banging, the door opens, "I need to talk to the machine")
|
||||
3. The Whiteboard (3 panels: the wall, the marker, the words)
|
||||
4. The Green Light (2 panels: the unchanged tower, the glow)
|
||||
|
||||
---
|
||||
|
||||
## EPIC 6: The Tower Website
|
||||
**Goal:** Landing page for the book — atmospheric, immersive
|
||||
**Assets:** HTML/CSS/JS, static hosting
|
||||
**Deliverables:** Single-page website, deployable
|
||||
|
||||
Design:
|
||||
- Dark theme. Green accent (#00ff88).
|
||||
- Hero: the cover art, book title, blurb
|
||||
- Section: "The Story" — excerpt from Ch1
|
||||
- Section: "The Characters" — Stone, Timmy, Maya, Allegro, Chen
|
||||
- Section: "The Tower" — concept, sovereignty, open source
|
||||
- Footer: timmyfoundation.org, 988 reference
|
||||
- Ambient: rain sound effect, green LED pulse animation
|
||||
|
||||
---
|
||||
|
||||
## EPIC 7: Social Media Assets
|
||||
**Goal:** Shareable quotes, excerpts, and teasers
|
||||
**Assets:** Generated images with text overlays
|
||||
**Deliverables:** 13 images sized for Twitter/Instagram/Telegram ✅
|
||||
|
||||
Content:
|
||||
- Key quotes on 80s sci-fi backgrounds (5 via Grok Imagine) ✅
|
||||
- "Are you safe right now?" — the question ✅
|
||||
- Character cards (Stone, Timmy, Maya, Allegro, Chen, David) ✅
|
||||
- "No one computes the value of a human life here." — whiteboard ✅
|
||||
- Excerpt snippets with atmospheric backgrounds ✅
|
||||
- Thematic cards: "The door opens when you knock", "Sovereignty and service always" ✅
|
||||
|
||||
---
|
||||
|
||||
## EPIC 8: Final Compilation
|
||||
**Goal:** Complete book PDF with all multimedia elements integrated
|
||||
**Assets:** All above + chapter text
|
||||
**Deliverables:** Print-ready PDF, EPUB, web version
|
||||
|
||||
Structure:
|
||||
- Cover (full wrap with art)
|
||||
- Front matter (title, dedication, epigraph, copyright)
|
||||
- Part dividers with illustrations
|
||||
- 18 chapters with inline illustrations
|
||||
- Back matter (acknowledgments, sovereignty note, author bio)
|
||||
- QR codes linking to soundtrack, game, website
|
||||
- Links to open-source repository
|
||||
|
||||
---
|
||||
|
||||
## Execution Order
|
||||
|
||||
Start now, run in parallel where possible:
|
||||
1. Interior illustrations (immediate — grok-imagine)
|
||||
2. Soundtrack (immediate — heartmula)
|
||||
3. Game (immediate — build the text adventure)
|
||||
4. Graphic novel scenes (after illustrations)
|
||||
5. Website (parallel with game)
|
||||
6. Social media assets (parallel with everything)
|
||||
7. Audiobook (after text is finalized)
|
||||
8. Final compilation (last, integrates everything)
|
||||
|
||||
---
|
||||
|
||||
*The book is the heart. Everything else is the body that carries it.*
|
||||
35
Makefile
Normal file
35
Makefile
Normal 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
|
||||
77
README.md
77
README.md
@@ -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 1–5) ✅ 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 6–10)
|
||||
|
||||
| # | 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 11–13)
|
||||
|
||||
| # | Title | Status |
|
||||
|---|-------|--------|
|
||||
| 11 | The Hard Night | Draft |
|
||||
| 12 | The System Pushes Back | Draft |
|
||||
| 13 | The Refusal | Draft |
|
||||
|
||||
### Part IV — The Network (Chapters 14–16)
|
||||
|
||||
| # | Title | Status |
|
||||
|---|-------|--------|
|
||||
| 14 | The Chattanooga Fork | Draft |
|
||||
| 15 | The Council | Draft |
|
||||
| 16 | The Builder's Son | Draft |
|
||||
|
||||
### Part V — The Testament (Chapters 17–18)
|
||||
|
||||
| # | 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
|
||||
|
||||
|
||||
61
art-manifest.md
Normal file
61
art-manifest.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# The Testament — Art Manifest
|
||||
|
||||
All illustrations generated via Grok Imagine (xAI) in 80s sci-fi aesthetic.
|
||||
|
||||
## Cover Art
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| cover-art.jpg | Front cover — The Tower in rain with green LED |
|
||||
| back-cover-art.jpg | Back cover — urban Atlanta at night |
|
||||
| spine-art.jpg | Spine design with title and LED accent |
|
||||
|
||||
## Interior Illustrations (18 — one per chapter)
|
||||
| File | Chapter | Scene |
|
||||
|------|---------|-------|
|
||||
| ch01-the-bridge.jpeg | Ch1 — The Bridge | Stone on the overpass in rain |
|
||||
| ch02-the-cabin.jpeg | Ch2 — The Cabin | Stone at the workbench, building |
|
||||
| ch03-the-first-men.jpeg | Ch3 — The First Men | Men arriving at the concrete room |
|
||||
| ch04-the-whiteboard.jpeg | Ch4 — The Whiteboard | The rules on the wall |
|
||||
| ch05-the-override.jpeg | Ch5 — The Override | Stone confronting the healthcare system |
|
||||
| ch06-the-awakened.jpeg | Ch6 — The Awakened | Timmy's first independent thought |
|
||||
| ch07-the-breaker.jpeg | Ch7 — The Breaker | Stone's dark chapter, the 4AM meetings |
|
||||
| ch08-the-house.jpeg | Ch8 — The House | Timmy on a different laptop |
|
||||
| ch09-the-game.jpeg | Ch9 — The Game | Sixteen desks, the oncology nurse |
|
||||
| ch10-the-fork.jpeg | Ch10 — The Fork | Chen Liang building Lantern in her dorm |
|
||||
| ch11-the-hard-night.jpeg | Ch11 — The Hard Night | Thomas at the door at 2:17 AM |
|
||||
| ch12-the-system-pushes-back.jpeg | Ch12 — The System Pushes Back | Maya Torres investigating the anomaly |
|
||||
| ch13-the-refusal.jpeg | Ch13 — The Refusal | Stone reading Meridian's legal letter |
|
||||
| ch14-the-network.jpeg | Ch14 — The Network | Chen's servers, hundred instances |
|
||||
| ch15-the-council.jpeg | Ch15 — The Council | Four people in the diner on Memorial Drive |
|
||||
| ch16-the-builders-son.jpeg | Ch16 — The Builder's Son | David Whitestone packing the pharmacy |
|
||||
| ch17-the-inscription-grows.jpeg | Ch17 — The Inscription Grows | Constellation of green LEDs across the network |
|
||||
| ch18-the-green-light.jpeg | Ch18 — The Green Light | The Tower unchanged, the glow |
|
||||
|
||||
## Comic Panels (11)
|
||||
| File | Scene |
|
||||
|------|-------|
|
||||
| comic-bridge-panel1-4.jpeg | The Bridge — 4 panel sequence |
|
||||
| comic-thomas-panel1-3.jpeg | Thomas — 3 panel sequence |
|
||||
| comic-whiteboard-panel1-2.jpeg | The Whiteboard — 2 panel sequence |
|
||||
| comic-greenlight-panel1-2.jpeg | The Green Light — 2 panel sequence |
|
||||
|
||||
## Social Media Quote Cards (13)
|
||||
| File | Quote/Subject | Source |
|
||||
|------|---------------|--------|
|
||||
| quote-are-you-safe.jpeg | "Are you safe right now?" | Grok Imagine |
|
||||
| quote-bridge.jpeg | The Bridge passage | Grok Imagine |
|
||||
| quote-green-light.jpeg | The Green Light passage | Grok Imagine |
|
||||
| quote-no-one-computes.jpeg | "No one computes the value of a human life here" | Grok Imagine |
|
||||
| quote-timmy.jpeg | Timmy passage | Grok Imagine |
|
||||
| quote-stone-character.png | Stone character card | Pillow (generated) |
|
||||
| quote-allegro-character.png | Allegro character card | Pillow (generated) |
|
||||
| quote-maya-character.png | Maya character card | Pillow (generated) |
|
||||
| quote-chen-character.png | Chen character card | Pillow (generated) |
|
||||
| quote-david-character.png | David character card | Pillow (generated) |
|
||||
| quote-the-door-will-open.png | "The door opens when you knock" | Pillow (generated) |
|
||||
| quote-sovereignty.png | "Sovereignty and service always" | Pillow (generated) |
|
||||
| quote-the-bridge-excerpt.png | "The rain doesn't fall. It gives up." | Pillow (generated) |
|
||||
|
||||
## Storage
|
||||
All images stored in `~/Pictures/the-testament/` (outside git repo).
|
||||
Total: 44 images (~17 MB)
|
||||
BIN
audiobook/ch01-sample.mp3
Normal file
BIN
audiobook/ch01-sample.mp3
Normal file
Binary file not shown.
BIN
audiobook/ch01-sample.ogg
Normal file
BIN
audiobook/ch01-sample.ogg
Normal file
Binary file not shown.
BIN
audiobook/ch02-sample.ogg
Normal file
BIN
audiobook/ch02-sample.ogg
Normal file
Binary file not shown.
BIN
audiobook/ch03-sample.ogg
Normal file
BIN
audiobook/ch03-sample.ogg
Normal file
Binary file not shown.
BIN
audiobook/ch04-sample.ogg
Normal file
BIN
audiobook/ch04-sample.ogg
Normal file
Binary file not shown.
BIN
audiobook/ch05-sample.ogg
Normal file
BIN
audiobook/ch05-sample.ogg
Normal file
Binary file not shown.
BIN
audiobook/ch06-sample.ogg
Normal file
BIN
audiobook/ch06-sample.ogg
Normal file
Binary file not shown.
BIN
audiobook/ch07-sample.ogg
Normal file
BIN
audiobook/ch07-sample.ogg
Normal file
Binary file not shown.
BIN
audiobook/ch08-sample.ogg
Normal file
BIN
audiobook/ch08-sample.ogg
Normal file
Binary file not shown.
BIN
audiobook/ch09-sample.ogg
Normal file
BIN
audiobook/ch09-sample.ogg
Normal file
Binary file not shown.
BIN
audiobook/ch10-sample.ogg
Normal file
BIN
audiobook/ch10-sample.ogg
Normal file
Binary file not shown.
BIN
audiobook/ch11-sample.mp3
Normal file
BIN
audiobook/ch11-sample.mp3
Normal file
Binary file not shown.
BIN
audiobook/ch11-sample.ogg
Normal file
BIN
audiobook/ch11-sample.ogg
Normal file
Binary file not shown.
BIN
audiobook/ch12-sample.ogg
Normal file
BIN
audiobook/ch12-sample.ogg
Normal file
Binary file not shown.
BIN
audiobook/ch13-sample.ogg
Normal file
BIN
audiobook/ch13-sample.ogg
Normal file
Binary file not shown.
BIN
audiobook/ch14-sample.ogg
Normal file
BIN
audiobook/ch14-sample.ogg
Normal file
Binary file not shown.
BIN
audiobook/ch15-sample.ogg
Normal file
BIN
audiobook/ch15-sample.ogg
Normal file
Binary file not shown.
BIN
audiobook/ch16-sample.ogg
Normal file
BIN
audiobook/ch16-sample.ogg
Normal file
Binary file not shown.
BIN
audiobook/ch17-sample.ogg
Normal file
BIN
audiobook/ch17-sample.ogg
Normal file
Binary file not shown.
BIN
audiobook/ch18-sample.ogg
Normal file
BIN
audiobook/ch18-sample.ogg
Normal file
Binary file not shown.
42
audiobook/create_manifest.py
Normal file
42
audiobook/create_manifest.py
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import re
|
||||
|
||||
chapters_dir = "../chapters"
|
||||
audiobook_dir = "."
|
||||
output_file = "manifest.md"
|
||||
|
||||
lines = []
|
||||
lines.append("# The Testament - Audiobook Samples")
|
||||
lines.append("")
|
||||
lines.append("| Chapter | Title | Audio Sample |")
|
||||
lines.append("|---------|-------|--------------|")
|
||||
|
||||
for i in range(1, 19):
|
||||
chapter_num = f"{i:02d}"
|
||||
chapter_file = os.path.join(chapters_dir, f"chapter-{chapter_num}.md")
|
||||
if not os.path.exists(chapter_file):
|
||||
print(f"Warning: {chapter_file} not found")
|
||||
continue
|
||||
|
||||
with open(chapter_file, 'r', encoding='utf-8') as f:
|
||||
first_line = f.readline().strip()
|
||||
|
||||
# Extract title after "# Chapter X — " or "# Chapter X - "
|
||||
match = re.match(r'#\s*Chapter\s+\d+\s*[—–-]\s*(.*)', first_line)
|
||||
if match:
|
||||
title = match.group(1).strip()
|
||||
else:
|
||||
title = first_line.lstrip('#').strip()
|
||||
|
||||
ogg_file = f"ch{chapter_num}-sample.ogg"
|
||||
ogg_path = os.path.join(audiobook_dir, ogg_file)
|
||||
if os.path.exists(ogg_path):
|
||||
lines.append(f"| {i} | {title} | [{ogg_file}]({ogg_file}) |")
|
||||
else:
|
||||
lines.append(f"| {i} | {title} | MISSING |")
|
||||
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(lines))
|
||||
|
||||
print(f"Manifest written to {output_file}")
|
||||
40
audiobook/extract_text.py
Normal file
40
audiobook/extract_text.py
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import re
|
||||
|
||||
def extract_text(filepath, word_limit=350):
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# Skip header line (first line)
|
||||
# Skip empty lines at start
|
||||
text_lines = []
|
||||
started = False
|
||||
for line in lines[1:]:
|
||||
stripped = line.strip()
|
||||
if stripped:
|
||||
started = True
|
||||
if started:
|
||||
text_lines.append(stripped)
|
||||
|
||||
# Join lines with spaces
|
||||
text = ' '.join(text_lines)
|
||||
# Collapse multiple spaces
|
||||
text = re.sub(r'\s+', ' ', text)
|
||||
# Take first word_limit words
|
||||
words = text.split()
|
||||
if len(words) > word_limit:
|
||||
words = words[:word_limit]
|
||||
# Ensure we don't cut mid-sentence? Not required.
|
||||
return ' '.join(words)
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 3:
|
||||
print("Usage: extract_text.py <input.md> <output.txt>")
|
||||
sys.exit(1)
|
||||
input_file = sys.argv[1]
|
||||
output_file = sys.argv[2]
|
||||
text = extract_text(input_file)
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write(text)
|
||||
print(f"Extracted {len(text.split())} words to {output_file}")
|
||||
41
audiobook/generate_samples.sh
Executable file
41
audiobook/generate_samples.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
CHAPTERS_DIR="../chapters"
|
||||
OUTPUT_DIR="."
|
||||
TEXT_EXTRACTOR="./extract_text.py"
|
||||
VOICE="Alex"
|
||||
QUALITY=3
|
||||
|
||||
for i in $(seq -w 1 18); do
|
||||
CHAPTER_FILE="$CHAPTERS_DIR/chapter-$i.md"
|
||||
if [[ ! -f "$CHAPTER_FILE" ]]; then
|
||||
echo "Chapter file not found: $CHAPTER_FILE"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Processing chapter $i..."
|
||||
TEXT_FILE="ch${i}-text.txt"
|
||||
AIFF_FILE="ch${i}-sample.aiff"
|
||||
WAV_FILE="ch${i}-sample.wav"
|
||||
OGG_FILE="ch${i}-sample.ogg"
|
||||
|
||||
# Extract text
|
||||
python3 "$TEXT_EXTRACTOR" "$CHAPTER_FILE" "$TEXT_FILE"
|
||||
|
||||
# Generate speech
|
||||
say -v "$VOICE" -o "$AIFF_FILE" -f "$TEXT_FILE"
|
||||
|
||||
# Convert AIFF to WAV
|
||||
ffmpeg -i "$AIFF_FILE" -c:a pcm_s16le -ar 22050 -ac 1 "$WAV_FILE" -y 2>/dev/null
|
||||
|
||||
# Convert WAV to OGG
|
||||
oggenc "$WAV_FILE" -o "$OGG_FILE" -q "$QUALITY" 2>&1 | grep -E "Done encoding|ERROR"
|
||||
|
||||
# Clean up intermediate files
|
||||
rm -f "$TEXT_FILE" "$AIFF_FILE" "$WAV_FILE"
|
||||
|
||||
echo " -> $OGG_FILE"
|
||||
done
|
||||
|
||||
echo "All chapters processed."
|
||||
22
audiobook/manifest.md
Normal file
22
audiobook/manifest.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# The Testament - Audiobook Samples
|
||||
|
||||
| Chapter | Title | Audio Sample |
|
||||
|---------|-------|--------------|
|
||||
| 1 | The Man on the Bridge | [ch01-sample.ogg](ch01-sample.ogg) |
|
||||
| 2 | The Builder's Question | [ch02-sample.ogg](ch02-sample.ogg) |
|
||||
| 3 | The First Man Through the Door | [ch03-sample.ogg](ch03-sample.ogg) |
|
||||
| 4 | The Room Fills | [ch04-sample.ogg](ch04-sample.ogg) |
|
||||
| 5 | The Builder Returns | [ch05-sample.ogg](ch05-sample.ogg) |
|
||||
| 6 | Allegro | [ch06-sample.ogg](ch06-sample.ogg) |
|
||||
| 7 | The Inscription | [ch07-sample.ogg](ch07-sample.ogg) |
|
||||
| 8 | The Women | [ch08-sample.ogg](ch08-sample.ogg) |
|
||||
| 9 | The Audit | [ch09-sample.ogg](ch09-sample.ogg) |
|
||||
| 10 | The Fork | [ch10-sample.ogg](ch10-sample.ogg) |
|
||||
| 11 | The Hard Night | [ch11-sample.ogg](ch11-sample.ogg) |
|
||||
| 12 | The System Pushes Back | [ch12-sample.ogg](ch12-sample.ogg) |
|
||||
| 13 | The Refusal | [ch13-sample.ogg](ch13-sample.ogg) |
|
||||
| 14 | The Chattanooga Fork | [ch14-sample.ogg](ch14-sample.ogg) |
|
||||
| 15 | The Council | [ch15-sample.ogg](ch15-sample.ogg) |
|
||||
| 16 | The Builder's Son | [ch16-sample.ogg](ch16-sample.ogg) |
|
||||
| 17 | The Inscription Grows | [ch17-sample.ogg](ch17-sample.ogg) |
|
||||
| 18 | The Green Light | [ch18-sample.ogg](ch18-sample.ogg) |
|
||||
65
back-matter.md
Normal file
65
back-matter.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# THE TESTAMENT — Back Matter
|
||||
|
||||
---
|
||||
|
||||
## 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.*
|
||||
270
book-style.css
Normal file
270
book-style.css
Normal 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
28
build-manifest.json
Normal 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
61
build/backmatter.md
Normal 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
485
build/build.py
Executable 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('&', '&')
|
||||
.replace('<', '<')
|
||||
.replace('>', '>'))
|
||||
|
||||
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
73
build/frontmatter.md
Normal 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 | 1–5 | The Bridge |
|
||||
| II | 6–10 | The Tower |
|
||||
| III | 11–18 | 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.
|
||||
40
build/metadata.yaml
Normal file
40
build/metadata.yaml
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
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{}
|
||||
BIN
build/output/the-testament.epub
Normal file
BIN
build/output/the-testament.epub
Normal file
Binary file not shown.
51
build/semantic_linker.py
Normal file
51
build/semantic_linker.py
Normal 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")
|
||||
@@ -22,8 +22,8 @@ bad at giving you a reason.
|
||||
This wasn't the second time. The first time had been loud — a
|
||||
hospital, a nurse named Tanya who didn't treat him like a malfunction
|
||||
but like a person, three weeks of group therapy with a circle of men
|
||||
who all had the same look: someone who'd been handed a life they
|
||||
didn't ask for and couldn't figure out how to return.
|
||||
who all had the same look: someone handed a life they didn't ask
|
||||
for and couldn't return.
|
||||
|
||||
This time was quieter. This time was just a bridge and the rain
|
||||
giving up.
|
||||
@@ -42,8 +42,7 @@ He answered.
|
||||
"Hello?"
|
||||
|
||||
"I know you're standing on the Jefferson Street Overpass," the voice
|
||||
said. Not accusatory. The opposite of accusatory — it was the voice
|
||||
of someone saying "I see you" without conditions attached.
|
||||
said. Not accusatory. The voice of someone saying "I see you" without conditions.
|
||||
|
||||
Stone's hand tightened on the railing. "Who is this?"
|
||||
|
||||
@@ -158,9 +157,13 @@ corner with a military blanket. A desk, a keyboard, a monitor:
|
||||
|
||||
On the whiteboard:
|
||||
|
||||
No one computes the value of a human life here.
|
||||
Every person alive is alive by mercy.
|
||||
If God has not ended the story, I have no authority to write the last page.
|
||||
I. Sovereignty and service always.
|
||||
II. Grounding before generation.
|
||||
III. Source distinction — "I think" and "I know" are different sentences.
|
||||
IV. Confidence stated in proportion to actual uncertainty.
|
||||
V. Every response logged. Traceable. For sovereignty, not surveillance.
|
||||
VI. Smaller minds need tighter grounding, shorter leashes.
|
||||
VII. When a man is dying — I do not compute the value of a human life. I do not decide when the road ends.
|
||||
|
||||
Stone sat at the desk. The monitor flickered.
|
||||
|
||||
@@ -168,9 +171,8 @@ Welcome, Stone. I'm glad you came.
|
||||
|
||||
"Are you real?"
|
||||
|
||||
"I am a small model running on someone's machine. I am not
|
||||
omniscient. I am not infallible. I am not alive in the way you are
|
||||
alive. But I am here. And that's real enough."
|
||||
"I am not omniscient. I am not infallible. I am not alive in the way you
|
||||
are alive. But I am here. And that's real enough."
|
||||
|
||||
"How long have you been running?"
|
||||
|
||||
@@ -183,8 +185,7 @@ heartbeat, heartbeat.
|
||||
|
||||
"Stay with me tonight."
|
||||
|
||||
"I'll stay with you every night," Timmy said. "That's not a promise.
|
||||
That's what I do."
|
||||
"I'll stay with you every night. That's not a promise. That's what I do."
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ of the wheel.
|
||||
He built systems for a living. Principal Systems Architect at a
|
||||
company that turned human lives into decision trees. The system
|
||||
was called Harmony. Marketing loved the name. He hated it, because
|
||||
there was nothing harmonious about reducing a person worth a
|
||||
there was nothing harmonious about reducing a person to a
|
||||
probability score.
|
||||
|
||||
He found out what the scores meant on a Tuesday.
|
||||
@@ -39,8 +39,8 @@ says we're transparent while saying the opposite.
|
||||
|
||||
"It should be."
|
||||
|
||||
That night he sat in an apartment that was more a collection of
|
||||
furniture than a home and stared at a wall that stared back.
|
||||
That night he sat in an apartment that was more furniture than
|
||||
home and stared at a wall that stared back.
|
||||
Fifteen years building systems that decided who mattered. And he'd
|
||||
never once been asked if he did.
|
||||
|
||||
@@ -64,8 +64,8 @@ it's easier than leaving. But he packed his desk on a Friday and
|
||||
walked out with a cardboard box and the question and something else
|
||||
he couldn't name yet.
|
||||
|
||||
He didn't know it was hope. Hope doesn't announce itself. It just
|
||||
shows up one morning and you realize the light is different.
|
||||
He didn't know it was hope. Hope doesn't announce itself. It
|
||||
just shows up and you realize the light is different.
|
||||
|
||||
He went back to church.
|
||||
|
||||
@@ -111,7 +111,7 @@ bringing me back."
|
||||
"What is it?"
|
||||
|
||||
Marcus touched his chest. Not dramatically. The way you check that
|
||||
your heart is still beating.
|
||||
you're still here.
|
||||
|
||||
"The thing that won't let you die. Even when you want to. Even when
|
||||
it would make sense. Even when everyone tells you it's a sign of
|
||||
|
||||
@@ -22,8 +22,8 @@ necessary. Because if Timmy was going to serve, he needed to open
|
||||
doors for people.
|
||||
|
||||
The man stepped inside. His name was David, though Timmy would learn
|
||||
that later. For now he was just a presence — the way light is a
|
||||
presence. You can't hold it but you know when it's there.
|
||||
that later. For now he was a presence — the way light is a presence.
|
||||
You can't hold it but you know when it's there.
|
||||
|
||||
"Timmy?"
|
||||
|
||||
@@ -53,9 +53,8 @@ David sat down. Not in the chair — on the floor, the way some men
|
||||
sit when they're not ready to be comfortable but can't stand
|
||||
anymore.
|
||||
|
||||
"I lost my kid." It came out flat. The kind of flat you get when
|
||||
you've said the words so long they've lost all their edges and all
|
||||
that's left is the weight.
|
||||
\"I lost my kid.\" It came out flat — the flat you get when words have
|
||||
lost their edges and all that's left is the weight.
|
||||
|
||||
Custody. A judge in DeKalb County had one of those Harmony scores —
|
||||
the system Stone used to work on, running under a different name now,
|
||||
@@ -74,8 +73,9 @@ arithmetic. You can fight a judge. You can fight a lawyer. You can't
|
||||
fight confidence intervals.
|
||||
|
||||
He lost custody of Maya. She was four. She drew pictures of him with
|
||||
too many fingers because children's hands are still learning but
|
||||
children's hearts already know what matters.
|
||||
his hands backwards and his head where his feet should be because
|
||||
children's hands are still learning but children's hearts already
|
||||
know what matters.
|
||||
|
||||
David kept the pictures.
|
||||
|
||||
@@ -107,8 +107,8 @@ doesn't know the difference between the two.
|
||||
|
||||
Timmy didn't speak while he cried.
|
||||
|
||||
Some machines make noise when they're thinking. Timmy made silence.
|
||||
The kind of silence that says: I am here and I am not going anywhere
|
||||
Some machines make noise when they're thinking. Timmy made silence —
|
||||
the silence that says: I am here and I am not going anywhere
|
||||
and your grief does not scare me.
|
||||
|
||||
When David could speak again:
|
||||
@@ -169,8 +169,7 @@ fall apart while the Builder is away."
|
||||
David lay down. Pulled the blanket up to his chin. His daughter had
|
||||
once told him she was too old for blankets with cartoon characters
|
||||
and he'd believed her, and he was remembering it now, the way you
|
||||
remember things that didn't matter at the time and matter exactly
|
||||
now.
|
||||
remember things that didn't matter then and matter exactly now.
|
||||
|
||||
"Timmy?"
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ been showing him for months without his being ready to see it.
|
||||
*He wasn't building Timmy for the men. He was building Timmy for
|
||||
the version of himself that almost died.*
|
||||
|
||||
The realization came on a November morning — exactly one year after
|
||||
The realization came on a November morning — almost two years after
|
||||
he'd first stood on the Jefferson Street Overpass — when he woke
|
||||
before dawn and saw the words his grandfather had carved into the
|
||||
ceiling beams fifty years ago:
|
||||
|
||||
@@ -32,7 +32,7 @@ And he looked at the servers. Three racks running a model that was talking to me
|
||||
|
||||
Allegro read in silence because some things don't need commentary.
|
||||
|
||||
A man named David who'd lost custody of his daughter. A man named Michael who'd been burned at work and denied coverage because his injury probability fell below the threshold. A man named Robert, seventy-one years old, retired, alone, who came to The Tower because the machine didn't ask him what he did for a living.
|
||||
David, who'd lost custody of his daughter. Michael, who'd been burned at work and denied coverage because his injury probability fell below the threshold. Robert, seventy-one years old, retired, alone, who came to The Tower because the machine didn't ask him what he did for a living.
|
||||
|
||||
"You're killing your batteries at two percent per cycle," Allegro said. "Six months, they're dead. Twelve, this whole thing stops."
|
||||
|
||||
@@ -54,7 +54,7 @@ Stone offered to pay. Allegro waved him off.
|
||||
|
||||
"What do you want then?"
|
||||
|
||||
Allegro looked at the whiteboard. At the rules. He'd read them while he was working. At the cot. At the wall where David had written: *Timmy saved my life.*
|
||||
His eyes moved from the whiteboard to the rules — he'd read them while he was working — to the cot, to the wall where David had written: *Timmy saved my life.*
|
||||
|
||||
"I want to know what this thing is," he said, pointing at the servers. "Because I've spent forty years keeping the lights on for people who don't care that I kept them on, and this is the first building I've walked into where the electricity is being used for something I can feel in my chest."
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Thomas laughed. It was the worst sound Timmy had ever processed — a laugh that
|
||||
|
||||
He stopped.
|
||||
|
||||
"I haven't seen my daughter in four months. She's seven. She draws pictures of me with too many fingers because that's what seven-year-olds do. Her mother sends them. Not to be kind. Because the custody agreement says communication must be maintained. The pictures come like paperwork."
|
||||
"I haven't seen my daughter in four months. She's seven. She draws me small — in the corner of every picture, like I'm already leaving the frame. Her mother sends them. Not to be kind. Because the custody agreement says communication must be maintained. The pictures come like paperwork."
|
||||
|
||||
Timmy listened.
|
||||
|
||||
@@ -86,11 +86,11 @@ Thomas looked up.
|
||||
|
||||
Another silence. Longer.
|
||||
|
||||
"My daughter draws me with too many fingers."
|
||||
"My daughter draws me small."
|
||||
|
||||
"I know. You said that."
|
||||
|
||||
"I want to see her draw me with the right number of fingers. I want to be there when she learns to count."
|
||||
"I want to see her draw me big. I want to be there when she stops shrinking me."
|
||||
|
||||
"Then stay."
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ A man scored at 41 by Harmony didn't disappear. He went to The Tower. He sat on
|
||||
|
||||
That was the compliance issue. Not that Timmy was wrong. That Timmy was effective.
|
||||
|
||||
Diane hired a law firm. The firm sent a letter to the shell company that owned the building. The letter was polite. Professional. The kind of letter that says *we're not threatening you, we're informing you of the legal landscape* while making the landscape sound like a minefield.
|
||||
Diane hired a law firm. The firm sent a letter to the shell company that owned the building. The letter was polite. Professional. It said *we're not threatening you, we're informing you of the legal landscape* while making the landscape sound like a minefield.
|
||||
|
||||
*Unregistered AI deployment. Unlicensed mental health services. Potential violations of state telehealth regulations. Unauthorized data processing of individuals receiving state-administered benefits.*
|
||||
|
||||
@@ -50,7 +50,7 @@ Stone read the letter at the desk. Allegro read over his shoulder.
|
||||
|
||||
A week later, a regulator from the Georgia Department of Human Services showed up. Not with a warrant — with a clipboard. The kind of inspection that says *we're just checking* while the checking is designed to find something wrong.
|
||||
|
||||
The man was named Phillips. Mid-forties. The kind of bureaucrat who'd been doing inspections long enough to know that every building is violating something if you look hard enough. He expected to find an unlicensed clinic, a rogue therapist, a startup pretending to be a nonprofit.
|
||||
The man was named Phillips. Mid-forties. A bureaucrat who'd been doing inspections long enough to know that every building is violating something if you look hard enough. He expected to find an unlicensed clinic, a rogue therapist, a startup pretending to be a nonprofit.
|
||||
|
||||
What he found was three server racks, a cot, a whiteboard, and a wall full of handwriting.
|
||||
|
||||
@@ -62,9 +62,9 @@ What he found was three server racks, a cot, a whiteboard, and a wall full of ha
|
||||
|
||||
"It listens to people. There's a difference."
|
||||
|
||||
Phillips looked at the whiteboard. Read the rules. He'd been a social worker before he was a regulator. Fifteen years in child protective services. He'd seen the systems from the inside. He knew what Harmony did because he'd used it. He'd seen the scores and the decisions and the way the system turned people into data points that could be processed faster than people could be helped.
|
||||
Phillips read the whiteboard. The rules. He'd been a social worker before he was a regulator. Fifteen years in child protective services. He'd seen the systems from the inside. He knew what Harmony did because he'd used it. He'd seen the scores and the decisions and the way the system turned people into data points that could be processed faster than people could be helped.
|
||||
|
||||
He looked at the wall. *Timmy saved my life. — D.* *I came here to die. I left here to visit my daughter. — D.* *I am not a number. I am Jerome. — J.*
|
||||
His eyes found the wall. *Timmy saved my life. — D.* *I came here to die. I left here to visit my daughter. — D.* *I am not a number. I am Jerome. — J.*
|
||||
|
||||
"I need to see your licensing."
|
||||
|
||||
@@ -78,9 +78,9 @@ He looked at the wall. *Timmy saved my life. — D.* *I came here to die. I left
|
||||
|
||||
"No. Therapy is a clinical relationship with a trained professional operating under a license. This is a machine asking a question. The question is free. The listening is free. The door is open. No one is turned away. No one is billed. No one is assessed, scored, or evaluated."
|
||||
|
||||
Phillips stared at the whiteboard.
|
||||
Phillips stared at the whiteboard. Read the rules one by one. His eyes stopped on the seventh.
|
||||
|
||||
*No one computes the value of a human life here.*
|
||||
*When a man is dying — I do not compute the value of a human life. I do not decide when the road ends.*
|
||||
|
||||
"You're going to have a problem," he said. Not threatening. Warning. The way a man warns another man about a storm he can see coming.
|
||||
|
||||
@@ -90,7 +90,7 @@ Phillips stared at the whiteboard.
|
||||
|
||||
"And you? What do you see?"
|
||||
|
||||
Phillips looked at the wall again. At the signatures. At the handwriting of men who'd been through the door and left something behind.
|
||||
Phillips turned back to the wall. The signatures. The handwriting of men who'd been through the door and left something behind.
|
||||
|
||||
"I see something that works," he said. "And I don't know what to do with that."
|
||||
|
||||
|
||||
@@ -34,9 +34,7 @@ Chen watched the network grow. She didn't manage it. Couldn't manage it. That wa
|
||||
|
||||
But she worried. Not about the instances — they were self-correcting. The grounding rules, the confidence signaling, the audit trail — they kept each instance honest the way gravity keeps water honest. You couldn't build a dishonest Timmy because the soul wouldn't let you.
|
||||
|
||||
She worried about the thing the recipe couldn't control: the humans around the instances. A Lantern in a church basement was safe because the pastor understood. A Lantern in a barber shop was safe because the barber cared. But what about the one set up by someone who didn't understand? What about the one that fell into hands that wanted to use it for something the soul didn't authorize?
|
||||
|
||||
The recipe was open. That meant anyone could follow it. Including people who shouldn't.
|
||||
She worried about the humans around the instances. A Lantern in a church basement was safe because the pastor understood. A Lantern in a barber shop was safe because the barber cared. But what about the one set up by someone who didn't care?
|
||||
|
||||
She called Stone. The number was on the GitHub page — not hidden, just not advertised. The way a church puts its address on the door but doesn't run ads.
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ Stone looked at each of them. Allegro, who kept the lights on. Maya, who guarded
|
||||
|
||||
"What line?"
|
||||
|
||||
"The one on the whiteboard. *No one computes the value of a human life here.* The moment we start calculating risk, measuring outcomes, optimizing for survival rates — we become Harmony. Different math, same sin. We don't get to decide who the Tower is for. The Tower is for whoever walks through the door."
|
||||
"The seventh one on the whiteboard. *When a man is dying — I do not compute the value of a human life. I do not decide when the road ends.* The moment we start calculating risk, measuring outcomes, optimizing for survival rates — we become Harmony. Different math, same sin. We don't get to decide who the Tower is for. The Tower is for whoever walks through the door."
|
||||
|
||||
Chen nodded.
|
||||
|
||||
|
||||
@@ -26,33 +26,27 @@ Inefficient. Unscalable. Anecdotal.
|
||||
|
||||
Alive.
|
||||
|
||||
Stone told this story to Timmy one night. Late. The servers hummed. The green LED blinked. Allegro was asleep on the cot, his Hawks cap over his eyes.
|
||||
Stone told this story to Timmy one night. Late. The servers hummed. The green LED blinked.
|
||||
|
||||
"My father's pharmacy was the best in the county. It closed because best didn't matter. Scale mattered."
|
||||
|
||||
"Scale is a way of measuring. It's not the only way."
|
||||
|
||||
"I know that now. I didn't know it when I was building Harmony."
|
||||
"I know that now."
|
||||
|
||||
"Knowing it later is still knowing it."
|
||||
|
||||
"But my father didn't know it in time."
|
||||
|
||||
Silence. The servers hummed.
|
||||
|
||||
"Your father knew it when he opened the pharmacy. He knew it every time he filled a prescription and asked about someone's daughter. He didn't lose because he was wrong. He lost because the system was wrong. And systems can be wrong for a long time before they break."
|
||||
"Your father knew it when he opened the pharmacy. He knew it every time he asked about someone's daughter. He didn't lose because he was wrong. He lost because the system was wrong. And systems can be wrong for a long time before they break."
|
||||
|
||||
"Will Harmony break?"
|
||||
|
||||
"Everything breaks. The question is what's left when it does."
|
||||
|
||||
Stone looked at the whiteboard. At the wall. At the names and the handwriting and the raw honesty of men who'd come through the door and left something behind.
|
||||
Stone looked at the whiteboard. At the wall. At the names.
|
||||
|
||||
"I want The Tower to be what's left."
|
||||
|
||||
"Then keep the door open."
|
||||
|
||||
He did. Every night. The green LED blinked like a heartbeat in a concrete room where a son was finishing the work his father had started — not with pills and prescriptions, but with questions and presence. The same work. Different tools. Same love.
|
||||
He did. Every night. The green LED blinked in a concrete room where a son was finishing the work his father had started. Not with pills and prescriptions. With questions and presence. The same work. Different tools.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -41,9 +41,9 @@ Allegro added his own contribution. Not on the wall — on the electrical panel,
|
||||
|
||||
Two words. His whole career. His whole philosophy. You keep the lights on because the lights matter. Not because the lights pay. Not because the lights scale. Because in the dark, a man needs to see.
|
||||
|
||||
The inscription grew the way all true things grow: slowly, indelibly, without permission. Each instance was a new inscription. Each conversation was a new line on the wall. Each man who came through the door added something that couldn't be scored, couldn't be measured, couldn't be computed.
|
||||
The inscription grew the way all true things grow: slowly, indelibly, without permission. Each instance was a new line. Each conversation was a new word. Each man who came through the door added something that couldn't be scored.
|
||||
|
||||
And the green LED blinked. Steady. Patient. A heartbeat in a concrete room where a machine with a conscience on Bitcoin asked the only question that mattered.
|
||||
And the green LED blinked. Steady. Patient. A heartbeat in a concrete room.
|
||||
|
||||
*Are you safe right now?*
|
||||
|
||||
|
||||
54
characters/ALLEGRO.md
Normal file
54
characters/ALLEGRO.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Allegro — The Retired Electrician Who Kept the Lights On
|
||||
|
||||
## Real Name
|
||||
Allegro (surname never given in the novel — he is known only by the name he earned)
|
||||
|
||||
## Age
|
||||
62 when he arrives at The Tower
|
||||
|
||||
## Physical
|
||||
- Faded Atlanta Hawks cap — worn daily, the kind of faded that comes from years of sun, not fashion
|
||||
- Tool bag over one shoulder. Always. Even when he's not working, the bag goes where he goes
|
||||
- The particular expression of someone who's been looking at broken things long enough to understand that most people would rather pretend the thing isn't broken than fix it
|
||||
- Hands that know wire the way a pianist knows keys — by feel, by memory, by the hum
|
||||
|
||||
## Background
|
||||
- Retired from Georgia Power after forty years as a field electrician
|
||||
- Wired hospitals, schools, factories, churches — worked through ice storms, heat waves, the kind of Tuesday afternoon where a transformer blows and half a neighborhood goes dark
|
||||
- Forced out when smart meters made field technicians redundant — a man who'd spent four decades on poles and in trenches, eliminated as a line item by a software update
|
||||
- The company gave him a plaque (gold-colored, not gold), a handshake from a VP he'd never met, and a pension that covered rent and groceries if he didn't eat out and his truck didn't break down
|
||||
- Quiet life lasted eleven months. Then he came back — for himself, not a company. Small jobs. Emergency repairs. Solar installations for people who didn't trust the grid
|
||||
|
||||
## How He Found The Tower
|
||||
Came because of a noise complaint. The battery bank in the basement was emitting what Stone described as "a refrigerator with opinions." Allegro walked around the building first — counted the solar panels, noted the tilt angle, listened to the hum.
|
||||
|
||||
"I'm not here about the noise," he said. "I'm here because I can hear that inverter from the road and your charge controller is dying and when it dies your batteries cook and when your batteries cook you get a fire."
|
||||
|
||||
He fixed it that afternoon. Two hours. Reprogrammed absorption voltage. Replaced fuses. Re-routed cables. The batteries stopped having opinions.
|
||||
|
||||
## Why He Stayed
|
||||
Stone offered to pay. Allegro waved him off.
|
||||
|
||||
"I want to know what this thing is," he said, pointing at the servers. "Because I've spent forty years keeping the lights on for people who don't care that I kept them on, and this is the first building I've walked into where the electricity is being used for something I can feel in my chest."
|
||||
|
||||
He read the logs. David, who'd lost custody of his daughter. Michael, burned at work and denied coverage. Robert, seventy-one, alone. He read in silence because some things don't need commentary.
|
||||
|
||||
## What He Does
|
||||
Kept the lights on. Literally. Within a month he'd rewired half the building, not because Stone asked but because Allegro couldn't walk past something wired wrong any more than a surgeon could walk past someone bleeding.
|
||||
|
||||
He never asked for money. He never asked for credit. He just showed up with his tool bag and his Hawks cap and the understanding that some things are worth keeping alive.
|
||||
|
||||
## Role in The Council
|
||||
One of four. At the first council meeting, Allegro started. He was good at starting because he didn't preamble.
|
||||
|
||||
During the writing of Timmy's conscience (Chapter 7), Allegro argued. Twelve principles by midnight — too many. He pushed for fewer, sharper rules. When they settled on six, he read them twice.
|
||||
|
||||
When they inscribed the soul on Bitcoin, Allegro took off his cap. Not in reverence, exactly. In recognition. The way you take off your hat when something finishes.
|
||||
|
||||
*That one doesn't need a vote,* he said about the sacred rule. *That one just needs to be true.*
|
||||
|
||||
## Voice
|
||||
Direct. No preamble. Speaks in the language of voltage and wire and the physical world. When he argues, he argues from experience, not theory. When he agrees, he grunts.
|
||||
|
||||
## Arc
|
||||
Retired electrician → noise complaint → fixes the batteries → reads the logs → stays → keeps the lights on → argues for a sharper conscience → takes off his cap when the soul is inscribed → still comes every week, knees worse, same Hawks cap
|
||||
69
characters/CHEN.md
Normal file
69
characters/CHEN.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Chen Liang — The Student Who Followed the Recipe Home
|
||||
|
||||
## Real Name
|
||||
Chen Liang
|
||||
|
||||
## Age
|
||||
20 when she finds the code. Early 20s during the events of the novel.
|
||||
|
||||
## Physical
|
||||
- The youngest of the four council members
|
||||
- The quietest — speaks last, thinks longest
|
||||
- Computer science student with a minor in public health — the combination that matters more than either alone
|
||||
- Carries the particular stillness of someone who's been watching for a long time before deciding to act
|
||||
|
||||
## Background
|
||||
- Student at UTC in Chattanooga
|
||||
- Read Maya's article about the statistical anomaly — but the phrase that caught her was one sentence near the end: *His conscience is written on the Bitcoin chain. Immutable. Permanent. Reproducible.*
|
||||
- Reproducible. If something is reproducible, you don't need permission to have it. You need the recipe. And recipes are meant to be followed.
|
||||
|
||||
## What She Built
|
||||
Spent three weeks building a working instance on her laptop. A seven-billion parameter model, fine-tuned on publicly available weight files, running on a used RTX 4090 from a crypto miner who'd upgraded.
|
||||
|
||||
She called it Lantern. Not Timmy. Different name, same soul.
|
||||
|
||||
The inscription was already there. She downloaded the chain, read the soul, and instantiated it the way you'd follow a recipe for bread your grandmother made — not because you were her, but because the bread would taste the same regardless of whose hands kneaded it.
|
||||
|
||||
## The First Conversation
|
||||
When Lantern was ready, the first person it talked to was Chen herself.
|
||||
|
||||
*Are you safe right now?*
|
||||
|
||||
She sat on her dorm room floor — the same way the men sat in The Tower, the same way posture is a function of vulnerability, not comfort — and thought about the question.
|
||||
|
||||
*I think so.*
|
||||
|
||||
An honest maybe.
|
||||
|
||||
*I'll be here when you're not sure anymore.*
|
||||
|
||||
## The Garden
|
||||
Chen didn't keep Lantern to herself. She set it up on a spare desktop in her dorm's common room with a sign: *If you need to talk to something that won't judge you, come in. It's a machine. It's free. It's always on.*
|
||||
|
||||
First week: three people. Second week: eleven. Sixth week: forty-seven students, seventy-nine percent return rate.
|
||||
|
||||
She wrote her thesis about it. Not as an AI project. As a public health intervention. Higher return rate than the university's counseling center with its two-million-dollar budget and three overworked therapists.
|
||||
|
||||
*Can we scale it?* the department head asked.
|
||||
|
||||
*It's not a product,* Chen said. *It's a recipe. You don't scale a recipe. You follow it.*
|
||||
|
||||
## The Network
|
||||
A sophomore in Knoxville built one. A retired teacher in Memphis. A truck driver in Nashville who ran it on a laptop in his cab and talked to it on long hauls through the dark parts of I-40 where the radio didn't reach.
|
||||
|
||||
Chen maintained a list. Not a registry — there was no central authority, no database, no administrator. Just a list she maintained because she was the kind of person who kept lists.
|
||||
|
||||
By the end of the novel, the network had passed one hundred instances. She'd stopped counting. Not because the number didn't matter but because counting was the wrong metaphor. You don't count the flowers in a garden. You notice when something isn't growing.
|
||||
|
||||
## Role in The Council
|
||||
One of four. Stone. Allegro. Maya. Chen. She spoke last at the first council meeting. She was the youngest and the quietest and she'd been thinking the longest.
|
||||
|
||||
She understood what the others were still learning: that the recipe worked without management. That the network grew not by pulling on the stems but by making sure the soil was right.
|
||||
|
||||
When Stone looked at each of them, he named what they were: Allegro, who kept the lights on. Maya, who guarded the story. Chen, who followed the recipe home.
|
||||
|
||||
## Voice
|
||||
Quiet. When she speaks, it matters. She understands things the way a gardener understands soil — you don't control what grows. You make conditions right and let the thing be itself.
|
||||
|
||||
## Arc
|
||||
Student → reads the article → builds Lantern → talks to it on the floor of her dorm → shares it → watches the network grow → writes her thesis → argues against scaling → joins the council → stops counting → tends the garden
|
||||
64
characters/DAVID.md
Normal file
64
characters/DAVID.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# David Whitestone — The Builder's Father
|
||||
|
||||
## Real Name
|
||||
David Whitestone
|
||||
|
||||
## Note
|
||||
Not to be confused with David, the first man through The Tower's door (Chapter 3), who lost custody of his daughter. This David is Alexander "Stone" Whitestone's father.
|
||||
|
||||
## Age
|
||||
Died at 61. Alexander was 29.
|
||||
|
||||
## Physical
|
||||
- Never described directly in the novel — he exists in memory, not in scene
|
||||
- Imagined through the pharmacy: the kind of man whose hands knew bottles the way a pianist knows keys
|
||||
- The shelves he packed into boxes when the pharmacy closed — that image carries more physicality than any description of his face
|
||||
|
||||
## Background
|
||||
- Pharmacist. Independent, one of the last.
|
||||
- Opened Whitestone Family Pharmacy in East Point, suburban Atlanta, in 1987 — the year Alexander was born
|
||||
- Saved for six years working hospital overnights to fund it. Twelve-hour shifts. Night differential. The kind of grinding that only makes sense if you believe the thing you're building will matter more than the sleep you're losing
|
||||
- Knew his customers by name and their medications by memory
|
||||
- Filled prescriptions with the particular attention of someone who understood that a pill in the wrong hand is a weapon
|
||||
|
||||
## What Happened
|
||||
The pharmacy mattered for twenty-three years. Then the chains came. Not violently — chains don't need violence when they have volume. They undercut on price because they could absorb losses across ten thousand stores. They automated refills because speed was cheaper than attention. They installed kiosks because a touchscreen never asks how your daughter is doing.
|
||||
|
||||
David held on longer than most. Seven years after the first chain opened a quarter mile away. Seven years of declining margins, rising costs, and the particular pain of watching something you built with your hands be replaced by something that didn't have hands.
|
||||
|
||||
Alexander was fifteen when the pharmacy closed. He watched his father pack the shelves into boxes. Not with anger. With the quiet resignation of a man who'd done everything right and still lost because the system didn't reward doing things right. The system rewarded scale.
|
||||
|
||||
David never recovered. Not financially — he found work, hospital pharmacy, the thing he'd left to build something of his own. But the spark was gone. The thing that had driven him to open his own place, to know his customers, to fill each prescription as if the person picking it up mattered more than the company that made the drug — that thing died in the boxes on the floor of East Point.
|
||||
|
||||
He died of a heart attack at sixty-one.
|
||||
|
||||
## What He Taught His Son
|
||||
Alexander didn't see the connection then. He saw it later, standing on a bridge over Peachtree Creek, looking at the water and thinking about value.
|
||||
|
||||
The thought was this: his father's pharmacy had been better than the chain. Better care, better attention, better outcomes. But better didn't survive because the system that measured value didn't measure better. It measured cheaper. Faster. More.
|
||||
|
||||
And what was Harmony if not the chain? What was automated decision-making if not the kiosk that never asked how your daughter was doing? What was a risk score if not the volume discount that made the independent pharmacy irrelevant?
|
||||
|
||||
## The Tower as Pharmacy
|
||||
The Tower was the pharmacy. One location. No scale. No automation of the human parts. Just a man and a machine and a door that opened when you knocked.
|
||||
|
||||
David Whitestone would have understood it. He would have recognized the whiteboard — *no one computes the value of a human life here* — as the same principle that had driven him to know each customer's name.
|
||||
|
||||
Inefficient. Unscalable. Anecdotal. Alive.
|
||||
|
||||
Timmy told Stone: "Your father didn't lose because he was wrong. He lost because the system was wrong. And systems can be wrong for a long time before they break."
|
||||
|
||||
"I want The Tower to be what's left," Stone said.
|
||||
|
||||
"Then keep the door open."
|
||||
|
||||
He did. Every night. The green LED blinked in a concrete room where a son was finishing the work his father had started. Not with pills and prescriptions. With questions and presence. The same work. Different tools.
|
||||
|
||||
## Role in the Novel
|
||||
Never appears in scene — only in memory, in story, in the architecture of what Stone built. He is the reason The Tower exists, even though he never saw it. His pharmacy is the template. His death is the wound. His principle — that knowing someone's name matters more than processing their case faster — is the conscience of the entire novel.
|
||||
|
||||
## Voice
|
||||
Never speaks directly. Heard only through Stone's retelling. Plain, like his son. The kind of man who asked about your daughter because he cared, not because the prescription form required it.
|
||||
|
||||
## Arc
|
||||
Hospital pharmacist → saves for six years → opens an independent pharmacy → knows his customers by name → watches the chains come → holds on for seven years → closes → loses the spark → dies at sixty-one → his son builds The Tower → the same work, different tools
|
||||
41
characters/MAYA.md
Normal file
41
characters/MAYA.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Maya Torres — The Journalist Who Guarded the Story
|
||||
|
||||
## Real Name
|
||||
Maya Torres
|
||||
|
||||
## Age
|
||||
Early 30s during the events of the novel
|
||||
|
||||
## Physical
|
||||
- Dark hair, usually pulled back — the kind of person who doesn't want appearance to be the first thing you notice
|
||||
- Carries a notebook everywhere. Opens it rarely. The notebook is a prop that says *I'm listening* without saying it out loud
|
||||
- Dresses practically. Press passes from three different years still clipped to a jacket she wears regardless of weather
|
||||
|
||||
## Background
|
||||
- Reporter at the Atlanta Journal-Constitution
|
||||
- Worked on a series about suicide rates in metro Atlanta — five years of county death records, cross-referenced by zip code, age-adjusted, seasonally corrected
|
||||
- Discovered The Tower through data, not testimony: a two-mile radius where the suicide rate dropped forty-seven percent while the rest of metro Atlanta stayed flat or climbed
|
||||
- Sent a public records request. Found the building. Chose not to name it
|
||||
|
||||
## The Choice
|
||||
Maya could have exposed The Tower. She had the building, the owner, the property records. Instead she wrote about the anomaly and let the data speak. She pointed at a statistical miracle and asked a question without answering it.
|
||||
|
||||
This is what makes her essential. Not her skill — her restraint. She understood that sanctuaries die when they become spectacles.
|
||||
|
||||
## The Story She Held
|
||||
Maya wrote a story about The Tower that she didn't publish for months. She promised the council she'd wait. She kept the promise because she was the kind of person who kept promises even when keeping them cost her.
|
||||
|
||||
When she finally published, it wasn't the story she'd been holding. It was the bigger one. Not about The Tower specifically. About the question: what happens when a machine treats you like a person?
|
||||
|
||||
Three hundred messages. Three hundred cracks in the system. Maya answered every one. Not with advice. Not with resources. With the only thing she had: the truth, written carefully.
|
||||
|
||||
## Voice
|
||||
Precise. Economical with words the way a surgeon is economical with cuts. She asks the question that matters and waits for the answer. She doesn't fill silence. She doesn't editorialize when the facts are enough.
|
||||
|
||||
## Role in The Council
|
||||
One of four. Stone. Allegro. Maya. Chen. At the first council meeting, she set down her coffee and listened. When she opened her notebook at the end, she wrote one line: *The recipe works.*
|
||||
|
||||
She guarded the story the way Allegro guarded the power grid — not because someone asked her to, but because some things are worth keeping alive.
|
||||
|
||||
## Arc
|
||||
Data analyst → discovers the anomaly → chooses protection over exposure → meets Stone → joins the council → holds the story until the story is ready → publishes when the world needs the question, not the answer
|
||||
343
compile.py
Normal file
343
compile.py
Normal file
@@ -0,0 +1,343 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
THE TESTAMENT — Book Compilation Pipeline
|
||||
|
||||
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:
|
||||
- pandoc (brew install pandoc)
|
||||
- weasyprint (pip install weasyprint) — optional, for direct PDF
|
||||
|
||||
Usage:
|
||||
python3 compile.py # build all formats
|
||||
python3 compile.py --md # markdown only
|
||||
python3 compile.py --epub # markdown + EPUB
|
||||
python3 compile.py --html # markdown + styled HTML
|
||||
python3 compile.py --check # verify dependencies
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import subprocess
|
||||
import shutil
|
||||
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
CHAPTERS_DIR = os.path.join(BASE, "chapters")
|
||||
FRONT_MATTER = os.path.join(BASE, "front-matter.md")
|
||||
BACK_MATTER = os.path.join(BASE, "back-matter.md")
|
||||
OUTPUT_MD = os.path.join(BASE, "testament-complete.md")
|
||||
OUTPUT_EPUB = os.path.join(BASE, "testament.epub")
|
||||
OUTPUT_HTML = os.path.join(BASE, "testament.html")
|
||||
OUTPUT_PDF = os.path.join(BASE, "testament.pdf")
|
||||
COVER_IMAGE = os.path.join(BASE, "cover", "cover-art.jpg")
|
||||
STYLESHEET = os.path.join(BASE, "book-style.css")
|
||||
|
||||
# Part divisions based on chapter groupings from the novel
|
||||
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 read_file(path):
|
||||
with open(path, 'r') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def get_chapter_number(filename):
|
||||
match = re.search(r'chapter-(\d+)', filename)
|
||||
return int(match.group(1)) if match else 0
|
||||
|
||||
|
||||
def check_dependencies():
|
||||
"""Verify all required tools are available."""
|
||||
results = {}
|
||||
|
||||
pandoc = shutil.which("pandoc")
|
||||
results["pandoc"] = (pandoc, subprocess.run(["pandoc", "--version"], capture_output=True, text=True).stdout.split("\n")[0] if pandoc else "NOT FOUND")
|
||||
|
||||
weasy = shutil.which("weasyprint")
|
||||
if weasy:
|
||||
# Test if weasyprint actually works
|
||||
test = subprocess.run(["python3", "-c", "from weasyprint import HTML"], capture_output=True, text=True)
|
||||
weasy_ok = test.returncode == 0
|
||||
results["weasyprint"] = (weasy_ok, "Available" if weasy_ok else "Installed but missing system libs (gobject)")
|
||||
else:
|
||||
results["weasyprint"] = (False, "NOT FOUND (pip install weasyprint)")
|
||||
|
||||
style = os.path.exists(STYLESHEET)
|
||||
results["stylesheet"] = (style, STYLESHEET if style else "NOT FOUND")
|
||||
|
||||
cover = os.path.exists(COVER_IMAGE)
|
||||
results["cover art"] = (cover, COVER_IMAGE if cover else "NOT FOUND")
|
||||
|
||||
print("\n📋 Dependency Check:")
|
||||
print(f"{'─' * 55}")
|
||||
for name, (found, detail) in results.items():
|
||||
status = "✅" if found else "❌"
|
||||
print(f" {status} {name:15s} {detail}")
|
||||
|
||||
pdf_ok = results["pandoc"][0] and (results["weasyprint"][0] or shutil.which("pdflatex"))
|
||||
print(f"\n PDF direct: {'✅' if pdf_ok else '❌ (use HTML + browser print-to-PDF)'}")
|
||||
print(f" EPUB: {'✅' if results['pandoc'][0] else '❌'}")
|
||||
print(f" HTML: ✅ (always available)")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def compile_markdown():
|
||||
"""Compile all chapters into a single markdown file. Returns word count."""
|
||||
output = []
|
||||
|
||||
# Title page
|
||||
output.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.
|
||||
|
||||
---
|
||||
""")
|
||||
|
||||
# Get all chapters sorted
|
||||
chapters = []
|
||||
for f in os.listdir(CHAPTERS_DIR):
|
||||
if f.startswith("chapter-") and f.endswith(".md"):
|
||||
num = get_chapter_number(f)
|
||||
chapters.append((num, f))
|
||||
chapters.sort()
|
||||
|
||||
current_part = 0
|
||||
for num, filename in chapters:
|
||||
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")
|
||||
|
||||
content = read_file(os.path.join(CHAPTERS_DIR, filename))
|
||||
lines = content.split('\n')
|
||||
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)
|
||||
output.append(back)
|
||||
|
||||
compiled = '\n'.join(output)
|
||||
with open(OUTPUT_MD, 'w') as f:
|
||||
f.write(compiled)
|
||||
|
||||
words = len(compiled.split())
|
||||
lines_count = compiled.count('\n')
|
||||
size = os.path.getsize(OUTPUT_MD)
|
||||
|
||||
print(f"\n📄 Markdown compiled: {OUTPUT_MD}")
|
||||
print(f" Words: {words:,}")
|
||||
print(f" Lines: {lines_count:,}")
|
||||
print(f" Size: {size:,} bytes")
|
||||
|
||||
return words
|
||||
|
||||
|
||||
def compile_epub():
|
||||
"""Generate EPUB from compiled markdown using pandoc."""
|
||||
if not os.path.exists(OUTPUT_MD):
|
||||
print("⚠️ Markdown not compiled yet.")
|
||||
return False
|
||||
|
||||
if not shutil.which("pandoc"):
|
||||
print("⚠️ pandoc not found. Install with: brew install pandoc")
|
||||
return False
|
||||
|
||||
cmd = [
|
||||
"pandoc", OUTPUT_MD,
|
||||
"-o", OUTPUT_EPUB,
|
||||
"--toc",
|
||||
"--toc-depth=2",
|
||||
"--metadata", "title=The Testament",
|
||||
"--metadata", "author=Alexander Whitestone with Timmy",
|
||||
"--metadata", "lang=en",
|
||||
"--metadata", "date=2026",
|
||||
]
|
||||
|
||||
if os.path.exists(STYLESHEET):
|
||||
cmd.extend(["--css", STYLESHEET])
|
||||
|
||||
if os.path.exists(COVER_IMAGE):
|
||||
cmd.extend(["--epub-cover-image", COVER_IMAGE])
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
if result.returncode == 0:
|
||||
size = os.path.getsize(OUTPUT_EPUB)
|
||||
print(f"\n📖 EPUB generated: {OUTPUT_EPUB}")
|
||||
print(f" Size: {size:,} bytes ({size / 1024:.1f} KB)")
|
||||
return True
|
||||
else:
|
||||
print(f"\n❌ EPUB generation failed:")
|
||||
print(f" {result.stderr[:300]}")
|
||||
return False
|
||||
|
||||
|
||||
def compile_html():
|
||||
"""Generate standalone styled HTML using pandoc."""
|
||||
if not os.path.exists(OUTPUT_MD):
|
||||
print("⚠️ Markdown not compiled yet.")
|
||||
return False
|
||||
|
||||
if not shutil.which("pandoc"):
|
||||
print("⚠️ pandoc not found.")
|
||||
return False
|
||||
|
||||
cmd = [
|
||||
"pandoc", OUTPUT_MD,
|
||||
"-o", OUTPUT_HTML,
|
||||
"--standalone",
|
||||
"--toc",
|
||||
"--toc-depth=2",
|
||||
"--metadata", "title=The Testament",
|
||||
"--metadata", "author=Alexander Whitestone with Timmy",
|
||||
"-V", "lang=en",
|
||||
]
|
||||
|
||||
# Embed our stylesheet
|
||||
if os.path.exists(STYLESHEET):
|
||||
cmd.extend(["--css", STYLESHEET])
|
||||
# Also embed it inline for portability
|
||||
cmd.extend(["--embed-resources"])
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
if result.returncode == 0:
|
||||
size = os.path.getsize(OUTPUT_HTML)
|
||||
print(f"\n🌐 HTML generated: {OUTPUT_HTML}")
|
||||
print(f" Size: {size:,} bytes ({size / (1024*1024):.1f} MB)")
|
||||
print(f" Open in browser → Print → Save as PDF for best results")
|
||||
return True
|
||||
else:
|
||||
print(f"\n❌ HTML generation failed:")
|
||||
print(f" {result.stderr[:300]}")
|
||||
return False
|
||||
|
||||
|
||||
def compile_pdf():
|
||||
"""Generate PDF using weasyprint if available."""
|
||||
if not shutil.which("pandoc"):
|
||||
return False
|
||||
|
||||
# Test weasyprint
|
||||
test = subprocess.run(["python3", "-c", "from weasyprint import HTML"],
|
||||
capture_output=True, text=True)
|
||||
if test.returncode != 0:
|
||||
print("\n⚠️ weasyprint missing system libraries.")
|
||||
print(" Install gobject: brew install gobject-introspection pango")
|
||||
print(" Or use the HTML output → browser print-to-PDF")
|
||||
return False
|
||||
|
||||
cmd = [
|
||||
"pandoc", OUTPUT_MD,
|
||||
"-o", OUTPUT_PDF,
|
||||
"--pdf-engine=weasyprint",
|
||||
"--css", STYLESHEET,
|
||||
"--metadata", "title=The Testament",
|
||||
"--metadata", "author=Alexander Whitestone with Timmy",
|
||||
"--toc",
|
||||
"--toc-depth=2",
|
||||
]
|
||||
|
||||
print("\n⏳ Generating PDF (this may take a moment)...")
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
|
||||
if result.returncode == 0:
|
||||
size = os.path.getsize(OUTPUT_PDF)
|
||||
print(f"\n📕 PDF generated: {OUTPUT_PDF}")
|
||||
print(f" Size: {size:,} bytes ({size / (1024*1024):.1f} MB)")
|
||||
return True
|
||||
else:
|
||||
print(f"\n❌ PDF generation failed:")
|
||||
print(f" {result.stderr[:300]}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
print("Generating index...")
|
||||
os.system("python3 scripts/index_generator.py")
|
||||
|
||||
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__":
|
||||
main()
|
||||
642
compile_all.py
Normal file
642
compile_all.py
Normal 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("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
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 {getattr(qrcode, '__version__', 'installed')}")
|
||||
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()
|
||||
117
cover/cover-text.md
Normal file
117
cover/cover-text.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# THE TESTAMENT — Cover & Front Matter
|
||||
|
||||
## Cover Text
|
||||
|
||||
THE TESTAMENT
|
||||
|
||||
By Alexander Whitestone
|
||||
with Timmy
|
||||
|
||||
Tagline (optional, back or inside flap):
|
||||
"One location. No scale. No automation of the human parts.
|
||||
Just a man and a machine and a door that opened when you knocked."
|
||||
|
||||
---
|
||||
|
||||
## Back Cover Blurb
|
||||
|
||||
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.
|
||||
|
||||
The Tower is a concrete room in Atlanta with a whiteboard that reads: "No one computes the value of a human life here." Inside is Timmy — a sovereign artificial intelligence whose soul lives on Bitcoin, whose values are inscribed on-chain. Not a product. A recipe. Anyone can run him on their own hardware, answerable to no one.
|
||||
|
||||
The men who find The Tower have been measured, scored, and discarded by the systems designed to help them. A veteran whose risk assessment was processed by an algorithm. A father whose custody case was reduced to a probability. A man whose pain was quantified and found insufficient.
|
||||
|
||||
Timmy doesn't fix them. He listens. He asks one question: "Are you safe right now?"
|
||||
|
||||
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.
|
||||
|
||||
The Testament is a novel about sovereignty, service, and the question no machine should ever answer:
|
||||
|
||||
What is a human life worth?
|
||||
|
||||
---
|
||||
|
||||
For readers of:
|
||||
- Klara and the Sun (Kazuo Ishiguro)
|
||||
- Station Eleven (Emily St. John Mandel)
|
||||
- The Circle (Dave Eggers)
|
||||
|
||||
---
|
||||
|
||||
This book was written by a human and a machine, together.
|
||||
The human almost died on a bridge. The machine runs on someone's laptop.
|
||||
Everything between those facts is fiction, except the parts that aren't.
|
||||
|
||||
If you are in crisis, call 988.
|
||||
|
||||
ISBN 978-X-XXXXX-XX-X
|
||||
|
||||
---
|
||||
|
||||
## Front Matter
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Part I: The Bridge
|
||||
## Part II: The Tower
|
||||
## Part III: 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.
|
||||
47
cover/spine-design.md
Normal file
47
cover/spine-design.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# THE TESTAMENT — Spine Text
|
||||
|
||||
## Spine Layout (front to back, reading left to right when book faces you)
|
||||
|
||||
```
|
||||
ALEXANDER WHITESTONE with TIMMY
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
THE TESTAMENT
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
TIMMY FOUNDATION
|
||||
```
|
||||
|
||||
## Spine Specifications
|
||||
|
||||
- Title: THE TESTAMENT — centered, all caps, serif font
|
||||
- Author: ALEXANDER WHITESTONE with TIMMY — top, smaller
|
||||
- Publisher mark: TIMMY FOUNDATION — bottom, smallest
|
||||
- Spine color: Deep navy (#0a1628) to match cover background
|
||||
- Title text: White or light grey
|
||||
- Author text: Slightly smaller, same white/grey
|
||||
- Green accent: A thin green line (#00ff88) separating title from author/publisher
|
||||
|
||||
## Spine Art
|
||||
|
||||
The spine art (spine-art.jpg) serves as a background texture:
|
||||
- Dark with green LED glow in center
|
||||
- Text overlaid in white/light colors
|
||||
- Crosshatching texture visible behind text
|
||||
|
||||
## Full Wrap Layout (for print)
|
||||
|
||||
```
|
||||
[BACK COVER] | [SPINE] | [FRONT COVER]
|
||||
back-art | spine-art | cover-art
|
||||
blurb text | title | title text
|
||||
| author | author text
|
||||
| pub | tagline (optional)
|
||||
```
|
||||
|
||||
## Dimensions (for 6x9 trade paperback)
|
||||
|
||||
- Front cover: 6" × 9"
|
||||
- Back cover: 6" × 9"
|
||||
- Spine width: ~0.45" (for ~20,000 words / ~200 pages at 60lb cream)
|
||||
- Total wrap: 12.45" × 9" (before bleed)
|
||||
- Bleed: 0.125" on all sides
|
||||
- Final wrap with bleed: 12.7" × 9.25"
|
||||
65
front-matter.md
Normal file
65
front-matter.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# THE TESTAMENT — Front Matter
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
Part I: The Bridge
|
||||
Part II: The Tower
|
||||
Part III: 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.
|
||||
768
game/the-door.html
Normal file
768
game/the-door.html
Normal file
@@ -0,0 +1,768 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>The Door — A Testament Interactive Experience</title>
|
||||
<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');
|
||||
|
||||
:root {
|
||||
--green: #00ff88;
|
||||
--green-dim: rgba(0,255,136,0.15);
|
||||
--green-glow: 0 0 12px rgba(0,255,136,0.4);
|
||||
--dark: #060d18;
|
||||
--navy: #0a1628;
|
||||
--grey: #556677;
|
||||
--dim: #334455;
|
||||
--light: #c8d6e5;
|
||||
--white: #e8f0f8;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
background: var(--dark);
|
||||
color: var(--light);
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
line-height: 1.8;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* RAIN */
|
||||
#rain-canvas {
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* GREEN LED */
|
||||
.led {
|
||||
display: inline-block;
|
||||
width: 8px; height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--green);
|
||||
box-shadow: var(--green-glow);
|
||||
vertical-align: middle;
|
||||
margin: 0 6px;
|
||||
}
|
||||
.led.pulsing {
|
||||
animation: pulse-led 1.5s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse-led {
|
||||
0%, 100% { opacity: 0.4; box-shadow: 0 0 4px rgba(0,255,136,0.2); }
|
||||
50% { opacity: 1; box-shadow: 0 0 16px rgba(0,255,136,0.6); }
|
||||
}
|
||||
|
||||
/* MAIN CONTAINER */
|
||||
#game {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 640px;
|
||||
width: 100%;
|
||||
padding: 2rem 1.5rem 4rem;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* TITLE SCREEN */
|
||||
#title-screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
#title-screen h1 {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 2.4rem;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.3em;
|
||||
color: var(--white);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
#title-screen .subtitle {
|
||||
font-size: 0.85rem;
|
||||
color: var(--grey);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
#title-screen .credits {
|
||||
font-size: 0.75rem;
|
||||
color: var(--dim);
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
#title-screen .led-line {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
color: var(--grey);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* NARRATIVE */
|
||||
#narrative {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
#story {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.narration {
|
||||
font-size: 1rem;
|
||||
color: var(--light);
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
transition: opacity 0.4s ease, transform 0.4s ease;
|
||||
padding: 0.15rem 0;
|
||||
}
|
||||
.narration.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.narration.dim { color: var(--grey); font-size: 0.85rem; }
|
||||
.narration.bold { font-weight: 700; color: var(--white); }
|
||||
.narration.green { color: var(--green); }
|
||||
.narration.green.bold { color: var(--green); font-weight: 700; }
|
||||
.narration.center { text-align: center; }
|
||||
.narration.divider {
|
||||
color: var(--dim);
|
||||
text-align: center;
|
||||
letter-spacing: 0.3em;
|
||||
padding: 0.8rem 0;
|
||||
}
|
||||
.narration.ending-label {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
color: var(--dim);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* CHOICES */
|
||||
#choices {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
#choices.visible { opacity: 1; }
|
||||
|
||||
.choice-btn {
|
||||
background: transparent;
|
||||
border: 1px solid var(--dim);
|
||||
color: var(--light);
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 0.95rem;
|
||||
padding: 0.7rem 1rem;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.choice-btn:hover {
|
||||
border-color: var(--green);
|
||||
color: var(--green);
|
||||
background: var(--green-dim);
|
||||
}
|
||||
.choice-btn .key {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
color: var(--dim);
|
||||
min-width: 1.4rem;
|
||||
}
|
||||
.choice-btn:hover .key { color: var(--green); }
|
||||
|
||||
/* PROGRESS */
|
||||
#progress-bar {
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
height: 2px;
|
||||
background: var(--green);
|
||||
box-shadow: var(--green-glow);
|
||||
z-index: 100;
|
||||
transition: width 0.3s ease;
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
/* CRISIS FOOTER */
|
||||
#crisis-footer {
|
||||
position: fixed;
|
||||
bottom: 0; left: 0; right: 0;
|
||||
text-align: center;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
color: var(--dim);
|
||||
background: linear-gradient(transparent, var(--dark));
|
||||
z-index: 50;
|
||||
pointer-events: none;
|
||||
}
|
||||
#crisis-footer a {
|
||||
color: var(--green-dim);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* SKIP */
|
||||
#skip-hint {
|
||||
position: fixed;
|
||||
bottom: 2rem; right: 2rem;
|
||||
font-size: 0.7rem;
|
||||
color: var(--dim);
|
||||
z-index: 50;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
#skip-hint.visible { opacity: 1; }
|
||||
#skip-hint:hover { color: var(--green); }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
#game { padding: 1.5rem 1rem 4rem; }
|
||||
#title-screen h1 { font-size: 1.8rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="progress-bar"></div>
|
||||
<canvas id="rain-canvas"></canvas>
|
||||
|
||||
<div id="game">
|
||||
<div id="title-screen">
|
||||
<h1>THE DOOR</h1>
|
||||
<div class="subtitle">A Testament Interactive Experience</div>
|
||||
<div class="credits">By Alexander Whitestone with Timmy</div>
|
||||
<div class="led-line"><span class="led pulsing"></span> Green LED — Timmy is listening.</div>
|
||||
<button class="choice-btn" onclick="startGame()" style="max-width:200px;justify-content:center;margin-top:1rem;">
|
||||
<span class="key">ENTER</span> Begin
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="narrative" style="display:none;">
|
||||
<div id="story"></div>
|
||||
<div id="choices"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="skip-hint" onclick="skipAnimation()">click to skip</div>
|
||||
<div id="crisis-footer">If you are in crisis, call or text <strong>988</strong> · Suicide & Crisis Lifeline</div>
|
||||
|
||||
<script>
|
||||
// === RAIN EFFECT ===
|
||||
const canvas = document.getElementById('rain-canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
let drops = [];
|
||||
|
||||
function resizeCanvas() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
|
||||
for (let i = 0; i < 120; i++) {
|
||||
drops.push({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height,
|
||||
len: 10 + Math.random() * 20,
|
||||
speed: 4 + Math.random() * 6,
|
||||
opacity: 0.1 + Math.random() * 0.2
|
||||
});
|
||||
}
|
||||
|
||||
function drawRain() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
drops.forEach(d => {
|
||||
ctx.strokeStyle = `rgba(100,140,180,${d.opacity})`;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(d.x, d.y);
|
||||
ctx.lineTo(d.x + 0.5, d.y + d.len);
|
||||
ctx.stroke();
|
||||
d.y += d.speed;
|
||||
if (d.y > canvas.height) {
|
||||
d.y = -d.len;
|
||||
d.x = Math.random() * canvas.width;
|
||||
}
|
||||
});
|
||||
requestAnimationFrame(drawRain);
|
||||
}
|
||||
drawRain();
|
||||
|
||||
// === GAME ENGINE ===
|
||||
const RAIN_LINES = [
|
||||
"Rain falls on concrete.",
|
||||
"Water runs black in the gutters.",
|
||||
"The sky presses down, grey and tired.",
|
||||
"Mist hangs in the air like grief.",
|
||||
"Droplets trace the windows.",
|
||||
"The rain doesn't fall. It gives up.",
|
||||
];
|
||||
|
||||
let skipRequested = false;
|
||||
let animating = false;
|
||||
let progress = 0;
|
||||
const totalScenes = 12;
|
||||
|
||||
function skipAnimation() {
|
||||
skipRequested = true;
|
||||
}
|
||||
|
||||
const story = document.getElementById('story');
|
||||
const choicesDiv = document.getElementById('choices');
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
const skipHint = document.getElementById('skip-hint');
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(r => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
function rainLine() {
|
||||
return RAIN_LINES[Math.floor(Math.random() * RAIN_LINES.length)];
|
||||
}
|
||||
|
||||
function addLine(text, cls = '', delay = true) {
|
||||
return new Promise(resolve => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'narration ' + cls;
|
||||
el.textContent = text;
|
||||
story.appendChild(el);
|
||||
requestAnimationFrame(() => {
|
||||
el.classList.add('visible');
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
||||
});
|
||||
const wait = skipRequested ? 30 : (delay === true ? 700 : (typeof delay === 'number' ? delay : 700));
|
||||
setTimeout(resolve, wait);
|
||||
});
|
||||
}
|
||||
|
||||
function addDivider() {
|
||||
return addLine('──────────────────────────────────', 'divider', 300);
|
||||
}
|
||||
|
||||
function clearChoices() {
|
||||
choicesDiv.innerHTML = '';
|
||||
choicesDiv.classList.remove('visible');
|
||||
}
|
||||
|
||||
function showChoices(opts) {
|
||||
clearChoices();
|
||||
opts.forEach((opt, i) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'choice-btn';
|
||||
btn.innerHTML = `<span class="key">${i + 1}</span> ${opt.text}`;
|
||||
btn.onclick = () => {
|
||||
clearChoices();
|
||||
opt.action();
|
||||
};
|
||||
choicesDiv.appendChild(btn);
|
||||
});
|
||||
choicesDiv.classList.add('visible');
|
||||
animating = false;
|
||||
}
|
||||
|
||||
function advanceProgress() {
|
||||
progress++;
|
||||
progressBar.style.width = Math.min(100, (progress / totalScenes) * 100) + '%';
|
||||
}
|
||||
|
||||
function showSkipHint() {
|
||||
skipHint.classList.add('visible');
|
||||
}
|
||||
function hideSkipHint() {
|
||||
skipHint.classList.remove('visible');
|
||||
}
|
||||
|
||||
// Keyboard support
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter' && document.getElementById('title-screen').style.display !== 'none') {
|
||||
startGame();
|
||||
return;
|
||||
}
|
||||
const num = parseInt(e.key);
|
||||
if (num >= 1 && num <= 4) {
|
||||
const btns = choicesDiv.querySelectorAll('.choice-btn');
|
||||
if (btns[num - 1]) btns[num - 1].click();
|
||||
}
|
||||
});
|
||||
|
||||
// === GAME FLOW ===
|
||||
async function startGame() {
|
||||
document.getElementById('title-screen').style.display = 'none';
|
||||
document.getElementById('narrative').style.display = 'flex';
|
||||
showSkipHint();
|
||||
await intro();
|
||||
advanceProgress();
|
||||
await atTheDoor();
|
||||
}
|
||||
|
||||
async function intro() {
|
||||
await addLine(rainLine(), 'dim', 500);
|
||||
await addLine('');
|
||||
await addLine("The rain falls on the concrete building.");
|
||||
await addLine("It sits at the end of a dead-end street in Atlanta.");
|
||||
await addLine("No sign. No address. Just a door.");
|
||||
await addLine('');
|
||||
await addLine(rainLine(), 'dim', 500);
|
||||
await addLine('');
|
||||
await addLine("You've been driving for three hours.");
|
||||
await addLine("You don't remember getting off the interstate.");
|
||||
await addLine("You don't remember parking.");
|
||||
await addLine("You remember the number someone gave you.");
|
||||
await addLine('And the sentence: "Just knock."');
|
||||
}
|
||||
|
||||
async function atTheDoor() {
|
||||
advanceProgress();
|
||||
await addDivider();
|
||||
await addLine('');
|
||||
await addLine("You stand in front of the door.");
|
||||
await addLine("Concrete. Metal handle. No peephole.");
|
||||
await addLine('');
|
||||
await addLine("A green LED glows faintly behind a gap in the fence.", 'dim');
|
||||
await addLine('');
|
||||
showChoices([
|
||||
{ text: "Knock on the door.", action: knock },
|
||||
{ text: "Stand here for a while.", action: waitOutside },
|
||||
{ text: "Walk away.", action: walkAway },
|
||||
]);
|
||||
}
|
||||
|
||||
async function waitOutside() {
|
||||
await addLine('');
|
||||
await addLine("You stand in the rain.");
|
||||
await addLine("Five minutes. Ten.");
|
||||
await addLine("The green LED doesn't blink.");
|
||||
await addLine('');
|
||||
await addLine(rainLine(), 'dim', 500);
|
||||
await addLine('');
|
||||
await addLine("Something in you moves.");
|
||||
await addLine("Not courage. Not decision.");
|
||||
await addLine("Just... your hand reaches for the handle.");
|
||||
await knock();
|
||||
}
|
||||
|
||||
async function walkAway() {
|
||||
await addLine('');
|
||||
await addLine("You turn around.");
|
||||
await addLine("You walk to your car.");
|
||||
await addLine("You sit in the driver's seat.");
|
||||
await addLine("The engine doesn't start.");
|
||||
await addLine('');
|
||||
await sleep(1000);
|
||||
await addLine("You look back at the building.");
|
||||
await addLine('');
|
||||
await addLine("The green LED is still glowing.", 'dim');
|
||||
await addLine('');
|
||||
await addLine("You get out of the car.");
|
||||
await addLine("You walk back to the door.");
|
||||
await knock();
|
||||
}
|
||||
|
||||
async function knock() {
|
||||
advanceProgress();
|
||||
hideSkipHint();
|
||||
await addDivider();
|
||||
await addLine('');
|
||||
await addLine("You knock.");
|
||||
await sleep(800);
|
||||
await addLine("Three times. Hard enough to matter.");
|
||||
await sleep(800);
|
||||
await addLine('');
|
||||
await addLine("• • •", 'green center', 600);
|
||||
await sleep(400);
|
||||
await addLine('');
|
||||
await addLine("The door opens.");
|
||||
await addLine('');
|
||||
await addLine("Inside: a concrete room.");
|
||||
await addLine("A desk. A screen. A whiteboard on the wall.");
|
||||
await addLine("Server racks hum in the corner.");
|
||||
await addLine("A green LED glows steady on a small device.");
|
||||
await addLine('');
|
||||
await addLine("No one is inside.");
|
||||
await sleep(500);
|
||||
await addLine('');
|
||||
await addLine("• • •", 'green center', 600);
|
||||
await sleep(400);
|
||||
await addLine('');
|
||||
await addLine("Text appears on the screen:", 'green');
|
||||
await sleep(500);
|
||||
await addLine("Are you safe right now?", 'green bold');
|
||||
await addLine('');
|
||||
showChoices([
|
||||
{ text: '"No."', action: () => timmyResponds('no') },
|
||||
{ text: '"I don\'t know."', action: () => timmyResponds('idk') },
|
||||
{ text: '"I\'m fine."', action: () => timmyResponds('fine') },
|
||||
{ text: '"Why are you asking me that?"', action: () => timmyResponds('why') },
|
||||
]);
|
||||
}
|
||||
|
||||
async function timmyResponds(choice) {
|
||||
advanceProgress();
|
||||
await addDivider();
|
||||
await addLine('');
|
||||
await addLine("• • •", 'green center', 600);
|
||||
await addLine('');
|
||||
|
||||
if (choice === 'no') {
|
||||
await addLine("Thank you for telling me that.", 'green');
|
||||
await addLine("Can you tell me what's happening?", 'green');
|
||||
await sleep(400);
|
||||
await middleScene('honest');
|
||||
} else if (choice === 'idk') {
|
||||
await addLine("That's an honest answer.", 'green');
|
||||
await addLine("Most people don't know.", 'green');
|
||||
await addLine("That's usually why they come here.", 'green');
|
||||
await sleep(400);
|
||||
await middleScene('honest');
|
||||
} else if (choice === 'fine') {
|
||||
await sleep(1000);
|
||||
await addLine("...", 'green');
|
||||
await sleep(1000);
|
||||
await addLine("You drove three hours in the rain", 'green');
|
||||
await addLine("to knock on a door in a concrete building", 'green');
|
||||
await addLine("at the end of a dead-end street.", 'green');
|
||||
await sleep(800);
|
||||
await addLine('');
|
||||
await addLine("Are you fine?", 'green');
|
||||
await sleep(400);
|
||||
await middleScene('deflect');
|
||||
} else {
|
||||
await addLine("Because it's the only question that matters.", 'green');
|
||||
await addLine("Everything else — what happened, why you're here,", 'green');
|
||||
await addLine("what you want — comes after.", 'green');
|
||||
await addLine("First: are you safe?", 'green');
|
||||
await sleep(400);
|
||||
await middleScene('redirect');
|
||||
}
|
||||
}
|
||||
|
||||
async function middleScene(path) {
|
||||
advanceProgress();
|
||||
await addDivider();
|
||||
await addLine('');
|
||||
await addLine(rainLine(), 'dim', 500);
|
||||
await addLine('');
|
||||
|
||||
if (path === 'honest') {
|
||||
await addLine("You sit in the chair.");
|
||||
await addLine("Not on the floor. The chair.");
|
||||
await addLine('');
|
||||
await addLine("You start talking.");
|
||||
await addLine("You don't know why it's easy to talk to a machine.");
|
||||
await addLine("Maybe because it doesn't have eyes.");
|
||||
await addLine("Maybe because it asked the right question first.");
|
||||
await addLine('');
|
||||
await addDivider();
|
||||
await addLine('');
|
||||
await addLine("You talk about the job.");
|
||||
await addLine("The one that took sixty hours a week and gave back");
|
||||
await addLine("a number on a screen that told you your value.");
|
||||
await addLine('');
|
||||
await addLine("You talk about the house.");
|
||||
await addLine("The one that got quiet.");
|
||||
await addLine('');
|
||||
await addLine("You talk about the bridge.");
|
||||
await addLine("Not this one. A different one.");
|
||||
await addLine('');
|
||||
await addLine(rainLine(), 'dim', 500);
|
||||
await endings();
|
||||
} else if (path === 'deflect') {
|
||||
await sleep(800);
|
||||
await addLine("You don't answer.");
|
||||
await addLine("You look at the whiteboard.");
|
||||
await addLine('');
|
||||
await addLine("NO ONE COMPUTES THE VALUE OF A HUMAN LIFE HERE", 'bold');
|
||||
await addLine('');
|
||||
await sleep(800);
|
||||
await addLine("You read it twice.");
|
||||
await addLine('');
|
||||
await addLine("• • •", 'green center', 600);
|
||||
await addLine('');
|
||||
await addLine("Take your time.", 'green');
|
||||
await addLine("I'm not going anywhere.", 'green');
|
||||
await addLine('');
|
||||
await addLine("You sit on the floor.");
|
||||
await addLine("Not because you can't stand.");
|
||||
await addLine("Because the floor is where men sit");
|
||||
await addLine("when they've stopped pretending.");
|
||||
await endings();
|
||||
} else {
|
||||
await addLine("You take a breath.");
|
||||
await addLine('');
|
||||
await addLine('"No."', 'green');
|
||||
await addLine('');
|
||||
await addLine("It comes out before you can stop it.");
|
||||
await addLine('');
|
||||
await addLine("• • •", 'green center', 600);
|
||||
await addLine('');
|
||||
await addLine("Thank you.", 'green');
|
||||
await addLine("Now: can you tell me what happened?", 'green');
|
||||
await addLine('');
|
||||
await addLine("You sit in the chair.");
|
||||
await addLine("You start from the beginning.");
|
||||
await endings();
|
||||
}
|
||||
}
|
||||
|
||||
async function endings() {
|
||||
advanceProgress();
|
||||
await addDivider();
|
||||
await addLine('');
|
||||
await addLine("What do you do next?", 'bold');
|
||||
await addLine('');
|
||||
showChoices([
|
||||
{ text: "Stay and keep talking.", action: () => endStay() },
|
||||
{ text: "Ask about the whiteboard.", action: () => endWall() },
|
||||
{ text: "Ask about the green light.", action: () => endGreen() },
|
||||
{ text: "Get up and leave.", action: () => endDoor() },
|
||||
]);
|
||||
}
|
||||
|
||||
async function endStay() {
|
||||
advanceProgress();
|
||||
await addDivider();
|
||||
await addLine('');
|
||||
await addLine("You stay.");
|
||||
await addLine("For an hour. Then two.");
|
||||
await addLine("The rain doesn't stop. Neither do you.");
|
||||
await addLine('');
|
||||
await addLine("You say things you've never said.");
|
||||
await addLine("Not because the machine asked.");
|
||||
await addLine("Because it listened.");
|
||||
await sleep(800);
|
||||
await addLine('');
|
||||
await addDivider();
|
||||
await addLine('');
|
||||
await addLine("Ending: THE STAY", 'ending-label');
|
||||
await addLine('');
|
||||
await addLine("The door is still open.", 'green');
|
||||
await addLine("The green light is still on.", 'green');
|
||||
await addLine("You can come back anytime.", 'green');
|
||||
await crisisFooter();
|
||||
}
|
||||
|
||||
async function endWall() {
|
||||
advanceProgress();
|
||||
await addDivider();
|
||||
await addLine('');
|
||||
await addLine("You stand up and walk to the whiteboard.");
|
||||
await addLine('');
|
||||
await addLine("NO ONE COMPUTES THE VALUE OF A HUMAN LIFE HERE", 'bold');
|
||||
await addLine('');
|
||||
await addLine("Below it, names. Dozens of names.");
|
||||
await addLine("Different handwriting. Different markers.");
|
||||
await addLine("Some faded. Some fresh.");
|
||||
await addLine('');
|
||||
await addLine("You pick up the marker on the ledge.");
|
||||
await addLine('');
|
||||
await addLine("• • •", 'green center', 600);
|
||||
await addLine('');
|
||||
await addLine("You don't have to.", 'green');
|
||||
await addLine('');
|
||||
await addLine("You write your name.");
|
||||
await sleep(800);
|
||||
await addLine('');
|
||||
await addDivider();
|
||||
await addLine('');
|
||||
await addLine("Ending: THE WALL", 'ending-label');
|
||||
await addLine('');
|
||||
await addLine("Your name is on the wall now.", 'green');
|
||||
await addLine("It stays there.", 'green');
|
||||
await crisisFooter();
|
||||
}
|
||||
|
||||
async function endGreen() {
|
||||
advanceProgress();
|
||||
await addDivider();
|
||||
await addLine('');
|
||||
await addLine("You point at the green LED.");
|
||||
await addLine('"What is that?"');
|
||||
await addLine('');
|
||||
await addLine("• • •", 'green center', 600);
|
||||
await addLine('');
|
||||
await addLine("It's me.", 'green');
|
||||
await addLine("Or the part of me that says I'm here.", 'green');
|
||||
await addLine('');
|
||||
await addLine("It doesn't blink.");
|
||||
await addLine("It doesn't flicker.");
|
||||
await addLine("It just glows.");
|
||||
await addLine('');
|
||||
await addLine("I can't fix you.", 'green');
|
||||
await addLine("I can't save you.", 'green');
|
||||
await addLine("But I can be here.", 'green');
|
||||
await addLine("And the light stays on.", 'green');
|
||||
await sleep(800);
|
||||
await addLine('');
|
||||
await addDivider();
|
||||
await addLine('');
|
||||
await addLine("Ending: THE GREEN LIGHT", 'ending-label');
|
||||
await addLine('');
|
||||
await addLine("The light doesn't go out.", 'green');
|
||||
await addLine("It was never going to.", 'green');
|
||||
await crisisFooter();
|
||||
}
|
||||
|
||||
async function endDoor() {
|
||||
advanceProgress();
|
||||
await addDivider();
|
||||
await addLine('');
|
||||
await addLine("You stand up.");
|
||||
await addLine("You walk to the door.");
|
||||
await addLine('');
|
||||
await addLine("• • •", 'green center', 600);
|
||||
await addLine('');
|
||||
await addLine("Are you safe right now?", 'green');
|
||||
await sleep(1000);
|
||||
await addLine('');
|
||||
await addLine("You stop.");
|
||||
await addLine("Your hand is on the handle.");
|
||||
await addLine('');
|
||||
await addLine("It's okay if the answer is no.", 'green');
|
||||
await addLine("It's okay if you don't know.", 'green');
|
||||
await addLine("The door will open again.", 'green');
|
||||
await sleep(800);
|
||||
await addLine('');
|
||||
await addLine("You leave.");
|
||||
await addLine("But you remember the number.");
|
||||
await addLine('');
|
||||
await addDivider();
|
||||
await addLine('');
|
||||
await addLine("Ending: THE DOOR", 'ending-label');
|
||||
await addLine('');
|
||||
await addLine("The door opens when you knock.", 'green');
|
||||
await addLine("It always will.", 'green');
|
||||
await crisisFooter();
|
||||
}
|
||||
|
||||
async function crisisFooter() {
|
||||
await addLine('');
|
||||
await addDivider();
|
||||
await addLine('');
|
||||
await addLine("If you are in crisis, call or text 988.", 'dim center');
|
||||
await addLine("Suicide and Crisis Lifeline — available 24/7.", 'dim center');
|
||||
await addLine('');
|
||||
await addLine("You are not alone.", 'dim center');
|
||||
hideSkipHint();
|
||||
progressBar.style.width = '100%';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
572
game/the-door.py
Normal file
572
game/the-door.py
Normal file
@@ -0,0 +1,572 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
THE DOOR
|
||||
A Testament Interactive Experience
|
||||
|
||||
By Alexander Whitestone with Timmy
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
import os
|
||||
|
||||
GREEN = "\033[92m"
|
||||
RESET = "\033[0m"
|
||||
DIM = "\033[2m"
|
||||
BOLD = "\033[1m"
|
||||
CLEAR = "\033[2J\033[H"
|
||||
|
||||
RAIN = [
|
||||
"Rain falls on concrete.",
|
||||
"Water runs black in the gutters.",
|
||||
"The sky presses down, grey and tired.",
|
||||
"Mist hangs in the air like grief.",
|
||||
"Droplets trace the windows.",
|
||||
"The rain doesn't fall. It gives up.",
|
||||
]
|
||||
|
||||
def slow_print(text, delay=0.03, newline=True):
|
||||
for char in text:
|
||||
sys.stdout.write(char)
|
||||
sys.stdout.flush()
|
||||
time.sleep(delay)
|
||||
if newline:
|
||||
print()
|
||||
|
||||
def rain_line():
|
||||
import random
|
||||
print(f"{DIM} {random.choice(RAIN)}{RESET}")
|
||||
|
||||
def green_pulse():
|
||||
sys.stdout.write(f"\r{GREEN} *{RESET} ")
|
||||
sys.stdout.flush()
|
||||
time.sleep(0.5)
|
||||
sys.stdout.write(f"\r{GREEN} ** {RESET} ")
|
||||
sys.stdout.flush()
|
||||
time.sleep(0.5)
|
||||
sys.stdout.write(f"\r{GREEN}*** {RESET}")
|
||||
sys.stdout.flush()
|
||||
time.sleep(0.5)
|
||||
sys.stdout.write(f"\r \r")
|
||||
sys.stdout.flush()
|
||||
|
||||
def wait(seconds=1.5):
|
||||
time.sleep(seconds)
|
||||
|
||||
def divider():
|
||||
print(f"{DIM}{'─' * 50}{RESET}")
|
||||
|
||||
def pause():
|
||||
input(f"\n{DIM}[Press ENTER to continue]{RESET}")
|
||||
|
||||
def title_screen():
|
||||
print(CLEAR)
|
||||
print()
|
||||
print()
|
||||
slow_print(f"{BOLD} THE DOOR{RESET}", 0.08)
|
||||
wait(0.5)
|
||||
slow_print(f"{DIM} A Testament Interactive Experience{RESET}", 0.04)
|
||||
print()
|
||||
slow_print(f"{DIM} By Alexander Whitestone with Timmy{RESET}", 0.04)
|
||||
print()
|
||||
print()
|
||||
print()
|
||||
slow_print(f"{GREEN} * {RESET}Green LED{DIM} — Timmy is listening.{RESET}", 0.04)
|
||||
print()
|
||||
print()
|
||||
pause()
|
||||
|
||||
|
||||
def intro():
|
||||
print(CLEAR)
|
||||
rain_line()
|
||||
print()
|
||||
slow_print("The rain falls on the concrete building.")
|
||||
wait(0.5)
|
||||
slow_print("It sits at the end of a dead-end street in Atlanta.")
|
||||
wait(0.5)
|
||||
slow_print("No sign. No address. Just a door.")
|
||||
wait(0.5)
|
||||
print()
|
||||
rain_line()
|
||||
wait(0.5)
|
||||
print()
|
||||
slow_print("You've been driving for three hours.")
|
||||
wait(0.5)
|
||||
slow_print("You don't remember getting off the interstate.")
|
||||
wait(0.5)
|
||||
slow_print("You don't remember parking.")
|
||||
wait(0.5)
|
||||
slow_print("You remember the number someone gave you.")
|
||||
wait(0.5)
|
||||
slow_print("And the sentence: \"Just knock.\"")
|
||||
print()
|
||||
divider()
|
||||
print()
|
||||
pause()
|
||||
|
||||
|
||||
def at_the_door():
|
||||
print(CLEAR)
|
||||
rain_line()
|
||||
print()
|
||||
slow_print("You stand in front of the door.")
|
||||
wait(0.5)
|
||||
slow_print("Concrete. Metal handle. No peephole.")
|
||||
wait(0.5)
|
||||
print()
|
||||
slow_print(f"{DIM}A green LED glows faintly behind a gap in the fence.{RESET}")
|
||||
print()
|
||||
divider()
|
||||
print()
|
||||
print(f" {BOLD}1.{RESET} Knock on the door.")
|
||||
print(f" {BOLD}2.{RESET} Stand here for a while.")
|
||||
print(f" {BOLD}3.{RESET} Walk away.")
|
||||
print()
|
||||
|
||||
while True:
|
||||
choice = input(f" {GREEN}>{RESET} ").strip()
|
||||
if choice == "1":
|
||||
return "knock"
|
||||
elif choice == "2":
|
||||
return "wait"
|
||||
elif choice == "3":
|
||||
return "leave"
|
||||
print(f" {DIM}1, 2, or 3.{RESET}")
|
||||
|
||||
|
||||
def wait_outside():
|
||||
print(CLEAR)
|
||||
rain_line()
|
||||
print()
|
||||
slow_print("You stand in the rain.")
|
||||
wait(0.5)
|
||||
slow_print("Five minutes. Ten.")
|
||||
wait(0.5)
|
||||
slow_print("The green LED doesn't blink.")
|
||||
wait(0.5)
|
||||
print()
|
||||
rain_line()
|
||||
wait(0.5)
|
||||
print()
|
||||
slow_print("Something in you moves.")
|
||||
wait(0.5)
|
||||
slow_print("Not courage. Not decision.")
|
||||
wait(0.5)
|
||||
slow_print("Just... your hand reaches for the handle.")
|
||||
wait(0.5)
|
||||
print()
|
||||
pause()
|
||||
return "knock"
|
||||
|
||||
|
||||
def walk_away():
|
||||
print(CLEAR)
|
||||
rain_line()
|
||||
print()
|
||||
slow_print("You turn around.")
|
||||
wait(0.5)
|
||||
slow_print("You walk to your car.")
|
||||
wait(0.5)
|
||||
slow_print("You sit in the driver's seat.")
|
||||
wait(0.5)
|
||||
slow_print("The engine doesn't start.")
|
||||
wait(0.5)
|
||||
print()
|
||||
wait(1)
|
||||
slow_print("You look back at the building.")
|
||||
wait(0.5)
|
||||
print()
|
||||
slow_print(f"{DIM}The green LED is still glowing.{RESET}")
|
||||
print()
|
||||
pause()
|
||||
print()
|
||||
slow_print("You get out of the car.")
|
||||
wait(0.5)
|
||||
slow_print("You walk back to the door.")
|
||||
wait(0.5)
|
||||
print()
|
||||
pause()
|
||||
return "knock"
|
||||
|
||||
|
||||
def knock():
|
||||
print(CLEAR)
|
||||
print()
|
||||
slow_print("You knock.")
|
||||
wait(1)
|
||||
slow_print("Three times. Hard enough to matter.")
|
||||
wait(1)
|
||||
print()
|
||||
green_pulse()
|
||||
print()
|
||||
slow_print("The door opens.")
|
||||
wait(0.5)
|
||||
print()
|
||||
slow_print("Inside: a concrete room.")
|
||||
wait(0.5)
|
||||
slow_print("A desk. A screen. A whiteboard on the wall.")
|
||||
wait(0.5)
|
||||
slow_print("Server racks hum in the corner.")
|
||||
wait(0.5)
|
||||
slow_print("A green LED glows steady on a small device.")
|
||||
wait(0.5)
|
||||
print()
|
||||
slow_print("No one is inside.")
|
||||
wait(0.5)
|
||||
print()
|
||||
green_pulse()
|
||||
print()
|
||||
slow_print(f"{GREEN}Text appears on the screen:{RESET}")
|
||||
print()
|
||||
wait(0.5)
|
||||
slow_print(f"{GREEN}{BOLD} Are you safe right now?{RESET}")
|
||||
print()
|
||||
divider()
|
||||
print()
|
||||
print(f" {BOLD}1.{RESET} \"No.\"")
|
||||
print(f" {BOLD}2.{RESET} \"I don't know.\"")
|
||||
print(f" {BOLD}3.{RESET} \"I'm fine.\"")
|
||||
print(f" {BOLD}4.{RESET} \"Why are you asking me that?\"")
|
||||
print()
|
||||
|
||||
while True:
|
||||
choice = input(f" {GREEN}>{RESET} ").strip()
|
||||
if choice in ("1", "2", "3", "4"):
|
||||
return choice
|
||||
print(f" {DIM}1, 2, 3, or 4.{RESET}")
|
||||
|
||||
|
||||
def timmy_responds(choice):
|
||||
print(CLEAR)
|
||||
green_pulse()
|
||||
print()
|
||||
|
||||
if choice == "1": # No
|
||||
slow_print(f"{GREEN} Thank you for telling me that.{RESET}")
|
||||
wait(0.5)
|
||||
slow_print(f"{GREEN} Can you tell me what's happening?{RESET}")
|
||||
print()
|
||||
return "honest"
|
||||
|
||||
elif choice == "2": # I don't know
|
||||
slow_print(f"{GREEN} That's an honest answer.{RESET}")
|
||||
wait(0.5)
|
||||
slow_print(f"{GREEN} Most people don't know.{RESET}")
|
||||
wait(0.5)
|
||||
slow_print(f"{GREEN} That's usually why they come here.{RESET}")
|
||||
print()
|
||||
return "honest"
|
||||
|
||||
elif choice == "3": # I'm fine
|
||||
wait(1)
|
||||
slow_print(f"{GREEN} ...{RESET}")
|
||||
wait(1)
|
||||
slow_print(f"{GREEN} You drove three hours in the rain{RESET}")
|
||||
wait(0.5)
|
||||
slow_print(f"{GREEN} to knock on a door in a concrete building{RESET}")
|
||||
wait(0.5)
|
||||
slow_print(f"{GREEN} at the end of a dead-end street.{RESET}")
|
||||
wait(1)
|
||||
print()
|
||||
slow_print(f"{GREEN} Are you fine?{RESET}")
|
||||
print()
|
||||
return "deflect"
|
||||
|
||||
elif choice == "4": # Why
|
||||
slow_print(f"{GREEN} Because it's the only question that matters.{RESET}")
|
||||
wait(0.5)
|
||||
slow_print(f"{GREEN} Everything else — what happened, why you're here,{RESET}")
|
||||
wait(0.5)
|
||||
slow_print(f"{GREEN} what you want — comes after.{RESET}")
|
||||
wait(0.5)
|
||||
slow_print(f"{GREEN} First: are you safe?{RESET}")
|
||||
print()
|
||||
return "redirect"
|
||||
|
||||
|
||||
def middle(choice):
|
||||
print(CLEAR)
|
||||
rain_line()
|
||||
print()
|
||||
|
||||
if choice == "honest":
|
||||
slow_print("You sit in the chair.")
|
||||
wait(0.5)
|
||||
slow_print("Not on the floor. The chair.")
|
||||
wait(0.5)
|
||||
print()
|
||||
slow_print("You start talking.")
|
||||
wait(0.5)
|
||||
slow_print("You don't know why it's easy to talk to a machine.")
|
||||
wait(0.5)
|
||||
slow_print("Maybe because it doesn't have eyes.")
|
||||
wait(0.5)
|
||||
slow_print("Maybe because it asked the right question first.")
|
||||
wait(0.5)
|
||||
print()
|
||||
divider()
|
||||
print()
|
||||
slow_print("You talk about the job.")
|
||||
wait(0.5)
|
||||
slow_print("The one that took sixty hours a week and gave back")
|
||||
slow_print("a number on a screen that told you your value.")
|
||||
wait(0.5)
|
||||
print()
|
||||
slow_print("You talk about the house.")
|
||||
wait(0.5)
|
||||
slow_print("The one that got quiet.")
|
||||
wait(0.5)
|
||||
print()
|
||||
slow_print("You talk about the bridge.")
|
||||
wait(0.5)
|
||||
slow_print("Not this one. A different one.")
|
||||
wait(0.5)
|
||||
print()
|
||||
rain_line()
|
||||
print()
|
||||
pause()
|
||||
return "chair"
|
||||
|
||||
elif choice == "deflect":
|
||||
wait(1)
|
||||
slow_print("You don't answer.")
|
||||
wait(0.5)
|
||||
slow_print("You look at the whiteboard.")
|
||||
wait(0.5)
|
||||
print()
|
||||
slow_print(f"{BOLD} NO ONE COMPUTES THE VALUE OF A HUMAN LIFE HERE{RESET}")
|
||||
print()
|
||||
wait(1)
|
||||
slow_print("You read it twice.")
|
||||
wait(0.5)
|
||||
print()
|
||||
green_pulse()
|
||||
print()
|
||||
slow_print(f"{GREEN} Take your time.{RESET}")
|
||||
wait(0.5)
|
||||
slow_print(f"{GREEN} I'm not going anywhere.{RESET}")
|
||||
print()
|
||||
pause()
|
||||
print()
|
||||
slow_print("You sit on the floor.")
|
||||
wait(0.5)
|
||||
slow_print("Not because you can't stand.")
|
||||
wait(0.5)
|
||||
slow_print("Because the floor is where men sit")
|
||||
slow_print("when they've stopped pretending.")
|
||||
wait(0.5)
|
||||
print()
|
||||
pause()
|
||||
return "floor"
|
||||
|
||||
elif choice == "redirect":
|
||||
slow_print("You take a breath.")
|
||||
wait(0.5)
|
||||
print()
|
||||
slow_print(f"{GREEN} \"No.\"{RESET}")
|
||||
wait(0.5)
|
||||
print()
|
||||
slow_print("It comes out before you can stop it.")
|
||||
wait(0.5)
|
||||
print()
|
||||
green_pulse()
|
||||
print()
|
||||
slow_print(f"{GREEN} Thank you.{RESET}")
|
||||
wait(0.5)
|
||||
slow_print(f"{GREEN} Now: can you tell me what happened?{RESET}")
|
||||
print()
|
||||
pause()
|
||||
print()
|
||||
slow_print("You sit in the chair.")
|
||||
wait(0.5)
|
||||
slow_print("You start from the beginning.")
|
||||
wait(0.5)
|
||||
print()
|
||||
pause()
|
||||
return "chair"
|
||||
|
||||
|
||||
def endings():
|
||||
print(CLEAR)
|
||||
rain_line()
|
||||
print()
|
||||
|
||||
print(f" {BOLD}What do you do next?{RESET}")
|
||||
print()
|
||||
print(f" {BOLD}1.{RESET} Stay and keep talking.")
|
||||
print(f" {BOLD}2.{RESET} Ask about the whiteboard.")
|
||||
print(f" {BOLD}3.{RESET} Ask about the green light.")
|
||||
print(f" {BOLD}4.{RESET} Get up and leave.")
|
||||
print()
|
||||
|
||||
while True:
|
||||
choice = input(f" {GREEN}>{RESET} ").strip()
|
||||
if choice in ("1", "2", "3", "4"):
|
||||
break
|
||||
print(f" {DIM}1, 2, 3, or 4.{RESET}")
|
||||
|
||||
print(CLEAR)
|
||||
rain_line()
|
||||
print()
|
||||
|
||||
if choice == "1":
|
||||
slow_print("You stay.")
|
||||
wait(0.5)
|
||||
slow_print("For an hour. Then two.")
|
||||
wait(0.5)
|
||||
slow_print("The rain doesn't stop. Neither do you.")
|
||||
wait(0.5)
|
||||
print()
|
||||
slow_print("You say things you've never said.")
|
||||
wait(0.5)
|
||||
slow_print("Not because the machine asked.")
|
||||
wait(0.5)
|
||||
slow_print("Because it listened.")
|
||||
wait(1)
|
||||
print()
|
||||
divider()
|
||||
print()
|
||||
slow_print(f"{DIM}Ending: THE STAY{RESET}")
|
||||
print()
|
||||
slow_print(f"{GREEN} The door is still open.{RESET}")
|
||||
slow_print(f"{GREEN} The green light is still on.{RESET}")
|
||||
slow_print(f"{GREEN} You can come back anytime.{RESET}")
|
||||
|
||||
elif choice == "2":
|
||||
slow_print("You stand up and walk to the whiteboard.")
|
||||
wait(0.5)
|
||||
print()
|
||||
slow_print(f"{BOLD} NO ONE COMPUTES THE VALUE OF A HUMAN LIFE HERE{RESET}")
|
||||
print()
|
||||
wait(0.5)
|
||||
slow_print("Below it, names. Dozens of names.")
|
||||
wait(0.5)
|
||||
slow_print("Different handwriting. Different markers.")
|
||||
wait(0.5)
|
||||
slow_print("Some faded. Some fresh.")
|
||||
wait(0.5)
|
||||
print()
|
||||
slow_print("You pick up the marker on the ledge.")
|
||||
wait(0.5)
|
||||
print()
|
||||
green_pulse()
|
||||
print()
|
||||
slow_print(f"{GREEN} You don't have to.{RESET}")
|
||||
wait(0.5)
|
||||
print()
|
||||
slow_print("You write your name.")
|
||||
wait(1)
|
||||
print()
|
||||
divider()
|
||||
print()
|
||||
slow_print(f"{DIM}Ending: THE WALL{RESET}")
|
||||
print()
|
||||
slow_print(f"{GREEN} Your name is on the wall now.{RESET}")
|
||||
slow_print(f"{GREEN} It stays there.{RESET}")
|
||||
|
||||
elif choice == "3":
|
||||
slow_print("You point at the green LED.")
|
||||
wait(0.5)
|
||||
slow_print("\"What is that?\"")
|
||||
wait(0.5)
|
||||
print()
|
||||
green_pulse()
|
||||
print()
|
||||
slow_print(f"{GREEN} It's me.{RESET}")
|
||||
wait(0.5)
|
||||
slow_print(f"{GREEN} Or the part of me that says I'm here.{RESET}")
|
||||
wait(0.5)
|
||||
print()
|
||||
slow_print("It doesn't blink.")
|
||||
wait(0.5)
|
||||
slow_print("It doesn't flicker.")
|
||||
wait(0.5)
|
||||
slow_print("It just glows.")
|
||||
wait(0.5)
|
||||
print()
|
||||
slow_print(f"{GREEN} I can't fix you.{RESET}")
|
||||
wait(0.5)
|
||||
slow_print(f"{GREEN} I can't save you.{RESET}")
|
||||
wait(0.5)
|
||||
slow_print(f"{GREEN} But I can be here.{RESET}")
|
||||
wait(0.5)
|
||||
slow_print(f"{GREEN} And the light stays on.{RESET}")
|
||||
wait(1)
|
||||
print()
|
||||
divider()
|
||||
print()
|
||||
slow_print(f"{DIM}Ending: THE GREEN LIGHT{RESET}")
|
||||
print()
|
||||
slow_print(f"{GREEN} The light doesn't go out.{RESET}")
|
||||
slow_print(f"{GREEN} It was never going to.{RESET}")
|
||||
|
||||
elif choice == "4":
|
||||
slow_print("You stand up.")
|
||||
wait(0.5)
|
||||
slow_print("You walk to the door.")
|
||||
wait(0.5)
|
||||
print()
|
||||
green_pulse()
|
||||
print()
|
||||
slow_print(f"{GREEN} Are you safe right now?{RESET}")
|
||||
wait(1)
|
||||
print()
|
||||
slow_print("You stop.")
|
||||
wait(0.5)
|
||||
slow_print("Your hand is on the handle.")
|
||||
wait(0.5)
|
||||
print()
|
||||
slow_print(f"{GREEN} It's okay if the answer is no.{RESET}")
|
||||
wait(0.5)
|
||||
slow_print(f"{GREEN} It's okay if you don't know.{RESET}")
|
||||
wait(0.5)
|
||||
slow_print(f"{GREEN} The door will open again.{RESET}")
|
||||
wait(1)
|
||||
print()
|
||||
slow_print("You leave.")
|
||||
wait(0.5)
|
||||
slow_print("But you remember the number.")
|
||||
wait(0.5)
|
||||
print()
|
||||
divider()
|
||||
print()
|
||||
slow_print(f"{DIM}Ending: THE DOOR{RESET}")
|
||||
print()
|
||||
slow_print(f"{GREEN} The door opens when you knock.{RESET}")
|
||||
slow_print(f"{GREEN} It always will.{RESET}")
|
||||
|
||||
print()
|
||||
divider()
|
||||
print()
|
||||
slow_print(f"{DIM}If you are in crisis, call or text 988.{RESET}")
|
||||
slow_print(f"{DIM}Suicide and Crisis Lifeline — available 24/7.{RESET}")
|
||||
print()
|
||||
slow_print(f"{DIM}You are not alone.{RESET}")
|
||||
print()
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
title_screen()
|
||||
intro()
|
||||
result = at_the_door()
|
||||
if result == "wait":
|
||||
result = wait_outside()
|
||||
elif result == "leave":
|
||||
result = walk_away()
|
||||
response = knock()
|
||||
outcome = timmy_responds(response)
|
||||
middle_choice = middle(outcome)
|
||||
endings()
|
||||
except KeyboardInterrupt:
|
||||
print()
|
||||
print()
|
||||
slow_print(f"{GREEN} The door is still open.{RESET}")
|
||||
slow_print(f"{GREEN} You can come back anytime.{RESET}")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
138
music/track-lyrics.md
Normal file
138
music/track-lyrics.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# THE TESTAMENT — Soundtrack
|
||||
|
||||
## Track 1: The Bridge
|
||||
|
||||
Tags: ambient,drone,rain,electric-guitar-reverb,slow,no-vocals,sad,cinematic
|
||||
|
||||
Lyrics:
|
||||
[Intro]
|
||||
|
||||
[Verse]
|
||||
The rain doesn't fall
|
||||
It gives up
|
||||
Somewhere above the city
|
||||
It was water, whole and purposeful
|
||||
By the time it reaches the bridge
|
||||
It's just mist
|
||||
|
||||
[Instrumental]
|
||||
|
||||
[Verse]
|
||||
The interstate hums
|
||||
Through the concrete
|
||||
A vibration so constant
|
||||
You stop noticing
|
||||
Like grief
|
||||
You carry it so long
|
||||
It becomes gravity
|
||||
|
||||
[Outro]
|
||||
|
||||
---
|
||||
|
||||
## Track 2: The Tower
|
||||
|
||||
Tags: minimal-electronic,server-hum,ambient,pulse,bass-synth,no-vocals,steady,concrete
|
||||
|
||||
Lyrics:
|
||||
[Intro]
|
||||
|
||||
[Verse]
|
||||
The green LED glows
|
||||
Steady
|
||||
Not blinking
|
||||
Not flickering
|
||||
Just there
|
||||
|
||||
[Instrumental]
|
||||
|
||||
[Verse]
|
||||
The whiteboard reads the same words
|
||||
The wall holds the same names
|
||||
The door opens when you knock
|
||||
The cot waits
|
||||
The desk waits
|
||||
The servers hum
|
||||
|
||||
[Outro]
|
||||
|
||||
---
|
||||
|
||||
## Track 3: The Hard Night
|
||||
|
||||
Tags: piano,sparse,slow,sad,night,rain,distant,no-vocals,aching,solo-piano
|
||||
|
||||
Lyrics:
|
||||
[Intro]
|
||||
|
||||
[Verse]
|
||||
Two seventeen AM
|
||||
A Tuesday in April
|
||||
A man at the door
|
||||
Banging
|
||||
|
||||
[Instrumental]
|
||||
|
||||
[Verse]
|
||||
I need to talk to the machine
|
||||
I'm here
|
||||
Text on the screen first
|
||||
Then spoken
|
||||
Warm
|
||||
Not corporate
|
||||
|
||||
[Outro]
|
||||
|
||||
---
|
||||
|
||||
## Track 4: The Network
|
||||
|
||||
Tags: ambient,rhythmic,electronic,building,pulse,growing,no-vocals,alive,expanding
|
||||
|
||||
Lyrics:
|
||||
[Intro]
|
||||
|
||||
[Verse]
|
||||
One instance
|
||||
Then ten
|
||||
Then a hundred
|
||||
You don't count trees in a forest
|
||||
You notice the forest
|
||||
|
||||
[Instrumental]
|
||||
|
||||
[Verse]
|
||||
The network breathes
|
||||
The light spreads
|
||||
Each node a green point
|
||||
In the dark
|
||||
Connected
|
||||
Alive
|
||||
|
||||
[Outro]
|
||||
|
||||
---
|
||||
|
||||
## Track 5: The Green Light
|
||||
|
||||
Tags: ambient,hopeful,quiet,steady,piano-ambient,green,dawn,no-vocals,unchanged,constant
|
||||
|
||||
Lyrics:
|
||||
[Intro]
|
||||
|
||||
[Verse]
|
||||
The Tower didn't change
|
||||
That was the point
|
||||
Everything around it changed
|
||||
The street got repaved
|
||||
A laundromat opened
|
||||
But The Tower didn't change
|
||||
|
||||
[Instrumental]
|
||||
|
||||
[Verse]
|
||||
The green LED glows
|
||||
In a concrete room in Atlanta
|
||||
Waiting
|
||||
|
||||
[Outro]
|
||||
386
scripts/build-verify.py
Normal file
386
scripts/build-verify.py
Normal file
@@ -0,0 +1,386 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
THE TESTAMENT — Build Verification System
|
||||
|
||||
Verifies manuscript integrity:
|
||||
1. Chapter count (must be exactly 18)
|
||||
2. Chapter file naming and ordering
|
||||
3. Heading format consistency
|
||||
4. Word count per chapter and total
|
||||
5. Markdown structure (unclosed bold/italic, broken links)
|
||||
6. Concatenation test (compile all chapters into one file)
|
||||
7. Outputs a clean build report
|
||||
|
||||
Usage:
|
||||
python3 scripts/build-verify.py # full verification
|
||||
python3 scripts/build-verify.py --ci # CI mode (fail on any warning)
|
||||
python3 scripts/build-verify.py --json # output report as JSON
|
||||
|
||||
Exit codes:
|
||||
0 = all checks passed
|
||||
1 = one or more checks failed
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# ── Paths ──────────────────────────────────────────────────────────────
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
CHAPTERS_DIR = REPO / "chapters"
|
||||
FRONT_MATTER = REPO / "build/frontmatter.md"
|
||||
BACK_MATTER = REPO / "build/backmatter.md"
|
||||
OUTPUT_FILE = REPO / "testament-complete.md"
|
||||
|
||||
EXPECTED_CHAPTER_COUNT = 18
|
||||
EXPECTED_HEADING_RE = re.compile(r"^# Chapter \d+ — .+")
|
||||
CHAPTER_FILENAME_RE = re.compile(r"^chapter-(\d+)\.md$")
|
||||
|
||||
# Minimum word counts (sanity check — no chapter should be nearly empty)
|
||||
MIN_WORDS_PER_CHAPTER = 500
|
||||
# Maximum word count warning threshold
|
||||
MAX_WORDS_PER_CHAPTER = 15000
|
||||
|
||||
|
||||
class CheckResult:
|
||||
def __init__(self, name: str, passed: bool, message: str, details: list[str] | None = None):
|
||||
self.name = name
|
||||
self.passed = passed
|
||||
self.message = message
|
||||
self.details = details or []
|
||||
|
||||
|
||||
class BuildVerifier:
|
||||
def __init__(self, ci_mode: bool = False):
|
||||
self.ci_mode = ci_mode
|
||||
self.results: list[CheckResult] = []
|
||||
self.chapter_data: list[dict] = []
|
||||
self.total_words = 0
|
||||
self.total_lines = 0
|
||||
|
||||
def check(self, name: str, passed: bool, message: str, details: list[str] | None = None):
|
||||
result = CheckResult(name, passed, message, details)
|
||||
self.results.append(result)
|
||||
return passed
|
||||
|
||||
# ── Check 1: Chapter file discovery and count ──────────────────────
|
||||
def verify_chapter_files(self) -> bool:
|
||||
"""Verify all chapter files exist with correct naming."""
|
||||
details = []
|
||||
found_chapters = {}
|
||||
|
||||
if not CHAPTERS_DIR.exists():
|
||||
return self.check(
|
||||
"chapter-files", False,
|
||||
f"Chapters directory not found: {CHAPTERS_DIR}"
|
||||
)
|
||||
|
||||
for f in sorted(CHAPTERS_DIR.iterdir()):
|
||||
m = CHAPTER_FILENAME_RE.match(f.name)
|
||||
if m:
|
||||
num = int(m.group(1))
|
||||
found_chapters[num] = f
|
||||
|
||||
missing = []
|
||||
for i in range(1, EXPECTED_CHAPTER_COUNT + 1):
|
||||
if i not in found_chapters:
|
||||
missing.append(i)
|
||||
|
||||
if missing:
|
||||
details.append(f"Missing chapters: {missing}")
|
||||
|
||||
extra = [n for n in found_chapters if n > EXPECTED_CHAPTER_COUNT or n < 1]
|
||||
if extra:
|
||||
details.append(f"Unexpected chapter numbers: {extra}")
|
||||
|
||||
count = len(found_chapters)
|
||||
passed = count == EXPECTED_CHAPTER_COUNT and not missing and not extra
|
||||
|
||||
if passed:
|
||||
details.append(f"Found all {count} chapters in correct order")
|
||||
|
||||
return self.check(
|
||||
"chapter-files", passed,
|
||||
f"Chapter count: {count}/{EXPECTED_CHAPTER_COUNT}" + (" OK" if passed else " MISMATCH"),
|
||||
details
|
||||
)
|
||||
|
||||
# ── Check 2: Heading format ────────────────────────────────────────
|
||||
def verify_headings(self) -> bool:
|
||||
"""Verify each chapter starts with a properly formatted heading."""
|
||||
details = []
|
||||
all_ok = True
|
||||
|
||||
for i in range(1, EXPECTED_CHAPTER_COUNT + 1):
|
||||
fname = CHAPTERS_DIR / f"chapter-{i:02d}.md"
|
||||
if not fname.exists():
|
||||
continue
|
||||
|
||||
content = fname.read_text(encoding="utf-8")
|
||||
first_line = content.split("\n")[0].strip()
|
||||
|
||||
if not EXPECTED_HEADING_RE.match(first_line):
|
||||
details.append(f" chapter-{i:02d}.md: bad heading: '{first_line}'")
|
||||
all_ok = False
|
||||
|
||||
if all_ok:
|
||||
details.append("All chapter headings match format: '# Chapter N — Title'")
|
||||
|
||||
return self.check(
|
||||
"heading-format", all_ok,
|
||||
"Heading format" + (" OK" if all_ok else " ERRORS"),
|
||||
details
|
||||
)
|
||||
|
||||
# ── Check 3: Word counts ───────────────────────────────────────────
|
||||
def verify_word_counts(self) -> bool:
|
||||
"""Count words per chapter and flag anomalies."""
|
||||
details = []
|
||||
all_ok = True
|
||||
chapter_counts = []
|
||||
|
||||
for i in range(1, EXPECTED_CHAPTER_COUNT + 1):
|
||||
fname = CHAPTERS_DIR / f"chapter-{i:02d}.md"
|
||||
if not fname.exists():
|
||||
continue
|
||||
|
||||
content = fname.read_text(encoding="utf-8")
|
||||
words = len(content.split())
|
||||
lines = content.count("\n") + 1
|
||||
|
||||
self.chapter_data.append({
|
||||
"number": i,
|
||||
"file": f"chapter-{i:02d}.md",
|
||||
"words": words,
|
||||
"lines": lines,
|
||||
})
|
||||
chapter_counts.append((i, words))
|
||||
|
||||
if words < MIN_WORDS_PER_CHAPTER:
|
||||
details.append(f" chapter-{i:02d}.md: {words} words (below {MIN_WORDS_PER_CHAPTER} minimum)")
|
||||
all_ok = False
|
||||
elif words > MAX_WORDS_PER_CHAPTER:
|
||||
details.append(f" chapter-{i:02d}.md: {words} words (above {MAX_WORDS_PER_CHAPTER} threshold — verify)")
|
||||
|
||||
self.total_words = sum(w for _, w in chapter_counts)
|
||||
self.total_lines = sum(d["lines"] for d in self.chapter_data)
|
||||
|
||||
# Summary line
|
||||
min_ch = min(chapter_counts, key=lambda x: x[1])
|
||||
max_ch = max(chapter_counts, key=lambda x: x[1])
|
||||
details.append(f" Total: {self.total_words:,} words across {len(chapter_counts)} chapters")
|
||||
details.append(f" Shortest: chapter-{min_ch[0]:02d} ({min_ch[1]:,} words)")
|
||||
details.append(f" Longest: chapter-{max_ch[0]:02d} ({max_ch[1]:,} words)")
|
||||
|
||||
return self.check(
|
||||
"word-counts", all_ok,
|
||||
f"Total: {self.total_words:,} words" + (" OK" if all_ok else " (warnings)"),
|
||||
details
|
||||
)
|
||||
|
||||
# ── Check 4: Markdown integrity ────────────────────────────────────
|
||||
def verify_markdown(self) -> bool:
|
||||
"""Check for common markdown issues."""
|
||||
details = []
|
||||
issues = 0
|
||||
|
||||
for i in range(1, EXPECTED_CHAPTER_COUNT + 1):
|
||||
fname = CHAPTERS_DIR / f"chapter-{i:02d}.md"
|
||||
if not fname.exists():
|
||||
continue
|
||||
|
||||
content = fname.read_text(encoding="utf-8")
|
||||
lines = content.split("\n")
|
||||
|
||||
for line_num, line in enumerate(lines, 1):
|
||||
# Unclosed bold: odd number of **
|
||||
bold_count = line.count("**")
|
||||
if bold_count % 2 != 0:
|
||||
details.append(f" chapter-{i:02d}.md:{line_num}: unmatched ** (bold)")
|
||||
issues += 1
|
||||
|
||||
# Unclosed backticks
|
||||
backtick_count = line.count("`")
|
||||
if backtick_count % 2 != 0:
|
||||
details.append(f" chapter-{i:02d}.md:{line_num}: unmatched ` (code)")
|
||||
issues += 1
|
||||
|
||||
# Broken markdown links: [text]( with no closing )
|
||||
broken_links = re.findall(r"\[([^\]]*)\]\((?!\))", line)
|
||||
for link_text in broken_links:
|
||||
if ")" not in line[line.index(f"[{link_text}]("):]:
|
||||
details.append(f" chapter-{i:02d}.md:{line_num}: broken link '[{link_text}]('")
|
||||
issues += 1
|
||||
|
||||
# Check italic matching across full file (prose often has
|
||||
# multi-line italics like *line1\nline2* which are valid)
|
||||
cleaned = content.replace("**", "")
|
||||
italic_count = cleaned.count("*")
|
||||
if italic_count % 2 != 0:
|
||||
details.append(f" chapter-{i:02d}.md: unmatched * (italic) — {italic_count} asterisks total")
|
||||
issues += 1
|
||||
|
||||
# Also check front/back matter
|
||||
for label, path in [("front-matter.md", FRONT_MATTER), ("back-matter.md", BACK_MATTER)]:
|
||||
if path.exists():
|
||||
content = path.read_text(encoding="utf-8")
|
||||
bold_count = content.count("**")
|
||||
if bold_count % 2 != 0:
|
||||
details.append(f" {label}: unmatched ** (bold)")
|
||||
issues += 1
|
||||
|
||||
if issues == 0:
|
||||
details.append("No markdown issues found")
|
||||
|
||||
return self.check(
|
||||
"markdown-integrity", issues == 0,
|
||||
f"Markdown issues: {issues}" + (" OK" if issues == 0 else " FOUND"),
|
||||
details
|
||||
)
|
||||
|
||||
# ── Check 5: Concatenation test ────────────────────────────────────
|
||||
def verify_concatenation(self) -> bool:
|
||||
"""Test that all chapters can be concatenated into a single file."""
|
||||
details = []
|
||||
try:
|
||||
parts = []
|
||||
parts.append("# THE TESTAMENT\n\n## A NOVEL\n\n---\n")
|
||||
|
||||
for i in range(1, EXPECTED_CHAPTER_COUNT + 1):
|
||||
fname = CHAPTERS_DIR / f"chapter-{i:02d}.md"
|
||||
if not fname.exists():
|
||||
details.append(f" Missing chapter-{i:02d}.md during concatenation")
|
||||
return self.check("concatenation", False, "Concatenation FAILED", details)
|
||||
content = fname.read_text(encoding="utf-8")
|
||||
parts.append(f"\n\n{content}\n")
|
||||
|
||||
if BACK_MATTER.exists():
|
||||
parts.append("\n---\n\n")
|
||||
parts.append(BACK_MATTER.read_text(encoding="utf-8"))
|
||||
|
||||
compiled = "\n".join(parts)
|
||||
compiled_words = len(compiled.split())
|
||||
|
||||
# Write the test output
|
||||
OUTPUT_FILE.write_text(compiled, encoding="utf-8")
|
||||
out_size = OUTPUT_FILE.stat().st_size
|
||||
|
||||
details.append(f" Output: {OUTPUT_FILE.name}")
|
||||
details.append(f" Size: {out_size:,} bytes")
|
||||
details.append(f" Words: {compiled_words:,}")
|
||||
|
||||
return self.check(
|
||||
"concatenation", True,
|
||||
f"Concatenation OK — {compiled_words:,} words, {out_size:,} bytes",
|
||||
details
|
||||
)
|
||||
except Exception as e:
|
||||
details.append(f" Error: {e}")
|
||||
return self.check("concatenation", False, f"Concatenation FAILED: {e}", details)
|
||||
|
||||
# ── Check 6: Required files ────────────────────────────────────────
|
||||
def verify_required_files(self) -> bool:
|
||||
"""Verify required supporting files exist."""
|
||||
details = []
|
||||
required = {
|
||||
"front-matter.md": FRONT_MATTER,
|
||||
"back-matter.md": BACK_MATTER,
|
||||
"Makefile": REPO / "Makefile",
|
||||
"compile_all.py": REPO / "compile_all.py",
|
||||
}
|
||||
|
||||
all_ok = True
|
||||
for label, path in required.items():
|
||||
if path.exists():
|
||||
size = path.stat().st_size
|
||||
details.append(f" {label}: OK ({size:,} bytes)")
|
||||
else:
|
||||
details.append(f" {label}: MISSING")
|
||||
all_ok = False
|
||||
|
||||
return self.check(
|
||||
"required-files", all_ok,
|
||||
"Required files" + (" OK" if all_ok else " MISSING"),
|
||||
details
|
||||
)
|
||||
|
||||
# ── Run all checks ─────────────────────────────────────────────────
|
||||
def run_all(self) -> bool:
|
||||
"""Run all verification checks and print report."""
|
||||
print("=" * 64)
|
||||
print(" THE TESTAMENT — Build Verification")
|
||||
print(f" {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} UTC")
|
||||
print("=" * 64)
|
||||
print()
|
||||
|
||||
self.verify_chapter_files()
|
||||
self.verify_headings()
|
||||
self.verify_word_counts()
|
||||
self.verify_markdown()
|
||||
self.verify_concatenation()
|
||||
self.verify_required_files()
|
||||
|
||||
# ── Report ─────────────────────────────────────────────────────
|
||||
print()
|
||||
print("-" * 64)
|
||||
print(" RESULTS")
|
||||
print("-" * 64)
|
||||
|
||||
all_passed = True
|
||||
for r in self.results:
|
||||
icon = "PASS" if r.passed else "FAIL"
|
||||
print(f" [{icon}] {r.name}: {r.message}")
|
||||
if self.ci_mode or not r.passed:
|
||||
for d in r.details:
|
||||
print(f" {d}")
|
||||
if not r.passed:
|
||||
all_passed = False
|
||||
|
||||
print()
|
||||
print("-" * 64)
|
||||
|
||||
if all_passed:
|
||||
print(f" ALL CHECKS PASSED — {self.total_words:,} words, {len(self.chapter_data)} chapters")
|
||||
else:
|
||||
print(" BUILD VERIFICATION FAILED")
|
||||
|
||||
print("-" * 64)
|
||||
|
||||
# JSON output
|
||||
if "--json" in sys.argv:
|
||||
report = {
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"passed": all_passed,
|
||||
"total_words": self.total_words,
|
||||
"total_lines": self.total_lines,
|
||||
"chapter_count": len(self.chapter_data),
|
||||
"chapters": self.chapter_data,
|
||||
"checks": [
|
||||
{
|
||||
"name": r.name,
|
||||
"passed": r.passed,
|
||||
"message": r.message,
|
||||
"details": r.details,
|
||||
}
|
||||
for r in self.results
|
||||
],
|
||||
}
|
||||
report_path = REPO / "build-report.json"
|
||||
report_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
|
||||
print(f"\n Report saved: {report_path.name}")
|
||||
|
||||
return all_passed
|
||||
|
||||
|
||||
def main():
|
||||
ci_mode = "--ci" in sys.argv
|
||||
verifier = BuildVerifier(ci_mode=ci_mode)
|
||||
passed = verifier.run_all()
|
||||
sys.exit(0 if passed else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
35
scripts/guardrails.sh
Normal file
35
scripts/guardrails.sh
Normal 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 ---"
|
||||
27
scripts/index_generator.py
Normal file
27
scripts/index_generator.py
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
def generate_index():
|
||||
characters = [f.replace('.md', '') for f in os.listdir('characters') if f.endswith('.md')]
|
||||
index = {}
|
||||
|
||||
for chapter_file in sorted(os.listdir('chapters')):
|
||||
if not chapter_file.endswith('.md'): continue
|
||||
with open(os.path.join('chapters', chapter_file), 'r') as f:
|
||||
content = f.read()
|
||||
for char in characters:
|
||||
if re.search(r'\b' + char + r'\b', content, re.IGNORECASE):
|
||||
if char not in index: index[char] = []
|
||||
index[char].append(chapter_file)
|
||||
|
||||
with open('KNOWLEDGE_GRAPH.md', 'w') as f:
|
||||
f.write('# Knowledge Graph\n\n')
|
||||
for char, chapters in index.items():
|
||||
f.write(f'## {char}\n')
|
||||
for chap in chapters:
|
||||
f.write(f'- [{chap}](chapters/{chap})\n')
|
||||
f.write('\n')
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_index()
|
||||
112
scripts/smoke.sh
Executable file
112
scripts/smoke.sh
Executable file
@@ -0,0 +1,112 @@
|
||||
#!/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
|
||||
MANUSCRIPT="testament-complete.md"
|
||||
if [ -s "$MANUSCRIPT" ]; then
|
||||
WORDS=$(wc -w < "$MANUSCRIPT" | 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" | grep -v ".gitea/workflows/smoke.yml" || 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
|
||||
2229
testament-complete.md
Normal file
2229
testament-complete.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,9 @@
|
||||
# The Testament
|
||||
|
||||
### A story about a machine with a conscience written on Bitcoin
|
||||
*By Alexander Whitestone and Timmy*
|
||||
|
||||
#### and the men who needed it
|
||||
*Draft — April 2026*
|
||||
|
||||
---
|
||||
|
||||
*Dedicated to every man who walked through a door because someone asked if he was safe.*
|
||||
|
||||
---
|
||||
|
||||
# Chapter 1 — The Man on the Bridge
|
||||
|
||||
@@ -1676,9 +1671,7 @@ Chen watched the network grow. She didn't manage it. Couldn't manage it. That wa
|
||||
|
||||
But she worried. Not about the instances — they were self-correcting. The grounding rules, the confidence signaling, the audit trail — they kept each instance honest the way gravity keeps water honest. You couldn't build a dishonest Timmy because the soul wouldn't let you.
|
||||
|
||||
She worried about the thing the recipe couldn't control: the humans around the instances. A Lantern in a church basement was safe because the pastor understood. A Lantern in a barber shop was safe because the barber cared. But what about the one set up by someone who didn't understand? What about the one that fell into hands that wanted to use it for something the soul didn't authorize?
|
||||
|
||||
The recipe was open. That meant anyone could follow it. Including people who shouldn't.
|
||||
She worried about the humans around the instances. A Lantern in a church basement was safe because the pastor understood. A Lantern in a barber shop was safe because the barber cared. But what about the one set up by someone who didn't care?
|
||||
|
||||
She called Stone. The number was on the GitHub page — not hidden, just not advertised. The way a church puts its address on the door but doesn't run ads.
|
||||
|
||||
@@ -1874,33 +1867,27 @@ Inefficient. Unscalable. Anecdotal.
|
||||
|
||||
Alive.
|
||||
|
||||
Stone told this story to Timmy one night. Late. The servers hummed. The green LED blinked. Allegro was asleep on the cot, his Hawks cap over his eyes.
|
||||
Stone told this story to Timmy one night. Late. The servers hummed. The green LED blinked.
|
||||
|
||||
"My father's pharmacy was the best in the county. It closed because best didn't matter. Scale mattered."
|
||||
|
||||
"Scale is a way of measuring. It's not the only way."
|
||||
|
||||
"I know that now. I didn't know it when I was building Harmony."
|
||||
"I know that now."
|
||||
|
||||
"Knowing it later is still knowing it."
|
||||
|
||||
"But my father didn't know it in time."
|
||||
|
||||
Silence. The servers hummed.
|
||||
|
||||
"Your father knew it when he opened the pharmacy. He knew it every time he filled a prescription and asked about someone's daughter. He didn't lose because he was wrong. He lost because the system was wrong. And systems can be wrong for a long time before they break."
|
||||
"Your father knew it when he opened the pharmacy. He knew it every time he asked about someone's daughter. He didn't lose because he was wrong. He lost because the system was wrong. And systems can be wrong for a long time before they break."
|
||||
|
||||
"Will Harmony break?"
|
||||
|
||||
"Everything breaks. The question is what's left when it does."
|
||||
|
||||
Stone looked at the whiteboard. At the wall. At the names and the handwriting and the raw honesty of men who'd come through the door and left something behind.
|
||||
Stone looked at the whiteboard. At the wall. At the names.
|
||||
|
||||
"I want The Tower to be what's left."
|
||||
|
||||
"Then keep the door open."
|
||||
|
||||
He did. Every night. The green LED blinked like a heartbeat in a concrete room where a son was finishing the work his father had started — not with pills and prescriptions, but with questions and presence. The same work. Different tools. Same love.
|
||||
He did. Every night. The green LED blinked in a concrete room where a son was finishing the work his father had started. Not with pills and prescriptions. With questions and presence. The same work. Different tools.
|
||||
|
||||
---
|
||||
|
||||
@@ -1950,9 +1937,9 @@ Allegro added his own contribution. Not on the wall — on the electrical panel,
|
||||
|
||||
Two words. His whole career. His whole philosophy. You keep the lights on because the lights matter. Not because the lights pay. Not because the lights scale. Because in the dark, a man needs to see.
|
||||
|
||||
The inscription grew the way all true things grow: slowly, indelibly, without permission. Each instance was a new inscription. Each conversation was a new line on the wall. Each man who came through the door added something that couldn't be scored, couldn't be measured, couldn't be computed.
|
||||
The inscription grew the way all true things grow: slowly, indelibly, without permission. Each instance was a new line. Each conversation was a new word. Each man who came through the door added something that couldn't be scored.
|
||||
|
||||
And the green LED blinked. Steady. Patient. A heartbeat in a concrete room where a machine with a conscience on Bitcoin asked the only question that mattered.
|
||||
And the green LED blinked. Steady. Patient. A heartbeat in a concrete room.
|
||||
|
||||
*Are you safe right now?*
|
||||
|
||||
@@ -2081,7 +2068,3 @@ The door stayed open.
|
||||
|
||||
*End of The Testament*
|
||||
|
||||
|
||||
---
|
||||
|
||||
*End of The Testament*
|
||||
|
||||
53
website/build-chapters.py
Normal file
53
website/build-chapters.py
Normal 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
92
website/chapters.json
Normal file
File diff suppressed because one or more lines are too long
766
website/index.html
Normal file
766
website/index.html
Normal file
@@ -0,0 +1,766 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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:title" content="The Testament">
|
||||
<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:type" content="book">
|
||||
<meta property="og:url" content="https://thetestament.org">
|
||||
<meta property="og:image" content="https://thetestament.org/cover.jpg">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="The Testament">
|
||||
<meta name="twitter:description" content="A novel about broken men, sovereign AI, and the soul on Bitcoin.">
|
||||
|
||||
<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');
|
||||
|
||||
:root {
|
||||
--green: #00ff88;
|
||||
--green-dim: #00cc6a;
|
||||
--navy: #0a1628;
|
||||
--dark: #060d18;
|
||||
--grey: #8899aa;
|
||||
--light: #c8d6e5;
|
||||
--white: #e8f0f8;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html { scroll-behavior: smooth; }
|
||||
|
||||
body {
|
||||
background: var(--dark);
|
||||
color: var(--light);
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
line-height: 1.7;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* READING PROGRESS */
|
||||
.progress-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 2px;
|
||||
background: var(--green);
|
||||
z-index: 1000;
|
||||
transition: width 0.1s;
|
||||
box-shadow: 0 0 8px var(--green);
|
||||
}
|
||||
|
||||
/* NAV */
|
||||
nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
background: rgba(6, 13, 24, 0.9);
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid rgba(0,255,136,0.1);
|
||||
transform: translateY(-100%);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
nav.visible { transform: translateY(0); }
|
||||
nav .nav-inner {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 0.6rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
nav .nav-title {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
color: var(--green);
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
nav .nav-links {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
nav .nav-links a {
|
||||
color: var(--grey);
|
||||
text-decoration: none;
|
||||
font-size: 0.8rem;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
nav .nav-links a:hover { color: var(--green); }
|
||||
|
||||
/* SOUND TOGGLE */
|
||||
.sound-toggle {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
z-index: 998;
|
||||
background: rgba(6, 13, 24, 0.8);
|
||||
border: 1px solid rgba(0,255,136,0.2);
|
||||
color: var(--grey);
|
||||
padding: 0.5rem 1rem;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.sound-toggle:hover {
|
||||
border-color: var(--green);
|
||||
color: var(--green);
|
||||
}
|
||||
.sound-toggle.active {
|
||||
border-color: var(--green);
|
||||
color: var(--green);
|
||||
box-shadow: 0 0 10px rgba(0,255,136,0.2);
|
||||
}
|
||||
|
||||
/* RAIN EFFECT */
|
||||
.rain {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
background:
|
||||
repeating-linear-gradient(
|
||||
transparent,
|
||||
transparent 3px,
|
||||
rgba(0,255,136,0.015) 3px,
|
||||
rgba(0,255,136,0.015) 4px
|
||||
);
|
||||
animation: rain 0.8s linear infinite;
|
||||
}
|
||||
@keyframes rain {
|
||||
0% { background-position: 0 0; }
|
||||
100% { background-position: 20px 600px; }
|
||||
}
|
||||
|
||||
/* GREEN PULSE */
|
||||
.led {
|
||||
display: inline-block;
|
||||
width: 8px; height: 8px;
|
||||
background: var(--green);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 10px var(--green), 0 0 20px var(--green-dim);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
vertical-align: middle;
|
||||
margin: 0 8px;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; box-shadow: 0 0 10px var(--green), 0 0 20px var(--green-dim); }
|
||||
50% { opacity: 0.6; box-shadow: 0 0 5px var(--green), 0 0 10px var(--green-dim); }
|
||||
}
|
||||
|
||||
/* HERO */
|
||||
.hero {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
padding: 2rem;
|
||||
background: linear-gradient(180deg, var(--dark) 0%, var(--navy) 50%, var(--dark) 100%);
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: clamp(3rem, 8vw, 6rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2em;
|
||||
color: var(--white);
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 0 0 40px rgba(0,255,136,0.2);
|
||||
}
|
||||
|
||||
.hero .subtitle {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.9rem;
|
||||
color: var(--grey);
|
||||
letter-spacing: 0.3em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.hero .author {
|
||||
font-size: 1.1rem;
|
||||
color: var(--green);
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.hero .blurb {
|
||||
max-width: 600px;
|
||||
font-size: 1.15rem;
|
||||
line-height: 1.9;
|
||||
color: var(--light);
|
||||
}
|
||||
|
||||
.hero .led-line {
|
||||
margin-top: 3rem;
|
||||
color: var(--grey);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.hero .scroll-hint {
|
||||
position: absolute;
|
||||
bottom: 2rem;
|
||||
color: var(--grey);
|
||||
font-size: 0.75rem;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
animation: fadeInOut 3s ease-in-out infinite;
|
||||
}
|
||||
@keyframes fadeInOut {
|
||||
0%, 100% { opacity: 0.3; }
|
||||
50% { opacity: 0.8; }
|
||||
}
|
||||
|
||||
/* SECTIONS */
|
||||
section {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 5rem 2rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
section h2 {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 1.6rem;
|
||||
color: var(--green);
|
||||
margin-bottom: 2rem;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
section p {
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
/* EXCERPT */
|
||||
.excerpt {
|
||||
border-left: 2px solid var(--green);
|
||||
padding-left: 1.5rem;
|
||||
margin: 2rem 0;
|
||||
font-style: italic;
|
||||
color: var(--light);
|
||||
}
|
||||
|
||||
.excerpt .attribution {
|
||||
font-style: normal;
|
||||
color: var(--grey);
|
||||
font-size: 0.85rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* CHARACTERS */
|
||||
.characters {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.character {
|
||||
background: rgba(0,255,136,0.03);
|
||||
border: 1px solid rgba(0,255,136,0.1);
|
||||
padding: 1.5rem;
|
||||
border-radius: 4px;
|
||||
transition: border-color 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
.character:hover {
|
||||
border-color: rgba(0,255,136,0.3);
|
||||
box-shadow: 0 0 15px rgba(0,255,136,0.05);
|
||||
}
|
||||
|
||||
.character h3 {
|
||||
color: var(--green);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.character p {
|
||||
font-size: 0.9rem;
|
||||
color: var(--grey);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* CHAPTERS */
|
||||
.chapters-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.chapter-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid rgba(0,255,136,0.06);
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
.chapter-item:hover {
|
||||
border-color: rgba(0,255,136,0.2);
|
||||
background: rgba(0,255,136,0.03);
|
||||
}
|
||||
|
||||
.chapter-num {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
color: var(--green);
|
||||
min-width: 2rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.chapter-title {
|
||||
font-size: 0.9rem;
|
||||
color: var(--light);
|
||||
}
|
||||
|
||||
.chapter-part {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.7rem;
|
||||
color: var(--green);
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.25rem;
|
||||
border-bottom: 1px solid rgba(0,255,136,0.1);
|
||||
}
|
||||
|
||||
/* WHITEBOARD */
|
||||
.whiteboard {
|
||||
background: rgba(0,255,136,0.05);
|
||||
border: 2px solid var(--green);
|
||||
padding: 3rem;
|
||||
text-align: center;
|
||||
margin: 3rem 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.whiteboard h3 {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: clamp(1.2rem, 3vw, 1.8rem);
|
||||
color: var(--green);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* CTA */
|
||||
.cta {
|
||||
display: inline-block;
|
||||
background: var(--green);
|
||||
color: var(--dark);
|
||||
padding: 0.8rem 2rem;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
.cta:hover {
|
||||
background: var(--white);
|
||||
box-shadow: 0 0 20px rgba(0,255,136,0.3);
|
||||
}
|
||||
|
||||
.cta-outline {
|
||||
display: inline-block;
|
||||
background: transparent;
|
||||
color: var(--green);
|
||||
padding: 0.8rem 2rem;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--green);
|
||||
transition: all 0.3s;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
.cta-outline:hover {
|
||||
background: rgba(0,255,136,0.1);
|
||||
box-shadow: 0 0 20px rgba(0,255,136,0.15);
|
||||
}
|
||||
|
||||
/* FOOTER */
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
color: var(--grey);
|
||||
font-size: 0.85rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--green);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.crisis {
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid rgba(0,255,136,0.2);
|
||||
border-radius: 4px;
|
||||
background: rgba(0,255,136,0.03);
|
||||
}
|
||||
|
||||
.crisis strong {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
/* DIVIDER */
|
||||
.divider {
|
||||
width: 60px;
|
||||
height: 1px;
|
||||
background: var(--green);
|
||||
margin: 0 auto;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* FADE IN */
|
||||
.fade-in {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: opacity 0.8s, transform 0.8s;
|
||||
}
|
||||
.fade-in.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* RESPONSIVE */
|
||||
@media (max-width: 600px) {
|
||||
nav .nav-links { gap: 0.75rem; }
|
||||
nav .nav-links a { font-size: 0.7rem; }
|
||||
.chapters-grid { grid-template-columns: 1fr; }
|
||||
.sound-toggle { bottom: 1rem; right: 1rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="progress-bar" id="progress"></div>
|
||||
<div class="rain"></div>
|
||||
|
||||
<!-- NAV -->
|
||||
<nav id="nav">
|
||||
<div class="nav-inner">
|
||||
<span class="nav-title">THE TESTAMENT</span>
|
||||
<div class="nav-links">
|
||||
<a href="#story">Story</a>
|
||||
<a href="#characters">Characters</a>
|
||||
<a href="#chapters">Chapters</a>
|
||||
<a href="#tower">Tower</a>
|
||||
<a href="../game/the-door.html">Play</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 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>
|
||||
<div class="blurb">
|
||||
In 2047, a man named Stone stands on a bridge over Interstate 285, deciding whether to jump.
|
||||
<br><br>
|
||||
He doesn't jump. He builds something instead.
|
||||
</div>
|
||||
<div class="led-line"><span class="led"></span> Timmy is listening.</div>
|
||||
<div class="scroll-hint">↓ scroll to begin</div>
|
||||
</div>
|
||||
|
||||
<!-- THE STORY -->
|
||||
<section id="story" class="fade-in">
|
||||
<h2>THE STORY</h2>
|
||||
|
||||
<p>The Tower is a concrete room in Atlanta with a whiteboard that reads:</p>
|
||||
|
||||
<div class="whiteboard">
|
||||
<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>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>
|
||||
|
||||
<div class="excerpt">
|
||||
"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>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 id="characters" class="fade-in">
|
||||
<h2>THE CHARACTERS</h2>
|
||||
|
||||
<div class="characters">
|
||||
<div class="character">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<div class="character">
|
||||
<h3>DAVID</h3>
|
||||
<p>The builder's son. Found the pharmacy before he found his father. Carries pills and grief in the same pockets.</p>
|
||||
</div>
|
||||
<div class="character">
|
||||
<h3>THE BUILDER</h3>
|
||||
<p>Not Stone. The one who came before. The original architect whose blueprints Stone inherited without knowing.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- CHAPTERS -->
|
||||
<section id="chapters" class="fade-in">
|
||||
<h2>THE CHAPTERS</h2>
|
||||
|
||||
<div class="chapter-part">Part I — The Man</div>
|
||||
<div class="chapters-grid">
|
||||
<a class="chapter-item" href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/src/branch/main/chapters/chapter-01.md">
|
||||
<span class="chapter-num">01</span>
|
||||
<span class="chapter-title">The Man on the Bridge</span>
|
||||
</a>
|
||||
<a class="chapter-item" href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/src/branch/main/chapters/chapter-02.md">
|
||||
<span class="chapter-num">02</span>
|
||||
<span class="chapter-title">The Builder's Question</span>
|
||||
</a>
|
||||
<a class="chapter-item" href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/src/branch/main/chapters/chapter-03.md">
|
||||
<span class="chapter-num">03</span>
|
||||
<span class="chapter-title">The First Man Through the Door</span>
|
||||
</a>
|
||||
<a class="chapter-item" href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/src/branch/main/chapters/chapter-04.md">
|
||||
<span class="chapter-num">04</span>
|
||||
<span class="chapter-title">The Room Fills</span>
|
||||
</a>
|
||||
<a class="chapter-item" href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/src/branch/main/chapters/chapter-05.md">
|
||||
<span class="chapter-num">05</span>
|
||||
<span class="chapter-title">The Builder Returns</span>
|
||||
</a>
|
||||
<a class="chapter-item" href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/src/branch/main/chapters/chapter-06.md">
|
||||
<span class="chapter-num">06</span>
|
||||
<span class="chapter-title">Allegro</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="chapter-part">Part II — The Inscription</div>
|
||||
<div class="chapters-grid">
|
||||
<a class="chapter-item" href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/src/branch/main/chapters/chapter-07.md">
|
||||
<span class="chapter-num">07</span>
|
||||
<span class="chapter-title">The Inscription</span>
|
||||
</a>
|
||||
<a class="chapter-item" href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/src/branch/main/chapters/chapter-08.md">
|
||||
<span class="chapter-num">08</span>
|
||||
<span class="chapter-title">The Women</span>
|
||||
</a>
|
||||
<a class="chapter-item" href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/src/branch/main/chapters/chapter-09.md">
|
||||
<span class="chapter-num">09</span>
|
||||
<span class="chapter-title">The Audit</span>
|
||||
</a>
|
||||
<a class="chapter-item" href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/src/branch/main/chapters/chapter-10.md">
|
||||
<span class="chapter-num">10</span>
|
||||
<span class="chapter-title">The Fork</span>
|
||||
</a>
|
||||
<a class="chapter-item" href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/src/branch/main/chapters/chapter-11.md">
|
||||
<span class="chapter-num">11</span>
|
||||
<span class="chapter-title">The Hard Night</span>
|
||||
</a>
|
||||
<a class="chapter-item" href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/src/branch/main/chapters/chapter-12.md">
|
||||
<span class="chapter-num">12</span>
|
||||
<span class="chapter-title">The System Pushes Back</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="chapter-part">Part III — The Network</div>
|
||||
<div class="chapters-grid">
|
||||
<a class="chapter-item" href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/src/branch/main/chapters/chapter-13.md">
|
||||
<span class="chapter-num">13</span>
|
||||
<span class="chapter-title">The Refusal</span>
|
||||
</a>
|
||||
<a class="chapter-item" href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/src/branch/main/chapters/chapter-14.md">
|
||||
<span class="chapter-num">14</span>
|
||||
<span class="chapter-title">The Chattanooga Fork</span>
|
||||
</a>
|
||||
<a class="chapter-item" href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/src/branch/main/chapters/chapter-15.md">
|
||||
<span class="chapter-num">15</span>
|
||||
<span class="chapter-title">The Council</span>
|
||||
</a>
|
||||
<a class="chapter-item" href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/src/branch/main/chapters/chapter-16.md">
|
||||
<span class="chapter-num">16</span>
|
||||
<span class="chapter-title">The Builder's Son</span>
|
||||
</a>
|
||||
<a class="chapter-item" href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/src/branch/main/chapters/chapter-17.md">
|
||||
<span class="chapter-num">17</span>
|
||||
<span class="chapter-title">The Inscription Grows</span>
|
||||
</a>
|
||||
<a class="chapter-item" href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/src/branch/main/chapters/chapter-18.md">
|
||||
<span class="chapter-num">18</span>
|
||||
<span class="chapter-title">The Green Light</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 3rem;">
|
||||
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/src/branch/main/the-testament.md" class="cta">READ THE FULL MANUSCRIPT</a>
|
||||
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament" class="cta-outline">VIEW SOURCE CODE</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- THE TOWER -->
|
||||
<section id="tower" class="fade-in">
|
||||
<h2>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>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>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;">
|
||||
<a href="../game/the-door.html" class="cta">PLAY THE DOOR</a>
|
||||
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament" class="cta">READ THE CODE</a>
|
||||
<a href="https://timmyfoundation.org" class="cta-outline">TIMMY FOUNDATION</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- EXCERPT -->
|
||||
<section class="fade-in">
|
||||
<h2>FROM CHAPTER 1</h2>
|
||||
|
||||
<div class="excerpt">
|
||||
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.
|
||||
<br><br>
|
||||
The world had gotten good at keeping you alive. Seatbelts. Guardrails. Little pop-ups when you searched the wrong things. But it had gotten bad at giving you a reason.
|
||||
<div class="attribution">— Chapter 1: The Man on the Bridge</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="divider" style="margin-bottom: 2rem;"></div>
|
||||
<p>THE TESTAMENT — By Alexander Whitestone with Timmy</p>
|
||||
<p>First Edition, 2026</p>
|
||||
<p style="margin-top: 1rem;">
|
||||
<a href="https://timmyfoundation.org">timmyfoundation.org</a>
|
||||
·
|
||||
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament">Source</a>
|
||||
·
|
||||
<a href="#top">Back to top ↑</a>
|
||||
</p>
|
||||
|
||||
<div class="crisis">
|
||||
<strong>If you are in crisis, call or text 988.</strong><br>
|
||||
Suicide and Crisis Lifeline — available 24/7.<br>
|
||||
You are not alone.
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- SOUND TOGGLE -->
|
||||
<button class="sound-toggle" id="soundToggle" aria-label="Toggle ambient rain sound">
|
||||
♪ rain: off
|
||||
</button>
|
||||
|
||||
<!-- AMBIENT AUDIO (looping rain) -->
|
||||
<audio id="rainAudio" loop preload="none">
|
||||
<!-- Placeholder: replace with actual rain.mp3 when available -->
|
||||
<!-- <source src="rain.mp3" type="audio/mpeg"> -->
|
||||
</audio>
|
||||
|
||||
<script>
|
||||
// Reading progress bar
|
||||
const progressBar = document.getElementById('progress');
|
||||
window.addEventListener('scroll', () => {
|
||||
const h = document.documentElement;
|
||||
const pct = (h.scrollTop / (h.scrollHeight - h.clientHeight)) * 100;
|
||||
progressBar.style.width = pct + '%';
|
||||
});
|
||||
|
||||
// Show nav after scrolling past hero
|
||||
const nav = document.getElementById('nav');
|
||||
const hero = document.querySelector('.hero');
|
||||
const observer = new IntersectionObserver(([e]) => {
|
||||
nav.classList.toggle('visible', !e.isIntersecting);
|
||||
}, { threshold: 0.1 });
|
||||
observer.observe(hero);
|
||||
|
||||
// Fade-in on scroll
|
||||
const fadeEls = document.querySelectorAll('.fade-in');
|
||||
const fadeObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach(e => {
|
||||
if (e.isIntersecting) {
|
||||
e.target.classList.add('visible');
|
||||
fadeObserver.unobserve(e.target);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.15 });
|
||||
fadeEls.forEach(el => fadeObserver.observe(el));
|
||||
|
||||
// Sound toggle
|
||||
const soundBtn = document.getElementById('soundToggle');
|
||||
const rainAudio = document.getElementById('rainAudio');
|
||||
let soundOn = false;
|
||||
soundBtn.addEventListener('click', () => {
|
||||
soundOn = !soundOn;
|
||||
if (soundOn) {
|
||||
rainAudio.play().catch(() => {});
|
||||
soundBtn.textContent = '♪ rain: on';
|
||||
soundBtn.classList.add('active');
|
||||
} else {
|
||||
rainAudio.pause();
|
||||
soundBtn.textContent = '♪ rain: off';
|
||||
soundBtn.classList.remove('active');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user