Research|Interactive Reader's Editions — Spec 1 Implementation Plan

_meta

Interactive Reader's Editions — Spec 1 Implementation Plan

Last modified 23 September 2026 · view source

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Make a finished corpus-dive renderable as an interactive reader’s edition in the local serve.py preview, and prove it end-to-end on one chapter of The Great Sin (overview + all three versions, with real cut/softening/wikilink marks, toggleable over a clean default).

Architecture: Two phases. Phase 1 is the rendering engine — serve.py gains CriticMarkup, footnotes, and wikilinks rendering, a clean-by-default reader template, and a toggle rail (test-first Python). Phase 2 is a vertical slice of content — the overview distilled from index.md, plus chapter I of The Great Sin in Russian (the canonical spine), 1905 English, and machine English, marked up from the dossier and aligned on shared section anchors (verification-gated, human-present). Remaining chapters and the cross-dive generator are explicit follow-ups, not in this plan.

Tech Stack: Python 3, Python-Markdown (markdown) + pymdown-extensions (CriticMarkup), the existing docs/serve.py, docs/research/lib/verify_quotes.py.

Global Constraints

TDD adaptation. Phase 1 (Python) follows test-first. Phase 2 is research/content production — it has no unit tests; each task ends with an explicit verification gate (mark-stripped fidelity diff, verify_quotes, render-and-inspect) instead. Phase 2 is human-present work in the dive discipline, not unattended subagent output.


Phase 1 — the rendering engine

Task 1: CriticMarkup + footnotes + wikilinks in serve.py

Files: - Modify: docs/serve.py:35 (add a require), docs/serve.py:485-492 (the MD extension list), docs/serve.py:536-537 (extract a render seam) - Test: docs/tests/test_render.py (create)

Interfaces: - Produces: serve.render_body(text: str) -> str — converts a Markdown string (incl. CriticMarkup, footnotes, [[wikilinks]]) to an HTML fragment. Consumed by md_to_html and by Task 2.

# docs/tests/test_render.py
import pathlib, sys
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))  # docs/
import serve

def test_critic_deletion_renders_del():
    html = serve.render_body("a cut here b")
    assert "<del" in html and "cut here" in html

def test_critic_substitution_renders_old_and_new():
    html = serve.render_body("x springs fromis connected with y")
    assert "springs from" in html and "is connected with" in html

def test_critic_comment_renders_note():
    html = serve.render_body("x Chertkov softened this y")
    assert "Chertkov softened this" in html

def test_footnote_renders():
    html = serve.render_body("Body text[^1]\n\n[^1]: the footnote")
    assert "the footnote" in html and ("footnote" in html)

def test_wikilink_renders_link():
    html = serve.render_body("the single tax that [[Henry George]] proposed")
    assert "Henry George" in html and "wikilink" in html

Run: cd docs && python3 -m pytest tests/test_render.py -v Expected: FAIL — AttributeError: module 'serve' has no attribute 'render_body'

After docs/serve.py:35 (require("markdown")), add:

require("pymdownx", "pymdown-extensions")

Replace the MD = markdown.Markdown(...) block at docs/serve.py:485-492 with:

from markdown.extensions.wikilinks import WikiLinkExtension

MD = markdown.Markdown(extensions=[
    TableExtension(),
    FencedCodeExtension(),
    # ponytail: no nl2br — a single newline in wrapped source is a soft wrap, not a
    # <br> (Markdown spec). Intended breaks still work via two trailing spaces.
    "sane_lists",
    "attr_list",
    "footnotes",            # the work's own authorial/translator notes ([^n])
    "pymdownx.critic",      # editorial marks: cut add ab note hi
    WikiLinkExtension(base_url="/wiki/", end_url=".html", html_class="wikilink"),
])

def render_body(text: str) -> str:
    """Convert a Markdown string (CriticMarkup, footnotes, [[wikilinks]]) to an HTML fragment."""
    MD.reset()
    return MD.convert(text)

Then at docs/serve.py:536-537, replace:

    MD.reset()
    body_html = MD.convert(text)

with:

    body_html = render_body(text)

Run: cd docs && python3 -m pytest tests/test_render.py -v Expected: PASS (5 passed). On first run require() pip-installs pymdown-extensions.

git add docs/serve.py docs/tests/test_render.py
git commit -m "feat(serve): render CriticMarkup, footnotes, and wikilinks"

Task 2: clean-by-default reader chrome + CSS

