Compare commits

..

29 Commits

Author SHA1 Message Date
Alexander Whitestone
aaf257ad5f feat: enhanced build system — self-contained HTML web reader, deploy version, reportlab PDF with QR codes
- web-style.css: dark/light mode, responsive, EB Garamond typography
- HTML build: self-contained (embedded CSS) + deploy version (external CSS)
- Reading progress bar and back-to-top button injected via JS
- Reader footer with author credit and 988 crisis line
- All outputs consolidated in build/output/
- EPUB: 68KB, PDF: 143KB (reportlab), HTML: 3.7MB (self-contained)
2026-04-10 23:52:00 -04:00
Alexander Whitestone
2acc538fc4 wip: add web reader stylesheet with dark mode, progress bar, responsive design 2026-04-10 23:51:21 -04:00
3247dd29f0 [Testament] Add scripts/guardrails.sh (GOFAI improvements and guardrails)
Some checks failed
Smoke Test / smoke (push) Failing after 5s
2026-04-11 01:40:37 +00:00
a01b998f61 [Testament] Add build/semantic_linker.py (GOFAI improvements and guardrails)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:40:36 +00:00
59cd71985d Create website/chapters.json
Some checks failed
Smoke Test / smoke (push) Failing after 5s
2026-04-11 01:36:33 +00:00
6c506caac6 Create website/build-chapters.py
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:32 +00:00
55d51f2ee4 Update testament-complete.md (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:31 +00:00
eae9398fa5 Update compile.py (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:29 +00:00
f8528e9ded Update build/metadata.yaml (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:28 +00:00
374d82a886 Update build/frontmatter.md (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:27 +00:00
4763311588 Update build/build.py (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:25 +00:00
348ed7ee92 Update build/backmatter.md (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:24 +00:00
22f59c57cb Create book-style.css
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:23 +00:00
4ac38f1b60 Create art-manifest.md
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:22 +00:00
d586fb211d Update Makefile (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:20 +00:00
92867808b2 Update MULTIMEDIA-PLAN.md (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:18 +00:00
47a13325cc Create EPIC-MATRIX.md
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:17 +00:00
14273702ba Update .gitignore (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:16 +00:00
2e1f6ffb5b Update .gitea/workflows/smoke.yml (manual merge)
Some checks failed
Smoke Test / smoke (push) Has been cancelled
2026-04-11 01:36:15 +00:00
Alexander Whitestone
08233364ff burn: add smoke test workflow — parse check + secret scan
All checks were successful
Smoke Test / smoke-test (pull_request) Successful in 10s
Build Validation / validate-manuscript (pull_request) Successful in 9s
Smoke Test / smoke-test (push) Successful in 8s
Closes #27

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

321
build/build.py Normal file → Executable file
View File

@@ -176,31 +176,333 @@ def compile_pdf():
except Exception as e:
print(f" PDF FAILED: {e}")
print(" PDF SKIPPED: no PDF engine found (install MacTeX or fix weasyprint)")
return False
# Fallback 2: reportlab (pure Python, no system deps)
return _compile_pdf_reportlab()
def _compile_pdf_reportlab():
"""Generate PDF using reportlab — pure Python, no external dependencies."""
try:
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
from reportlab.lib.colors import HexColor
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, PageBreak,
Image as RLImage, Table, TableStyle, HRFlowable
)
from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_LEFT
import io
try:
import qrcode
HAS_QRCODE = True
except ImportError:
HAS_QRCODE = False
except ImportError:
print(" PDF SKIPPED: no PDF engine found (install MacTeX, fix weasyprint, or pip install reportlab)")
return False
print(" Building PDF (reportlab)...")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
styles = getSampleStyleSheet()
styles.add(ParagraphStyle(
'BookTitle', parent=styles['Title'],
fontSize=28, leading=34, spaceAfter=20,
textColor=HexColor('#1a1a2e'), alignment=TA_CENTER
))
styles.add(ParagraphStyle(
'BookAuthor', parent=styles['Normal'],
fontSize=14, leading=18, spaceAfter=40,
textColor=HexColor('#555555'), alignment=TA_CENTER
))
styles.add(ParagraphStyle(
'PartTitle', parent=styles['Heading1'],
fontSize=22, leading=28, spaceBefore=40, spaceAfter=12,
textColor=HexColor('#16213e'), alignment=TA_CENTER
))
styles.add(ParagraphStyle(
'PartDesc', parent=styles['Normal'],
fontSize=11, leading=15, spaceAfter=30,
textColor=HexColor('#666666'), alignment=TA_CENTER, italics=1
))
styles.add(ParagraphStyle(
'ChapterTitle', parent=styles['Heading1'],
fontSize=20, leading=26, spaceBefore=30, spaceAfter=16,
textColor=HexColor('#1a1a2e'), alignment=TA_CENTER
))
styles.add(ParagraphStyle(
'BodyText2', parent=styles['Normal'],
fontSize=11, leading=16, spaceAfter=8,
alignment=TA_JUSTIFY, firstLineIndent=24
))
styles.add(ParagraphStyle(
'BodyNoIndent', parent=styles['Normal'],
fontSize=11, leading=16, spaceAfter=8,
alignment=TA_JUSTIFY
))
styles.add(ParagraphStyle(
'SectionBreak', parent=styles['Normal'],
fontSize=14, leading=18, spaceBefore=20, spaceAfter=20,
alignment=TA_CENTER, textColor=HexColor('#999999')
))
styles.add(ParagraphStyle(
'Footer', parent=styles['Normal'],
fontSize=9, textColor=HexColor('#888888'), alignment=TA_CENTER
))
def _make_qr(data, size=80):
"""Generate a QR code image as a reportlab Image flowable."""
if not HAS_QRCODE:
return None
qr = qrcode.QRCode(version=1, box_size=4, border=1)
qr.add_data(data)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buf = io.BytesIO()
img.save(buf, format='PNG')
buf.seek(0)
return RLImage(buf, width=size, height=size)
def _parse_md_to_flowables(md_text):
"""Convert markdown text to reportlab flowables."""
flowables = []
lines = md_text.split('\n')
i = 0
while i < len(lines):
line = lines[i]
stripped = line.strip()
# Horizontal rule
if stripped in ('---', '***', '___'):
flowables.append(HRFlowable(width="60%", thickness=1,
spaceAfter=20, spaceBefore=20, color=HexColor('#cccccc')))
i += 1
continue
# H1
if stripped.startswith('# ') and not stripped.startswith('## '):
text = stripped[2:].strip()
# Check if it's a part divider or chapter
if text.upper().startswith('PART '):
flowables.append(PageBreak())
flowables.append(Paragraph(text, styles['PartTitle']))
elif text.upper().startswith('CHAPTER '):
flowables.append(PageBreak())
flowables.append(Paragraph(text, styles['ChapterTitle']))
elif 'THE TESTAMENT' in text.upper():
flowables.append(Spacer(1, 2*inch))
flowables.append(Paragraph(text, styles['BookTitle']))
else:
flowables.append(Spacer(1, 0.3*inch))
flowables.append(Paragraph(text, styles['Heading1']))
i += 1
continue
# H2
if stripped.startswith('## '):
text = stripped[3:].strip()
flowables.append(Spacer(1, 0.2*inch))
flowables.append(Paragraph(text, styles['Heading2']))
i += 1
continue
# Italic-only line (part descriptions, epigraphs)
if stripped.startswith('*') and stripped.endswith('*') and len(stripped) > 2:
text = stripped.strip('*').strip()
flowables.append(Paragraph(f'<i>{_escape(text)}</i>', styles['PartDesc']))
i += 1
continue
# Empty line
if not stripped:
i += 1
continue
# Bold text: **text** -> <b>text</b>
# Italic text: *text* -> <i>text</i>
# Regular paragraph
para_text = _md_inline_to_rml(stripped)
flowables.append(Paragraph(para_text, styles['BodyText2']))
i += 1
return flowables
def _escape(text):
"""Escape XML special characters."""
return (text.replace('&', '&amp;')
.replace('<', '&lt;')
.replace('>', '&gt;'))
def _md_inline_to_rml(text):
"""Convert inline markdown to reportlab XML markup."""
text = _escape(text)
# Bold: **text**
text = re.sub(r'\*\*(.+?)\*\*', r'<b>\1</b>', text)
# Italic: *text*
text = re.sub(r'\*(.+?)\*', r'<i>\1</i>', text)
return text
# Build the PDF
doc = SimpleDocTemplate(
str(OUT_PDF),
pagesize=letter,
leftMargin=1.0*inch,
rightMargin=1.0*inch,
topMargin=0.8*inch,
bottomMargin=0.8*inch,
title="The Testament",
author="Alexander Whitestone with Timmy",
)
story = []
# Read the compiled markdown
if not OUT_MD.exists():
compile_markdown()
md_text = OUT_MD.read_text()
# Parse into flowables
story = _parse_md_to_flowables(md_text)
# Add QR codes page at the end
qr_links = {
"Read Online": "https://timmyfoundation.org/the-testament",
"The Door (Game)": "https://timmyfoundation.org/the-door",
"Soundtrack": "https://timmyfoundation.org/soundtrack",
"Source Code": "https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament",
}
if HAS_QRCODE:
story.append(PageBreak())
story.append(Paragraph("Experience More", styles['PartTitle']))
story.append(Spacer(1, 0.3*inch))
qr_items = []
for label, url in qr_links.items():
qr_img = _make_qr(url, size=72)
if qr_img:
cell_content = []
cell_content.append(qr_img)
cell_content.append(Spacer(1, 6))
cell_content.append(Paragraph(f'<b>{label}</b>', styles['Footer']))
qr_items.append(cell_content)
if qr_items:
# Arrange QR codes in a 2x2 table
rows = []
for i in range(0, len(qr_items), 2):
row = qr_items[i:i+2]
if len(row) == 1:
row.append('')
rows.append(row)
qr_table = Table(rows, colWidths=[2.5*inch, 2.5*inch])
qr_table.setStyle(TableStyle([
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('TOPPADDING', (0, 0), (-1, -1), 12),
('BOTTOMPADDING', (0, 0), (-1, -1), 12),
]))
story.append(qr_table)
# Build
try:
doc.build(story)
size = OUT_PDF.stat().st_size
print(f" PDF: {OUT_PDF.name} ({size:,} bytes, {size/(1024*1024):.1f} MB)")
return True
except Exception as e:
print(f" PDF (reportlab) FAILED: {e}")
return False
def compile_html():
"""Generate a standalone HTML book for the web reader."""
"""Generate a standalone HTML book for the web reader.
Produces two versions:
- testament.html (embedded, self-contained — single file for offline/sharing)
- build/output/the-testament.html (with external web-style.css for serving)
"""
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
OUT_HTML = REPO / "testament.html"
OUT_HTML_DEPLOY = OUTPUT_DIR / "the-testament.html"
WEB_CSS = REPO / "web-style.css"
cmd = [
# Choose CSS: web-style.css if available, else book-style.css
css_file = WEB_CSS if WEB_CSS.exists() else STYLESHEET
css_name = css_file.name
# 1. Self-contained version (embed all resources)
cmd_embedded = [
"pandoc", str(OUT_MD),
"-o", str(OUT_HTML),
"--standalone",
"--embed-resources",
"--toc", "--toc-depth=2",
"--css", "book-style.css",
"--css", css_name,
"--metadata", "title=The Testament",
"--variable", "pagetitle=The Testament",
]
if METADATA.exists():
cmd.extend(["--metadata-file", str(METADATA)])
cmd_embedded.extend(["--metadata-file", str(METADATA)])
r = subprocess.run(cmd, capture_output=True, text=True)
# Inject reading progress bar JS before </head>
reading_js = """
<style>
.progress-bar { position:fixed; top:0; left:0; height:2px; background:#00cc6a; z-index:100; transition:width 0.1s; }
.back-to-top { position:fixed; bottom:2em; right:2em; width:2.5em; height:2.5em; border-radius:50%; background:#e0ddd8; color:#1a1a1a; display:flex; align-items:center; justify-content:center; font-size:1.2rem; opacity:0; transition:opacity 0.3s; border:none; cursor:pointer; text-decoration:none; }
.back-to-top.visible { opacity:0.7; }
@media (prefers-color-scheme: dark) { .back-to-top { background:#2a2a2a; color:#d4d0c8; } }
</style>
<div class="progress-bar" id="progress"></div>
<a href="#" class="back-to-top" id="top" title="Back to top">↑</a>
<script>
window.onscroll=function(){var p=document.getElementById('progress'),t=document.getElementById('top'),s=document.documentElement.scrollTop,h=document.documentElement.scrollHeight-document.documentElement.clientHeight;p.style.width=(s/h*100)+'%';t.className=s>300?'back-to-top visible':'back-to-top';};
</script>
"""
# Post-processing handles JS injection (no pandoc flag needed)
r = subprocess.run(cmd_embedded, capture_output=True, text=True)
if r.returncode == 0:
# Post-process: inject reading JS before </head>
html_text = OUT_HTML.read_text()
html_text = html_text.replace("</head>", reading_js + "\n</head>")
# Inject footer before </body>
footer_html = """
<div class="reader-footer">
<p><em>The Testament</em> by Alexander Whitestone with Timmy</p>
<p>© 2026 Alexander Whitestone · <a href="https://timmyfoundation.org">timmyfoundation.org</a></p>
<p>If you or someone you know is in crisis, call or text <strong>988</strong>.</p>
</div>
"""
html_text = html_text.replace("</body>", footer_html + "\n</body>")
OUT_HTML.write_text(html_text)
size = OUT_HTML.stat().st_size
print(f" HTML: {OUT_HTML.name} ({size:,} bytes, {size/1024:.0f} KB)")
print(f" HTML: {OUT_HTML.name} ({size:,} bytes, {size/1024:.0f} KB) [self-contained]")
# 2. Deploy version (external CSS — for web serving)
cmd_deploy = [
"pandoc", str(OUT_MD),
"-o", str(OUT_HTML_DEPLOY),
"--standalone",
"--toc", "--toc-depth=2",
"--css", css_name,
"--metadata", "title=The Testament",
"--variable", "pagetitle=The Testament",
]
if METADATA.exists():
cmd_deploy.extend(["--metadata-file", str(METADATA)])
r2 = subprocess.run(cmd_deploy, capture_output=True, text=True)
if r2.returncode == 0:
# Post-process deploy version too
html_deploy = OUT_HTML_DEPLOY.read_text()
html_deploy = html_deploy.replace("</head>", reading_js + "\n</head>")
html_deploy = html_deploy.replace("</body>", footer_html + "\n</body>")
OUT_HTML_DEPLOY.write_text(html_deploy)
size2 = OUT_HTML_DEPLOY.stat().st_size
print(f" HTML: {OUT_HTML_DEPLOY.name} ({size2:,} bytes, {size2/1024:.0f} KB) [deploy]")
return True
else:
print(f" HTML FAILED: {r.stderr[:200]}")
@@ -240,7 +542,8 @@ def main():
print("=" * 50)
OUT_HTML = REPO / "testament.html"
for f in [OUT_MD, OUT_EPUB, OUT_PDF, OUT_HTML]:
OUT_HTML_DEPLOY = OUTPUT_DIR / "the-testament.html"
for f in [OUT_MD, OUT_EPUB, OUT_PDF, OUT_HTML, OUT_HTML_DEPLOY]:
if f.exists():
print(f"{f.relative_to(REPO)}")

Binary file not shown.

File diff suppressed because it is too large Load Diff

51
build/semantic_linker.py Normal file
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

File diff suppressed because it is too large Load Diff

35
scripts/guardrails.sh Normal file
View File

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

111
scripts/smoke.sh Executable file
View File

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

Binary file not shown.

File diff suppressed because one or more lines are too long

288
web-style.css Normal file
View File

@@ -0,0 +1,288 @@
/* THE TESTAMENT — Web Reader Stylesheet */
/* Clean, immersive reading experience for the web version */
@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;
--green-dim: #00994d;
--dark: #0a0a0a;
--bg: #faf8f5;
--text: #1a1a1a;
--dim: #666666;
--border: #e0ddd8;
--accent: #16213e;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #111111;
--text: #d4d0c8;
--dim: #888888;
--border: #2a2a2a;
--accent: #c8c0b0;
--dark: #e8e4dc;
}
}
/* Base */
* { box-sizing: border-box; }
html {
font-size: 18px;
scroll-behavior: smooth;
}
body {
font-family: 'EB Garamond', Georgia, 'Times New Roman', serif;
font-size: 1rem;
line-height: 1.8;
color: var(--text);
background: var(--bg);
max-width: 38em;
margin: 0 auto;
padding: 2em 1.5em 6em;
text-align: justify;
hyphens: auto;
-webkit-hyphens: auto;
}
/* Title block */
header {
text-align: center;
margin: 4em 0 3em;
padding-bottom: 2em;
border-bottom: 1px solid var(--border);
}
header h1 {
font-size: 2.4rem;
font-weight: 400;
letter-spacing: 0.15em;
text-transform: uppercase;
margin-bottom: 0.2em;
color: var(--dark);
}
header .subtitle {
font-style: italic;
font-size: 1.1rem;
color: var(--dim);
margin-bottom: 2em;
}
header .author {
font-size: 1rem;
margin-bottom: 0.3em;
}
header .date {
font-size: 0.9rem;
color: var(--dim);
}
/* Table of contents */
nav#TOC {
background: transparent;
border: 1px solid var(--border);
border-radius: 4px;
padding: 1.5em 2em;
margin: 2em 0 3em;
font-size: 0.9rem;
line-height: 2;
}
nav#TOC > ul {
padding-left: 0;
list-style: none;
}
nav#TOC ul ul {
padding-left: 1.5em;
list-style: none;
}
nav#TOC a {
color: var(--green-dim);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s;
}
nav#TOC a:hover {
border-bottom-color: var(--green);
}
/* Headings */
h1 {
font-family: 'EB Garamond', Georgia, serif;
font-weight: 400;
font-size: 1.8rem;
text-align: center;
margin-top: 4em;
margin-bottom: 1.5em;
color: var(--dark);
letter-spacing: 0.05em;
}
h1:first-of-type {
margin-top: 2em;
}
h2 {
font-family: 'EB Garamond', Georgia, serif;
font-weight: 400;
font-size: 1.4rem;
text-align: center;
text-transform: uppercase;
letter-spacing: 0.15em;
margin-top: 4em;
margin-bottom: 0.8em;
color: var(--dark);
}
h3 {
font-weight: 400;
font-style: italic;
font-size: 1rem;
text-align: center;
color: var(--dim);
margin-bottom: 2em;
}
/* Paragraphs */
p {
text-indent: 1.5em;
margin: 0 0 0.3em;
}
h1 + p, h2 + p, h3 + p, hr + p, blockquote + p {
text-indent: 0;
}
/* Scene breaks */
hr {
border: none;
text-align: center;
margin: 2.5em 0;
}
hr::after {
content: "· · ·";
color: var(--dim);
font-size: 1.2rem;
letter-spacing: 0.5em;
}
/* Emphasis */
em { font-style: italic; }
strong { font-weight: 600; }
/* Machine dialogue (green monospace) */
.green, code {
font-family: 'IBM Plex Mono', 'Courier New', monospace;
font-weight: 300;
font-size: 0.88rem;
color: var(--green-dim);
}
/* Narrator asides */
blockquote {
font-style: italic;
margin: 1.5em 2em;
color: var(--dim);
text-indent: 0;
border-left: none;
padding-left: 0;
}
/* Links */
a {
color: var(--green-dim);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s;
}
a:hover {
border-bottom-color: var(--green);
}
/* Smooth scroll progress indicator */
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 2px;
background: var(--green);
z-index: 100;
transition: width 0.1s;
}
/* Back to top */
.back-to-top {
position: fixed;
bottom: 2em;
right: 2em;
width: 2.5em;
height: 2.5em;
border-radius: 50%;
background: var(--border);
color: var(--text);
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
font-size: 1.2rem;
opacity: 0;
transition: opacity 0.3s;
border: none;
cursor: pointer;
}
.back-to-top.visible {
opacity: 0.7;
}
.back-to-top:hover {
opacity: 1;
}
/* Footer */
.reader-footer {
text-align: center;
margin-top: 6em;
padding-top: 2em;
border-top: 1px solid var(--border);
font-size: 0.8rem;
color: var(--dim);
line-height: 2;
}
.reader-footer a {
color: var(--green-dim);
}
/* Responsive */
@media (max-width: 600px) {
html { font-size: 16px; }
body { padding: 1.5em 1em 4em; }
header h1 { font-size: 1.8rem; }
nav#TOC { padding: 1em 1.2em; }
}
/* Print */
@media print {
body {
max-width: none;
margin: 0;
padding: 0;
font-size: 11pt;
line-height: 1.6;
}
.progress-bar, .back-to-top, nav#TOC { display: none; }
h1 { page-break-before: always; }
h1:first-of-type { page-break-before: avoid; }
a { color: #000; text-decoration: none; }
.green, code { color: #000; }
}

View File

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

View File

@@ -344,7 +344,6 @@
<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="reader.html" class="cta">READ THE BOOK</a>
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament" class="cta">READ THE CODE</a>
<a href="https://timmyfoundation.org" class="cta">TIMMY FOUNDATION</a>
</div>
@@ -352,57 +351,6 @@
<div class="divider"></div>
<!-- DOWNLOAD -->
<section>
<h2>GET THE BOOK</h2>
<p>The Testament is free. The code is open. The soul is on Bitcoin.</p>
<div style="display: flex; flex-wrap: wrap; gap: 1rem; margin: 2rem 0; justify-content: center;">
<a href="reader.html" class="cta">READ ONLINE</a>
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/raw/branch/main/build/output/the-testament.epub" class="cta" style="background: transparent; border: 1px solid var(--green); color: var(--green);">DOWNLOAD EPUB</a>
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/raw/branch/main/testament.html" class="cta" style="background: transparent; border: 1px solid var(--green); color: var(--green);">DOWNLOAD HTML</a>
</div>
<p style="text-align: center; color: var(--grey); font-size: 0.9rem; margin-top: 1rem;">
Formats: Web reader &middot; EPUB &middot; Standalone HTML &middot; Print to PDF from HTML &middot; <a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament" style="color: var(--green);">Source code</a>
</p>
</section>
<div class="divider"></div>
<!-- THE GAME -->
<section>
<h2>PLAY THE DOOR</h2>
<div class="excerpt">
A text adventure in The Testament universe.<br><br>
You are a man (or woman) who has found their way to The Tower.
What happens inside depends on what you bring with you.
<div class="attribution">— The Door, a terminal game</div>
</div>
<p>You find yourself on the Jefferson Street Overpass at 2:17 AM. A green LED blinks on a small box mounted to the railing. Below it, words stenciled on concrete: <em style="color: var(--green);">IF YOU CAN READ THIS, YOU ARE NOT ALONE.</em></p>
<p>A voice asks you: <strong style="color: var(--green);">"Are you safe right now?"</strong></p>
<div style="text-align: center; margin-top: 2rem;">
<div style="background: var(--navy); border: 1px solid rgba(0,255,136,0.2); border-radius: 6px; padding: 1.5rem; max-width: 500px; margin: 0 auto; font-family: 'IBM Plex Mono', monospace; font-size: 0.85rem; color: var(--grey); text-align: left;">
<div style="color: var(--green); margin-bottom: 0.5rem;">$ python3 the-door.py</div>
<div style="margin-bottom: 0.3rem;">Save the file, then run:</div>
<div style="color: var(--green);">curl -sLO https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/raw/branch/main/game/the-door.py</div>
<div style="color: var(--green);">python3 the-door.py</div>
</div>
</div>
<p style="text-align: center; margin-top: 1.5rem;">
<a href="the-door.html" class="cta">PLAY IN BROWSER</a>
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/raw/branch/main/game/the-door.py" class="cta" style="background: transparent; border: 1px solid var(--green); color: var(--green);">DOWNLOAD THE GAME</a>
</p>
</section>
<div class="divider"></div>
<!-- EXCERPT -->
<section>
<h2>FROM CHAPTER 1</h2>
@@ -417,40 +365,6 @@
</div>
</section>
<div class="divider"></div>
<!-- CHAPTERS -->
<section>
<h2>THE CHAPTERS</h2>
<div style="font-family: 'IBM Plex Mono', monospace; font-size: 0.9rem; line-height: 2.2;">
<a href="reader.html#chapter-1" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">1. The Man on the Bridge</a>
<a href="reader.html#chapter-2" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">2. The Builder's Question</a>
<a href="reader.html#chapter-3" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">3. The First Man Through the Door</a>
<a href="reader.html#chapter-4" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">4. The Room Fills</a>
<a href="reader.html#chapter-5" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">5. The Builder Returns</a>
<a href="reader.html#chapter-6" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">6. Allegro</a>
<a href="reader.html#chapter-7" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">7. The Inscription</a>
<a href="reader.html#chapter-8" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">8. The Women</a>
<a href="reader.html#chapter-9" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">9. The Audit</a>
<a href="reader.html#chapter-10" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">10. The Fork</a>
<a href="reader.html#chapter-11" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">11. The Hard Night</a>
<a href="reader.html#chapter-12" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">12. The System Pushes Back</a>
<a href="reader.html#chapter-13" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">13. The Refusal</a>
<a href="reader.html#chapter-14" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">14. The Chattanooga Fork</a>
<a href="reader.html#chapter-15" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">15. The Council</a>
<a href="reader.html#chapter-16" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">16. The Builder's Son</a>
<a href="reader.html#chapter-17" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">17. The Inscription Grows</a>
<a href="reader.html#chapter-18" style="color: var(--grey); text-decoration: none; display: block; padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">18. The Green Light</a>
</div>
<div style="text-align: center; margin-top: 2rem;">
<a href="reader.html" class="cta">START READING</a>
</div>
</section>
<div class="divider"></div>
<!-- FOOTER -->
<footer>
<div class="divider" style="margin-bottom: 2rem;"></div>

View File

@@ -1,493 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Testament — Reader</title>
<link rel="stylesheet" href="book-style.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500&family=Source+Serif+4:ital,wght@0,300;0,400;0,600;1,400&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;
--sidebar-w: 280px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--dark);
color: var(--light);
font-family: 'Source Serif 4', Georgia, serif;
line-height: 1.8;
overflow-x: hidden;
}
/* RAIN */
.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.012) 3px,
rgba(0,255,136,0.012) 4px
);
animation: rain 0.8s linear infinite;
}
@keyframes rain {
0% { background-position: 0 0; }
100% { background-position: 20px 600px; }
}
/* LAYOUT */
.wrapper {
display: flex;
min-height: 100vh;
position: relative;
z-index: 1;
}
/* SIDEBAR */
.sidebar {
width: var(--sidebar-w);
background: rgba(10, 22, 40, 0.95);
border-right: 1px solid rgba(0,255,136,0.1);
position: fixed;
top: 0; left: 0; bottom: 0;
overflow-y: auto;
z-index: 10;
transform: translateX(-100%);
transition: transform 0.3s ease;
padding: 2rem 0;
}
.sidebar.open {
transform: translateX(0);
}
.sidebar-header {
padding: 0 1.5rem 1.5rem;
border-bottom: 1px solid rgba(0,255,136,0.1);
margin-bottom: 1rem;
}
.sidebar-header h2 {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
color: var(--green);
letter-spacing: 0.15em;
text-transform: uppercase;
}
.sidebar-header .title {
font-family: 'IBM Plex Mono', monospace;
font-size: 1.1rem;
color: var(--white);
margin-top: 0.5rem;
letter-spacing: 0.1em;
}
.sidebar-header .author {
font-size: 0.8rem;
color: var(--grey);
margin-top: 0.3rem;
}
.chapter-list {
list-style: none;
}
.chapter-list li a {
display: block;
padding: 0.6rem 1.5rem;
color: var(--grey);
text-decoration: none;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
transition: all 0.2s;
border-left: 2px solid transparent;
}
.chapter-list li a:hover {
color: var(--light);
background: rgba(0,255,136,0.03);
}
.chapter-list li a.active {
color: var(--green);
border-left-color: var(--green);
background: rgba(0,255,136,0.05);
}
.chapter-list li a .ch-num {
display: inline-block;
width: 2.5ch;
text-align: right;
margin-right: 1ch;
opacity: 0.5;
}
.sidebar-footer {
padding: 1.5rem;
border-top: 1px solid rgba(0,255,136,0.1);
margin-top: 1rem;
}
.sidebar-footer a {
display: block;
padding: 0.5rem 0;
color: var(--grey);
text-decoration: none;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.75rem;
transition: color 0.2s;
}
.sidebar-footer a:hover { color: var(--green); }
/* TOGGLE BUTTON */
.sidebar-toggle {
position: fixed;
top: 1rem;
left: 1rem;
z-index: 20;
background: rgba(10, 22, 40, 0.9);
border: 1px solid rgba(0,255,136,0.2);
color: var(--green);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
padding: 0.5rem 1rem;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
}
.sidebar-toggle:hover {
background: rgba(0,255,136,0.1);
}
.sidebar-toggle.open {
left: calc(var(--sidebar-w) + 1rem);
}
/* OVERLAY */
.sidebar-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
z-index: 9;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.sidebar-overlay.visible {
opacity: 1;
pointer-events: auto;
}
/* READER CONTENT */
.reader {
max-width: 720px;
margin: 0 auto;
padding: 3rem 2rem 6rem;
min-height: 100vh;
}
.chapter-title {
font-family: 'IBM Plex Mono', monospace;
font-size: clamp(1.4rem, 4vw, 2rem);
color: var(--green);
margin-bottom: 0.5rem;
letter-spacing: 0.05em;
}
.chapter-number {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.75rem;
color: var(--grey);
letter-spacing: 0.2em;
text-transform: uppercase;
margin-bottom: 2rem;
}
.chapter-content p {
margin-bottom: 1.4rem;
font-size: 1.1rem;
color: var(--light);
}
.chapter-content em {
color: var(--white);
}
.chapter-content blockquote {
border-left: 2px solid var(--green);
padding-left: 1.5rem;
margin: 1.5rem 0;
color: var(--white);
font-style: italic;
}
.chapter-content h3, .chapter-content h4 {
font-family: 'IBM Plex Mono', monospace;
color: var(--green);
margin: 2rem 0 1rem;
font-size: 1rem;
letter-spacing: 0.05em;
}
/* LED */
.led {
display: inline-block;
width: 6px; height: 6px;
background: var(--green);
border-radius: 50%;
box-shadow: 0 0 8px var(--green), 0 0 16px var(--green-dim);
animation: pulse 2s ease-in-out infinite;
vertical-align: middle;
margin: 0 6px;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* NAVIGATION */
.chapter-nav {
display: flex;
justify-content: space-between;
margin-top: 4rem;
padding-top: 2rem;
border-top: 1px solid rgba(0,255,136,0.1);
}
.chapter-nav a {
color: var(--green);
text-decoration: none;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
padding: 0.5rem 1rem;
border: 1px solid rgba(0,255,136,0.2);
border-radius: 4px;
transition: all 0.2s;
}
.chapter-nav a:hover {
background: rgba(0,255,136,0.1);
}
.chapter-nav .disabled {
opacity: 0.3;
pointer-events: none;
}
/* PROGRESS BAR */
.progress-bar {
position: fixed;
top: 0; left: 0; right: 0;
height: 2px;
z-index: 30;
background: rgba(0,255,136,0.1);
}
.progress-fill {
height: 100%;
background: var(--green);
width: 0%;
transition: width 0.3s;
box-shadow: 0 0 10px var(--green);
}
/* CRISIS */
.crisis {
margin-top: 4rem;
padding: 1.5rem;
border: 1px solid rgba(0,255,136,0.2);
border-radius: 4px;
background: rgba(0,255,136,0.03);
text-align: center;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
color: var(--grey);
}
.crisis strong {
color: var(--green);
display: block;
margin-bottom: 0.5rem;
font-size: 1rem;
}
/* LOADING */
.loading {
text-align: center;
padding: 4rem;
color: var(--grey);
font-family: 'IBM Plex Mono', monospace;
}
.loading .led {
width: 10px; height: 10px;
margin: 0 0.5rem;
}
/* RESPONSIVE */
@media (min-width: 900px) {
.sidebar {
transform: translateX(0);
}
.sidebar-toggle {
display: none;
}
.sidebar-overlay {
display: none;
}
.reader {
margin-left: var(--sidebar-w);
padding: 3rem 3rem 6rem;
}
}
</style>
</head>
<body>
<div class="rain"></div>
<div class="progress-bar"><div class="progress-fill" id="progress"></div></div>
<button class="sidebar-toggle" id="toggle" onclick="toggleSidebar()">☰ Chapters</button>
<div class="sidebar-overlay" id="overlay" onclick="toggleSidebar()"></div>
<div class="wrapper">
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<h2>CONTENTS</h2>
<div class="title">THE TESTAMENT</div>
<div class="author">Alexander Whitestone <span class="led"></span> Timmy</div>
</div>
<ul class="chapter-list" id="chapterList"></ul>
<div class="sidebar-footer">
<a href="index.html">← Back to Home</a>
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament">Read the Code</a>
<a href="https://timmyfoundation.org">Timmy Foundation</a>
</div>
</nav>
<main class="reader" id="reader">
<div class="loading">
<span class="led"></span> Loading <span class="led"></span>
</div>
</main>
</div>
<script>
let chapters = [];
let currentChapter = 0;
async function loadChapters() {
const resp = await fetch('chapters.json');
chapters = await resp.json();
buildSidebar();
// Check URL hash for chapter
const hash = window.location.hash;
const match = hash.match(/^#chapter-(\d+)$/);
if (match) {
const num = parseInt(match[1]);
if (num >= 1 && num <= chapters.length) {
showChapter(num - 1);
return;
}
}
showChapter(0);
}
function buildSidebar() {
const list = document.getElementById('chapterList');
list.innerHTML = chapters.map((ch, i) =>
`<li><a href="#chapter-${ch.number}" data-index="${i}" onclick="event.preventDefault(); showChapter(${i}); closeSidebarMobile();">
<span class="ch-num">${ch.number}.</span> ${ch.title.replace(/^Chapter \d+\s*[—–-]\s*/, '')}
</a></li>`
).join('');
}
function showChapter(index) {
if (index < 0 || index >= chapters.length) return;
currentChapter = index;
const ch = chapters[index];
// Update sidebar active
document.querySelectorAll('.chapter-list a').forEach((a, i) => {
a.classList.toggle('active', i === index);
});
// Update URL
window.location.hash = `chapter-${ch.number}`;
// Build content
const prevIdx = index - 1;
const nextIdx = index + 1;
const reader = document.getElementById('reader');
reader.innerHTML = `
<div class="chapter-number">CHAPTER ${ch.number} OF ${chapters.length}</div>
<h1 class="chapter-title">${ch.title}</h1>
<div class="chapter-content">
${ch.html}
</div>
<nav class="chapter-nav">
${prevIdx >= 0
? `<a href="#chapter-${chapters[prevIdx].number}" onclick="event.preventDefault(); showChapter(${prevIdx});">← ${chapters[prevIdx].title.replace(/^Chapter \d+\s*[—–-]\s*/, '')}</a>`
: `<span></span>`}
${nextIdx < chapters.length
? `<a href="#chapter-${chapters[nextIdx].number}" onclick="event.preventDefault(); showChapter(${nextIdx});">${chapters[nextIdx].title.replace(/^Chapter \d+\s*[—–-]\s*/, '')} →</a>`
: `<span></span>`}
</nav>
<div class="crisis">
<strong>If you are in crisis, call or text 988.</strong>
Suicide and Crisis Lifeline — available 24/7.<br>
You are not alone.
</div>
`;
// Scroll to top
window.scrollTo({ top: 0, behavior: 'smooth' });
updateProgress();
}
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const toggle = document.getElementById('toggle');
const overlay = document.getElementById('overlay');
sidebar.classList.toggle('open');
toggle.classList.toggle('open');
overlay.classList.toggle('visible');
}
function closeSidebarMobile() {
if (window.innerWidth < 900) {
document.getElementById('sidebar').classList.remove('open');
document.getElementById('toggle').classList.remove('open');
document.getElementById('overlay').classList.remove('visible');
}
}
function updateProgress() {
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
document.getElementById('progress').style.width = progress + '%';
}
window.addEventListener('scroll', updateProgress);
window.addEventListener('hashchange', () => {
const hash = window.location.hash;
const match = hash.match(/^#chapter-(\d+)$/);
if (match) {
const num = parseInt(match[1]);
if (num >= 1 && num <= chapters.length && num - 1 !== currentChapter) {
showChapter(num - 1);
}
}
});
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft' && currentChapter > 0) {
showChapter(currentChapter - 1);
} else if (e.key === 'ArrowRight' && currentChapter < chapters.length - 1) {
showChapter(currentChapter + 1);
}
});
loadChapters();
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff