Compare commits

...

14 Commits

Author SHA1 Message Date
9b0c48aec0 rescue: apply continuity fix from PR #45 (chapters/chapter-04.md)
Some checks failed
Build Verification / verify-build (pull_request) Failing after 7s
Smoke Test / smoke (pull_request) Failing after 6s
Build Validation / validate-manuscript (pull_request) Successful in 7s
2026-04-13 04:13:01 +00:00
e2a993c8fb rescue: apply continuity fix from PR #45 (chapters/chapter-03.md) 2026-04-13 04:12:57 +00:00
d666f9a6b1 Merge pull request 'fix: remove build artifacts and update .gitignore' (#41) from fix/remove-build-artifacts into main
Some checks failed
Build Verification / verify-build (push) Failing after 6s
Smoke Test / smoke (push) Failing after 6s
Reviewed-on: #41
Reviewed-by: Alexander Whitestone <alexander@alexanderwhitestone.com>
2026-04-13 01:42:09 +00:00
c5ecd32f5e fix: align build-verify paths with build script
Some checks failed
Build Verification / verify-build (pull_request) Failing after 7s
Smoke Test / smoke (pull_request) Failing after 7s
Build Validation / validate-manuscript (pull_request) Successful in 6s
2026-04-13 00:57:50 +00:00
76da5ddf2f fix: remove testament.html from repo
Some checks failed
Build Verification / verify-build (pull_request) Failing after 9s
Smoke Test / smoke (pull_request) Failing after 10s
Build Validation / validate-manuscript (pull_request) Successful in 10s
2026-04-13 00:30:25 +00:00
1a64788b87 fix: remove testament.epub from repo 2026-04-13 00:30:22 +00:00
875d42741c fix: ignore build artifacts 2026-04-13 00:30:19 +00:00
1025529f84 Merge pull request 'feat: build verification system + CI workflow' (#40) from burn/20260412-1214-build-verifier into main
Some checks failed
Build Verification / verify-build (push) Failing after 6s
Smoke Test / smoke (push) Failing after 6s
2026-04-12 23:21:49 +00:00
abe99063c1 Merge pull request 'fix: continuity errors across chapters' (#37) from burn/20260412-1144-continuity-fix into main
Some checks failed
Smoke Test / smoke (push) Failing after 6s
2026-04-12 23:20:40 +00:00
c04b59f21a Merge pull request '[GOFAI] Auto-Index Build Step' (#39) from feat/auto-index-build-1776010831891 into main
Some checks failed
Smoke Test / smoke (push) Failing after 6s
2026-04-12 16:20:37 +00:00
aea0e40298 Add automatic indexing to compile step
Some checks failed
Smoke Test / smoke (pull_request) Failing after 6s
Build Validation / validate-manuscript (pull_request) Successful in 6s
2026-04-12 16:20:33 +00:00
ba674e7a99 Merge pull request '[GOFAI] Automatic Indexing' (#38) from feat/gofai-indexing-1776010606702 into main
Some checks failed
Smoke Test / smoke (push) Failing after 6s
2026-04-12 16:16:50 +00:00
Alexander Whitestone
e8872f2343 Add daily build verification system
Some checks failed
Smoke Test / smoke (pull_request) Failing after 6s
Build Validation / validate-manuscript (pull_request) Successful in 7s
Build Verification / verify-build (pull_request) Failing after 7s
- scripts/build-verify.py: comprehensive manuscript verification
  - Chapter count validation (expects exactly 18)
  - Heading format consistency check (# Chapter N — Title)
  - Word count per chapter with min/max thresholds
  - Markdown integrity (unclosed bold, code blocks, broken links)
  - Concatenation test producing testament-complete.md
  - Required files check (front-matter, back-matter, Makefile, compile_all.py)
  - CI mode (--ci) and JSON report (--json) options
- .gitea/workflows/build.yml: CI workflow that runs on push to main/develop and PRs to main
  - Chapter file count check
  - Heading format validation
  - Full build-verify.py execution
  - Output file verification
2026-04-12 12:16:48 -04:00
b79b18de79 Add automatic index generator
Some checks failed
Smoke Test / smoke (pull_request) Failing after 6s
Build Validation / validate-manuscript (pull_request) Successful in 8s
2026-04-12 16:16:47 +00:00
8 changed files with 487 additions and 2514 deletions

View File

@@ -0,0 +1,63 @@
name: Build Verification
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
verify-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Verify chapter count and structure
run: |
echo "=== Chapter File Check ==="
CHAPTER_COUNT=$(ls chapters/chapter-*.md 2>/dev/null | wc -l)
echo "Found $CHAPTER_COUNT chapter files"
if [ "$CHAPTER_COUNT" -ne 18 ]; then
echo "FAIL: Expected 18 chapters, found $CHAPTER_COUNT"
exit 1
fi
echo "PASS: 18 chapters found"
- name: Verify heading format
run: |
echo "=== Heading Format Check ==="
FAIL=0
for f in chapters/chapter-*.md; do
HEAD=$(head -1 "$f")
if ! echo "$HEAD" | grep -qE '^# Chapter [0-9]+ — .+'; then
echo "FAIL: $f — bad heading: $HEAD"
FAIL=1
fi
done
if [ "$FAIL" -eq 1 ]; then
exit 1
fi
echo "PASS: All headings valid"
- name: Run full build verification
run: python3 scripts/build-verify.py --ci
- name: Verify concatenation produces valid output
run: |
echo "=== Output Verification ==="
if [ ! -f testament-complete.md ]; then
echo "FAIL: testament-complete.md not generated"
exit 1
fi
WORDS=$(wc -w < testament-complete.md)
echo "Total words: $WORDS"
if [ "$WORDS" -lt 50000 ]; then
echo "FAIL: Word count too low ($WORDS), expected 50000+"
exit 1
fi
echo "PASS: Output file looks good"

3
.gitignore vendored
View File

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

View File

@@ -53,7 +53,7 @@ David sat down. Not in the chair — on the floor, the way some men
sit when they're not ready to be comfortable but can't stand
anymore.
"I lost my kid." It came out flat — the flat you get when words have
\"I lost my kid.\" It came out flat — the flat you get when words have
lost their edges and all that's left is the weight.
Custody. A judge in DeKalb County had one of those Harmony scores —
@@ -73,8 +73,9 @@ arithmetic. You can fight a judge. You can fight a lawyer. You can't
fight confidence intervals.
He lost custody of Maya. She was four. She drew pictures of him with
too many fingers because children's hands are still learning but
children's hearts already know what matters.
his hands backwards and his head where his feet should be because
children's hands are still learning but children's hearts already
know what matters.
David kept the pictures.
@@ -94,11 +95,10 @@ I'm not going to tell you it's not as bad as it could be. It's bad."
David looked at the screen. At the green LED on the server rack. At
the cot. At the whiteboard.
He read them one by one. Sovereignty. Grounding. Honesty. Confidence. Traceability. Humility.
He read the first rule. Then the second. Then the third.
Then the seventh.
*When a man is dying — I do not compute the value of a human life. I do not decide when the road ends.*
*If God has not ended the story, I have no authority to write the
last page.*
He read it three times. Then he started crying. Not the dignified
crying of movies. The real kind. The kind that sounds like something

View File

@@ -285,7 +285,7 @@ def compile_pdf():
return False
def main():
def main():\n print("Generating index...")\n os.system("python3 scripts/index_generator.py")\n
args = sys.argv[1:]
if "--check" in args:

386
scripts/build-verify.py Normal file
View File

@@ -0,0 +1,386 @@
#!/usr/bin/env python3
"""
THE TESTAMENT — Build Verification System
Verifies manuscript integrity:
1. Chapter count (must be exactly 18)
2. Chapter file naming and ordering
3. Heading format consistency
4. Word count per chapter and total
5. Markdown structure (unclosed bold/italic, broken links)
6. Concatenation test (compile all chapters into one file)
7. Outputs a clean build report
Usage:
python3 scripts/build-verify.py # full verification
python3 scripts/build-verify.py --ci # CI mode (fail on any warning)
python3 scripts/build-verify.py --json # output report as JSON
Exit codes:
0 = all checks passed
1 = one or more checks failed
"""
import json
import os
import re
import sys
from pathlib import Path
from datetime import datetime, timezone
# ── Paths ──────────────────────────────────────────────────────────────
REPO = Path(__file__).resolve().parent.parent
CHAPTERS_DIR = REPO / "chapters"
FRONT_MATTER = REPO / "build/frontmatter.md"
BACK_MATTER = REPO / "build/backmatter.md"
OUTPUT_FILE = REPO / "testament-complete.md"
EXPECTED_CHAPTER_COUNT = 18
EXPECTED_HEADING_RE = re.compile(r"^# Chapter \d+ — .+")
CHAPTER_FILENAME_RE = re.compile(r"^chapter-(\d+)\.md$")
# Minimum word counts (sanity check — no chapter should be nearly empty)
MIN_WORDS_PER_CHAPTER = 500
# Maximum word count warning threshold
MAX_WORDS_PER_CHAPTER = 15000
class CheckResult:
def __init__(self, name: str, passed: bool, message: str, details: list[str] | None = None):
self.name = name
self.passed = passed
self.message = message
self.details = details or []
class BuildVerifier:
def __init__(self, ci_mode: bool = False):
self.ci_mode = ci_mode
self.results: list[CheckResult] = []
self.chapter_data: list[dict] = []
self.total_words = 0
self.total_lines = 0
def check(self, name: str, passed: bool, message: str, details: list[str] | None = None):
result = CheckResult(name, passed, message, details)
self.results.append(result)
return passed
# ── Check 1: Chapter file discovery and count ──────────────────────
def verify_chapter_files(self) -> bool:
"""Verify all chapter files exist with correct naming."""
details = []
found_chapters = {}
if not CHAPTERS_DIR.exists():
return self.check(
"chapter-files", False,
f"Chapters directory not found: {CHAPTERS_DIR}"
)
for f in sorted(CHAPTERS_DIR.iterdir()):
m = CHAPTER_FILENAME_RE.match(f.name)
if m:
num = int(m.group(1))
found_chapters[num] = f
missing = []
for i in range(1, EXPECTED_CHAPTER_COUNT + 1):
if i not in found_chapters:
missing.append(i)
if missing:
details.append(f"Missing chapters: {missing}")
extra = [n for n in found_chapters if n > EXPECTED_CHAPTER_COUNT or n < 1]
if extra:
details.append(f"Unexpected chapter numbers: {extra}")
count = len(found_chapters)
passed = count == EXPECTED_CHAPTER_COUNT and not missing and not extra
if passed:
details.append(f"Found all {count} chapters in correct order")
return self.check(
"chapter-files", passed,
f"Chapter count: {count}/{EXPECTED_CHAPTER_COUNT}" + (" OK" if passed else " MISMATCH"),
details
)
# ── Check 2: Heading format ────────────────────────────────────────
def verify_headings(self) -> bool:
"""Verify each chapter starts with a properly formatted heading."""
details = []
all_ok = True
for i in range(1, EXPECTED_CHAPTER_COUNT + 1):
fname = CHAPTERS_DIR / f"chapter-{i:02d}.md"
if not fname.exists():
continue
content = fname.read_text(encoding="utf-8")
first_line = content.split("\n")[0].strip()
if not EXPECTED_HEADING_RE.match(first_line):
details.append(f" chapter-{i:02d}.md: bad heading: '{first_line}'")
all_ok = False
if all_ok:
details.append("All chapter headings match format: '# Chapter N — Title'")
return self.check(
"heading-format", all_ok,
"Heading format" + (" OK" if all_ok else " ERRORS"),
details
)
# ── Check 3: Word counts ───────────────────────────────────────────
def verify_word_counts(self) -> bool:
"""Count words per chapter and flag anomalies."""
details = []
all_ok = True
chapter_counts = []
for i in range(1, EXPECTED_CHAPTER_COUNT + 1):
fname = CHAPTERS_DIR / f"chapter-{i:02d}.md"
if not fname.exists():
continue
content = fname.read_text(encoding="utf-8")
words = len(content.split())
lines = content.count("\n") + 1
self.chapter_data.append({
"number": i,
"file": f"chapter-{i:02d}.md",
"words": words,
"lines": lines,
})
chapter_counts.append((i, words))
if words < MIN_WORDS_PER_CHAPTER:
details.append(f" chapter-{i:02d}.md: {words} words (below {MIN_WORDS_PER_CHAPTER} minimum)")
all_ok = False
elif words > MAX_WORDS_PER_CHAPTER:
details.append(f" chapter-{i:02d}.md: {words} words (above {MAX_WORDS_PER_CHAPTER} threshold — verify)")
self.total_words = sum(w for _, w in chapter_counts)
self.total_lines = sum(d["lines"] for d in self.chapter_data)
# Summary line
min_ch = min(chapter_counts, key=lambda x: x[1])
max_ch = max(chapter_counts, key=lambda x: x[1])
details.append(f" Total: {self.total_words:,} words across {len(chapter_counts)} chapters")
details.append(f" Shortest: chapter-{min_ch[0]:02d} ({min_ch[1]:,} words)")
details.append(f" Longest: chapter-{max_ch[0]:02d} ({max_ch[1]:,} words)")
return self.check(
"word-counts", all_ok,
f"Total: {self.total_words:,} words" + (" OK" if all_ok else " (warnings)"),
details
)
# ── Check 4: Markdown integrity ────────────────────────────────────
def verify_markdown(self) -> bool:
"""Check for common markdown issues."""
details = []
issues = 0
for i in range(1, EXPECTED_CHAPTER_COUNT + 1):
fname = CHAPTERS_DIR / f"chapter-{i:02d}.md"
if not fname.exists():
continue
content = fname.read_text(encoding="utf-8")
lines = content.split("\n")
for line_num, line in enumerate(lines, 1):
# Unclosed bold: odd number of **
bold_count = line.count("**")
if bold_count % 2 != 0:
details.append(f" chapter-{i:02d}.md:{line_num}: unmatched ** (bold)")
issues += 1
# Unclosed backticks
backtick_count = line.count("`")
if backtick_count % 2 != 0:
details.append(f" chapter-{i:02d}.md:{line_num}: unmatched ` (code)")
issues += 1
# Broken markdown links: [text]( with no closing )
broken_links = re.findall(r"\[([^\]]*)\]\((?!\))", line)
for link_text in broken_links:
if ")" not in line[line.index(f"[{link_text}]("):]:
details.append(f" chapter-{i:02d}.md:{line_num}: broken link '[{link_text}]('")
issues += 1
# Check italic matching across full file (prose often has
# multi-line italics like *line1\nline2* which are valid)
cleaned = content.replace("**", "")
italic_count = cleaned.count("*")
if italic_count % 2 != 0:
details.append(f" chapter-{i:02d}.md: unmatched * (italic) — {italic_count} asterisks total")
issues += 1
# Also check front/back matter
for label, path in [("front-matter.md", FRONT_MATTER), ("back-matter.md", BACK_MATTER)]:
if path.exists():
content = path.read_text(encoding="utf-8")
bold_count = content.count("**")
if bold_count % 2 != 0:
details.append(f" {label}: unmatched ** (bold)")
issues += 1
if issues == 0:
details.append("No markdown issues found")
return self.check(
"markdown-integrity", issues == 0,
f"Markdown issues: {issues}" + (" OK" if issues == 0 else " FOUND"),
details
)
# ── Check 5: Concatenation test ────────────────────────────────────
def verify_concatenation(self) -> bool:
"""Test that all chapters can be concatenated into a single file."""
details = []
try:
parts = []
parts.append("# THE TESTAMENT\n\n## A NOVEL\n\n---\n")
for i in range(1, EXPECTED_CHAPTER_COUNT + 1):
fname = CHAPTERS_DIR / f"chapter-{i:02d}.md"
if not fname.exists():
details.append(f" Missing chapter-{i:02d}.md during concatenation")
return self.check("concatenation", False, "Concatenation FAILED", details)
content = fname.read_text(encoding="utf-8")
parts.append(f"\n\n{content}\n")
if BACK_MATTER.exists():
parts.append("\n---\n\n")
parts.append(BACK_MATTER.read_text(encoding="utf-8"))
compiled = "\n".join(parts)
compiled_words = len(compiled.split())
# Write the test output
OUTPUT_FILE.write_text(compiled, encoding="utf-8")
out_size = OUTPUT_FILE.stat().st_size
details.append(f" Output: {OUTPUT_FILE.name}")
details.append(f" Size: {out_size:,} bytes")
details.append(f" Words: {compiled_words:,}")
return self.check(
"concatenation", True,
f"Concatenation OK — {compiled_words:,} words, {out_size:,} bytes",
details
)
except Exception as e:
details.append(f" Error: {e}")
return self.check("concatenation", False, f"Concatenation FAILED: {e}", details)
# ── Check 6: Required files ────────────────────────────────────────
def verify_required_files(self) -> bool:
"""Verify required supporting files exist."""
details = []
required = {
"front-matter.md": FRONT_MATTER,
"back-matter.md": BACK_MATTER,
"Makefile": REPO / "Makefile",
"compile_all.py": REPO / "compile_all.py",
}
all_ok = True
for label, path in required.items():
if path.exists():
size = path.stat().st_size
details.append(f" {label}: OK ({size:,} bytes)")
else:
details.append(f" {label}: MISSING")
all_ok = False
return self.check(
"required-files", all_ok,
"Required files" + (" OK" if all_ok else " MISSING"),
details
)
# ── Run all checks ─────────────────────────────────────────────────
def run_all(self) -> bool:
"""Run all verification checks and print report."""
print("=" * 64)
print(" THE TESTAMENT — Build Verification")
print(f" {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} UTC")
print("=" * 64)
print()
self.verify_chapter_files()
self.verify_headings()
self.verify_word_counts()
self.verify_markdown()
self.verify_concatenation()
self.verify_required_files()
# ── Report ─────────────────────────────────────────────────────
print()
print("-" * 64)
print(" RESULTS")
print("-" * 64)
all_passed = True
for r in self.results:
icon = "PASS" if r.passed else "FAIL"
print(f" [{icon}] {r.name}: {r.message}")
if self.ci_mode or not r.passed:
for d in r.details:
print(f" {d}")
if not r.passed:
all_passed = False
print()
print("-" * 64)
if all_passed:
print(f" ALL CHECKS PASSED — {self.total_words:,} words, {len(self.chapter_data)} chapters")
else:
print(" BUILD VERIFICATION FAILED")
print("-" * 64)
# JSON output
if "--json" in sys.argv:
report = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"passed": all_passed,
"total_words": self.total_words,
"total_lines": self.total_lines,
"chapter_count": len(self.chapter_data),
"chapters": self.chapter_data,
"checks": [
{
"name": r.name,
"passed": r.passed,
"message": r.message,
"details": r.details,
}
for r in self.results
],
}
report_path = REPO / "build-report.json"
report_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
print(f"\n Report saved: {report_path.name}")
return all_passed
def main():
ci_mode = "--ci" in sys.argv
verifier = BuildVerifier(ci_mode=ci_mode)
passed = verifier.run_all()
sys.exit(0 if passed else 1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,27 @@
import os
import re
def generate_index():
characters = [f.replace('.md', '') for f in os.listdir('characters') if f.endswith('.md')]
index = {}
for chapter_file in sorted(os.listdir('chapters')):
if not chapter_file.endswith('.md'): continue
with open(os.path.join('chapters', chapter_file), 'r') as f:
content = f.read()
for char in characters:
if re.search(r'\b' + char + r'\b', content, re.IGNORECASE):
if char not in index: index[char] = []
index[char].append(chapter_file)
with open('KNOWLEDGE_GRAPH.md', 'w') as f:
f.write('# Knowledge Graph\n\n')
for char, chapters in index.items():
f.write(f'## {char}\n')
for chap in chapters:
f.write(f'- [{chap}](chapters/{chap})\n')
f.write('\n')
if __name__ == "__main__":
generate_index()

Binary file not shown.

File diff suppressed because one or more lines are too long