Files: - Modify: docs/serve.py (the CSS string ~docs/serve.py:60-481; the page template in md_to_html ~docs/serve.py:557-578) - Test: docs/tests/test_render.py (extend)

Interfaces: - Consumes: serve.render_body (Task 1). - Produces: serve.reader_chrome(body_html: str) -> str — returns the toggle-rail HTML + script when the page contains editorial marks/wikilinks/footnotes, else "". Consumed by md_to_html.

# append to docs/tests/test_render.py
def test_reader_chrome_present_when_marks_exist():
    chrome = serve.reader_chrome('<p>x <del class="critic">cut</del> y</p>')
    assert "Show cuts" in chrome and "data-layer" in chrome

def test_reader_chrome_absent_for_plain_text():
    assert serve.reader_chrome("<p>just prose</p>") == ""

Run: cd docs && python3 -m pytest tests/test_render.py -k reader_chrome -v Expected: FAIL — module 'serve' has no attribute 'reader_chrome'

Add this function just below render_body in docs/serve.py:

def reader_chrome(body_html: str) -> str:
    """Toggle rail for reader-edition pages; empty for ordinary docs."""
    has_marks = ('class="critic"' in body_html or 'class="wikilink"' in body_html
                 or 'class="footnote' in body_html or 'id="fn' in body_html)
    if not has_marks:
        return ""
    return """
<div id="reader-rail">
  <button data-layer="wikilinks">Wikilinks</button>
  <button data-layer="cuts">Show cuts</button>
  <button data-layer="notes">Notes</button>
  <button data-layer="footnotes">Footnotes</button>
</div>
<script>
(function(){
  var b=document.body;
  document.querySelectorAll('#reader-rail button').forEach(function(btn){
    btn.addEventListener('click',function(){
      var on=b.classList.toggle('show-'+btn.dataset.layer);
      btn.classList.toggle('active',on);
    });
  });
})();
</script>"""

In the md_to_html return template (docs/serve.py:575-577), change:

<main>
{body_html}
</main>

to:

<main class="reader-main">
{body_html}
</main>
{reader_chrome(body_html)}

