Compare commits
10 Commits
burn/20260
...
burn/20260
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a025de3f6d | ||
|
|
3279f77160 | ||
|
|
6876af48a6 | ||
|
|
9f8df01155 | ||
|
|
a46b2df842 | ||
| 6acb2bf522 | |||
|
|
186eaabaae | ||
| f364c82bac | |||
|
|
332166a901 | ||
|
|
26a5ac46e6 |
15
Makefile
15
Makefile
@@ -1,8 +1,15 @@
|
||||
# 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 pdf epub html md clean check
|
||||
.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:
|
||||
@@ -21,8 +28,8 @@ 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:
|
||||
@which pandoc >/dev/null 2>&1 && echo "✓ pandoc" || echo "✗ pandoc (brew install pandoc)"
|
||||
@which xelatex >/dev/null 2>&1 && echo "✓ xelatex" || echo "✗ xelatex (install MacTeX)"
|
||||
@python3 -c "import weasyprint" 2>/dev/null && echo "✓ weasyprint" || echo "— weasyprint (optional, PDF fallback)"
|
||||
python3 compile_all.py --check
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
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 {qrcode.__version__}")
|
||||
except ImportError:
|
||||
print(f" ❌ qrcode NOT FOUND (pip install qrcode)")
|
||||
|
||||
style = STYLESHEET.exists()
|
||||
print(f" {'✅' if style else '⚠️ '} stylesheet {STYLESHEET if style else 'NOT FOUND (optional)'}")
|
||||
|
||||
cover = COVER_IMAGE.exists()
|
||||
print(f" {'✅' if cover else '⚠️ '} cover art {COVER_IMAGE if cover else 'NOT FOUND (optional)'}")
|
||||
|
||||
|
||||
# ── Clean ──────────────────────────────────────────────────────────────
|
||||
def clean():
|
||||
"""Remove all build artifacts."""
|
||||
artifacts = [OUT_MD, OUT_EPUB, OUT_HTML, OUT_PDF, OUT_JSON, OUT_MANIFEST]
|
||||
# Also clean build/output/
|
||||
for f in OUTPUT_DIR.glob("*"):
|
||||
if f.is_file():
|
||||
artifacts.append(f)
|
||||
|
||||
removed = 0
|
||||
for f in artifacts:
|
||||
if f.exists():
|
||||
f.unlink()
|
||||
removed += 1
|
||||
print(f" 🗑️ {f.relative_to(REPO)}")
|
||||
|
||||
if removed == 0:
|
||||
print(" (nothing to clean)")
|
||||
else:
|
||||
print(f" Removed {removed} files.")
|
||||
|
||||
|
||||
# ── Main ───────────────────────────────────────────────────────────────
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
t0 = time.time()
|
||||
|
||||
if "--check" in args:
|
||||
check_dependencies()
|
||||
return
|
||||
|
||||
if "--clean" in args:
|
||||
print("🧹 Cleaning build artifacts...")
|
||||
clean()
|
||||
return
|
||||
|
||||
do_all = not any(a.startswith("--") for a in args)
|
||||
do_md = "--md" in args or do_all
|
||||
do_epub = "--epub" in args or do_all
|
||||
do_pdf = "--pdf" in args or do_all
|
||||
do_html = "--html" in args or do_all
|
||||
do_json = "--json" in args or do_all
|
||||
|
||||
print("=" * 65)
|
||||
print(" THE TESTAMENT — Unified Compilation Pipeline")
|
||||
print("=" * 65)
|
||||
|
||||
results = {}
|
||||
|
||||
# Step 1: Markdown (always first — others depend on it)
|
||||
if do_md or do_epub or do_pdf or do_html:
|
||||
results["markdown"] = compile_markdown()
|
||||
|
||||
# Step 2: EPUB
|
||||
if do_epub:
|
||||
results["epub"] = compile_epub()
|
||||
|
||||
# Step 3: PDF
|
||||
if do_pdf:
|
||||
results["pdf"] = compile_pdf()
|
||||
|
||||
# Step 4: HTML
|
||||
if do_html:
|
||||
results["html"] = compile_html()
|
||||
|
||||
# Step 5: chapters.json
|
||||
if do_json or do_all:
|
||||
results["chapters_json"] = compile_chapters_json()
|
||||
|
||||
# Step 6: Build manifest
|
||||
if do_all or "--manifest" in args:
|
||||
results["manifest"] = generate_manifest()
|
||||
|
||||
# Summary
|
||||
elapsed = time.time() - t0
|
||||
print(f"\n{'─' * 65}")
|
||||
built = [k for k, v in results.items() if v]
|
||||
failed = [k for k, v in results.items() if not v]
|
||||
if built:
|
||||
print(f" ✅ Built: {', '.join(built)}")
|
||||
if failed:
|
||||
print(f" ❌ Failed: {', '.join(failed)}")
|
||||
print(f" ⏱️ Completed in {elapsed:.1f}s")
|
||||
print(f"{'=' * 65}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -31,8 +31,9 @@ else
|
||||
fi
|
||||
|
||||
# 1c. Verify compiled output exists and is non-empty
|
||||
if [ -s build/the-testament-full.md ]; then
|
||||
WORDS=$(wc -w < build/the-testament-full.md | tr -d ' ')
|
||||
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
|
||||
@@ -87,7 +88,7 @@ SECRET_PATTERNS=(
|
||||
FOUND_SECRETS=false
|
||||
for pattern in "${SECRET_PATTERNS[@]}"; do
|
||||
# Search text files only, skip .git and binary files
|
||||
HITS=$(grep -rn "$pattern" --include="*.md" --include="*.py" --include="*.sh" --include="*.yml" --include="*.yaml" --include="*.json" --include="*.html" --include="*.js" --include="*.css" --include="*.txt" --include="*.cfg" --include="*.ini" --exclude-dir=.git . 2>/dev/null | grep -v "scripts/smoke.sh" || true)
|
||||
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
|
||||
|
||||
BIN
testament.epub
BIN
testament.epub
Binary file not shown.
524
testament.html
524
testament.html
File diff suppressed because one or more lines are too long
@@ -4,6 +4,19 @@
|
||||
<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');
|
||||
|
||||
@@ -19,6 +32,8 @@
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html { scroll-behavior: smooth; }
|
||||
|
||||
body {
|
||||
background: var(--dark);
|
||||
color: var(--light);
|
||||
@@ -27,6 +42,85 @@
|
||||
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;
|
||||
@@ -114,6 +208,19 @@
|
||||
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;
|
||||
@@ -165,6 +272,11 @@
|
||||
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 {
|
||||
@@ -180,6 +292,55 @@
|
||||
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);
|
||||
@@ -215,6 +376,24 @@
|
||||
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;
|
||||
@@ -250,14 +429,47 @@
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- HERO -->
|
||||
<div class="hero">
|
||||
<div class="hero" id="top">
|
||||
<h1>THE TESTAMENT</h1>
|
||||
<div class="subtitle">A Novel</div>
|
||||
<div class="author">By Alexander Whitestone <span class="led"></span> with Timmy</div>
|
||||
@@ -267,10 +479,11 @@
|
||||
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>
|
||||
<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>
|
||||
@@ -298,7 +511,7 @@
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- CHARACTERS -->
|
||||
<section>
|
||||
<section id="characters" class="fade-in">
|
||||
<h2>THE CHARACTERS</h2>
|
||||
|
||||
<div class="characters">
|
||||
@@ -326,13 +539,117 @@
|
||||
<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>
|
||||
<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>
|
||||
@@ -345,14 +662,14 @@
|
||||
|
||||
<div style="text-align: center; margin-top: 2rem;">
|
||||
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament" class="cta">READ THE CODE</a>
|
||||
<a href="https://timmyfoundation.org" class="cta">TIMMY FOUNDATION</a>
|
||||
<a href="https://timmyfoundation.org" class="cta-outline">TIMMY FOUNDATION</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- EXCERPT -->
|
||||
<section>
|
||||
<section class="fade-in">
|
||||
<h2>FROM CHAPTER 1</h2>
|
||||
|
||||
<div class="excerpt">
|
||||
@@ -370,7 +687,13 @@
|
||||
<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></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>
|
||||
@@ -379,5 +702,63 @@
|
||||
</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