Append to the CSS string (before its closing """ at docs/serve.py:481):

/* Reader edition — clean by default, layers opt-in */
.reader-main { max-width: 38rem; margin: 0 auto; font-size: 1.12rem; line-height: 1.85; }
.reader-main del.critic { display: none; }                 /* cuts hidden */
body.show-cuts .reader-main del.critic {
  display: inline; background: #FAEEDA; color: #633806;
  border-left: 3px solid #EF9F27; padding: 0 .3em; font-style: italic;
}
.reader-main ins.critic { text-decoration: none; }
body.show-cuts .reader-main ins.critic { background: #E1F5EE; }
.reader-main .critic.comment, .reader-main .critic.subst del { display: none; }
body.show-notes .reader-main .critic.comment { display: inline; color: #854F0B; }
body.show-cuts .reader-main .critic.subst del { display: inline; }
.reader-main a.wikilink { color: inherit; text-decoration: none; border: 0; }
body.show-wikilinks .reader-main a.wikilink { color: #185FA5; border-bottom: 1px solid #85B7EB; }
.reader-main .footnote, .reader-main sup[id^="fnref"] { display: none; }
body.show-footnotes .reader-main .footnote,
body.show-footnotes .reader-main sup[id^="fnref"] { display: revert; }
#reader-rail { position: sticky; bottom: 1rem; display: flex; gap: .5rem;
  justify-content: center; margin-top: 2rem; flex-wrap: wrap; }
#reader-rail button.active { background: #185FA5; color: #fff; }

Note: confirm pymdownx.critic’s emitted class names against the rendered output (open one fixture page); the spec assumes del.critic / ins.critic / .critic.comment. Adjust the selectors to the actual classes if they differ — this is the one place to verify, not assume.

cd docs && python3 -m pytest tests/test_render.py -v        # all pass
printf '%s\n' '---' 'title: Critic smoke' '---' '# Critic smoke' \
  'Poverty springs fromis connected withChertkov softened this this.' \
  'A cut sentence.var. 8 The tax [[Henry George]] proposed.[^1]' '' \
  '[^1]: a footnote.' > /tmp/smoke.md
python3 -c "import serve,pathlib; print('show-cuts' in serve.md_to_html(pathlib.Path('/tmp/smoke.md')))"

Expected: tests pass; the last command prints True (the rail/CSS hooks are present).

git add docs/serve.py docs/tests/test_render.py
git commit -m "feat(serve): clean-by-default reader chrome with opt-in layers"

Phase 2 — The Great Sin vertical slice (chapter I)

Human-present, dive-discipline work. Ground in docs/research/1905-the-great-sin/dossier.yaml and extracts/. No mainstream framing in any prose (governing principle).

Task 3: the overview page, distilled from index.md

Files: - Read: docs/research/1905-the-great-sin/index.md - Create: docs/research/1905-the-great-sin/reader/overview.md


Task 4: the Russian version (the spine), chapter I

Files: - Read: docs/research/1905-the-great-sin/extracts/v36_206_230_Velikij_greh.txt, …/dossier.yaml (evidence rows E15/E16/E17 and the softening row) - Create: docs/research/1905-the-great-sin/reader/the-great-sin.ru.md

Interfaces: - Produces: the shared section-anchor scheme — each chapter heading carries {#sec-N} (intro = {#sec-0}, I = {#sec-1}, … IX = {#sec-9}). Tasks 5 and 6 reuse the exact same anchors.

``bash cd docs && python3 - <<'PY' import re, pathlib raw = pathlib.Path('research/1905-the-great-sin/reader/the-great-sin.ru.md').read_text(encoding='utf-8') raw = raw.split('---',2)[-1] # drop frontmatter raw = re.sub(r'\.*?<<\}','',raw,flags=re.S) # drop notes raw = re.sub(r'\{--(.*?)--\}',r'\1',raw,flags=re.S)# cut text restored raw = re.sub(r'\{\+\+(.*?)\+\+\}',r'\1',raw,flags=re.S) raw = re.sub(r'\{~~(.*?)~>(.*?)~~\}',r'\2',raw,flags=re.S) # substitution -> printed reading raw = re.sub(r'\[\[(.*?)\]\]',r'\1',raw) print("Stripped prose length:", len(raw.strip())) PY ``` Compare the stripped chapter-I prose against the extract's chapter I — the printed-reading text must match the established PSS text word-for-word (the cuts restore the variant text, the substitution resolves to the printed «связаны с»). Eyeball the diff for the chapter. - [ ] **Step 5:** `git add docs/research/1905-the-great-sin/reader/the-great-sin.ru.md && git commit -m "content(great-sin): Russian spine, chapter I, marked"` --- ### Task 5: the 1905 English version, chapter I **Files:** - Create: `docs/research/1905-the-great-sin/reader/the-great-sin.en-1905.md` - Source: "A Great Iniquity" (Tchertkoff & Mayo, 1905) — public domain, on Wikisource (`https://en.wikisource.org/wiki/A_Great_Iniquity`) - [ ] **Step 1:** Fetch the Wikisource text for chapter I; clean it (strip wiki chrome, keep the authorial/translator footnotes as `[^n]`). - [ ] **Step 2:** Frontmatter (`version: en-1905`, `derivedFrom: ru`, no `spine`). Heading `## I {#sec-1}` — the **same anchor** as Task 4. - [ ] **Step 3:** Port the marks into English: the softening as `{~~spring from~>is connected with~~}{>>…, the cuts as …English of the cut passage…var. N… (the 1905 English of the variant — machine-rendered and labelled if no period English of the variant exists), wikilinks as [[Henry George]]. Where the 1905 English dropped something present in the spine, mark it …dropped from the 1905 English. - [ ] Step 4 (gate): renders via serve.md_to_html; the chapter-I anchor {#sec-1} matches Task 4’s; read it beside the Russian to confirm alignment. - [ ] Step 5: git add … && git commit -m "content(great-sin): 1905 English version, chapter I, marked"


Task 6: the machine English version, chapter I

Files: - Create: docs/research/1905-the-great-sin/reader/the-great-sin.en-machine.md


Task 7: wire the slice into the preview and verify end-to-end

Files: - Modify: docs/research/1905-the-great-sin/reader/overview.md (link the three versions)


Self-review (done)

Follow-up plans (not this plan)

  1. Finish The Great Sin — chapters II–IX across the three versions (repeat T4–6).
  2. The cross-dive generator — a companion step (skill or --bundle-text) that emits the version files + marks from a finished dive’s dossier, runnable over the ~14 shipped dives. Build only after Johan reviews this pilot.
  3. Spec 2 — the e-reader website UI (Eleventy): the docked rail, theming, TOC, My Library, reading progress, PWA offline, reader comments, update log. → Now specified and pulled forward into serve.py as a prototype: 2026-07-03-interactive-reader-prototype-design.md (v1 = the reading + annotation UX; the PWA pieces stay deferred to the docs/pwa/ stages).