Six-skill presentation system: ingest talks into a rhetoric vault, run interactive clarification, generate a speaker profile, create presentations that match your documented patterns, produce the deck illustrations + thumbnail visual layer, and publish talk pages to a Jekyll shownotes site. Includes a 102-entry Presentation Patterns taxonomy (91 observable, 11 unobservable go-live items) for scoring, brainstorming, and go-live preparation.
86
92%
Does it follow best practices?
Impact
86%
1.24xAverage score across 26 eval scenarios
Advisory
Suggest reviewing before use
"""Tests for extract-narrative.py and check-rhetorical.py."""
import copy
from pathlib import Path
import pytest
import yaml
FIXTURE = Path(__file__).parent / "fixtures" / "outline-example.yaml"
@pytest.fixture(scope="session")
def outline(outline_schema):
return outline_schema.load_outline(FIXTURE)
@pytest.fixture(scope="session")
def base_data():
return yaml.safe_load(FIXTURE.read_text(encoding="utf-8"))
# ── extract-narrative.py ─────────────────────────────────────────────
def test_narrative_renders_title(extract_narrative, outline):
out = extract_narrative.render(outline)
assert out.startswith("# Demo Talk — Narrative Read")
def test_narrative_includes_chapter_headings(extract_narrative, outline):
out = extract_narrative.render(outline)
assert "### The Setup" in out
assert "### The Turn" in out
assert "### The Close" in out
def test_narrative_omits_image_prompts(extract_narrative, outline):
out = extract_narrative.render(outline)
assert "STYLE ANCHOR" not in out
assert "crumpled paper receipt" not in out
def test_narrative_omits_script_dialogue(extract_narrative, outline):
"""Speaker dialogue lives in script.md, not narrative.md."""
out = extract_narrative.render(outline)
assert "Not for a coffee. Not for a flight." not in out
assert "Doers, write the rule." not in out
def test_narrative_omits_applied_patterns(extract_narrative, outline):
"""Structural taxonomy lives in rhetorical-review.md, not narrative.md."""
out = extract_narrative.render(outline)
assert "opening-punch" not in out
assert "call-to-adventure" not in out
assert "applied_patterns" not in out
def test_narrative_full_renders_per_slide_walk(extract_narrative, outline):
"""Full mode walks slides[] one line each — not the argument beats."""
out = extract_narrative.render(outline)
assert "## The Deck, Slide by Slide" in out
# One line per slide, keyed by slide number + title
assert "**1. Cold Open**" in out
assert "**11. New Bliss + Thanks**" in out
# The argument-beat prose must NOT appear in the full (slide) view
assert "Open cold with the receipt" not in out
assert "Pay off the master story" not in out
def test_narrative_slide_synopsis_prefers_overlay_then_visual(
extract_narrative, outline,
):
out = extract_narrative.render(outline)
# Slide 2 has a text_overlay — use it
assert "VALIDATION REMOVED · TESTS DELETED · TAX MISCOMPUTED" in out
# Slide 1's text_overlay is the literal "none" — fall back to its visual
assert "Receipt screenshot with one line circled in red." in out
def test_narrative_inlines_interlude_at_anchor(
extract_narrative, outline_schema, base_data,
):
"""An interlude renders as a live-demo line right after its anchor slide."""
data = copy.deepcopy(base_data)
data["interludes"] = [{
"id": "demo-vat",
"after_slide": 8,
"title": "Live coding: agent rewrites the VAT calc",
"chapter": "ch2",
"script": [{"line": "Watch what happens."}],
}]
outline = outline_schema.Outline.model_validate(data)
out = extract_narrative.render(outline)
assert "- *Live coding: agent rewrites the VAT calc — live demo*" in out
# It sits between slide 8 and slide 10
assert out.index("**8. Master Story Recall**") < out.index("Live coding")
assert out.index("Live coding") < out.index("**10. Call to Action**")
def test_narrative_omits_cuttable_marker_when_none(extract_narrative, outline):
"""Base fixture has no cuttable chapters — the marker must NOT appear."""
out = extract_narrative.render(outline)
assert "*cuttable*" not in out
def test_narrative_marks_cuttable_chapter(extract_narrative, outline_schema, base_data):
data = copy.deepcopy(base_data)
data["chapters"][0]["cuttable"] = True
outline = outline_schema.Outline.model_validate(data)
out = extract_narrative.render(outline)
assert "*cuttable*" in out
def test_narrative_renders_tldr_when_present(extract_narrative, outline):
"""The fixture's tldr renders under a TL;DR heading, bullets preserved."""
out = extract_narrative.render(outline)
assert "## TL;DR" in out
assert "Agents ship code that violates constraints" in out
assert "- They lack authority to push back." in out
def test_narrative_never_reprints_full_thesis(
extract_narrative, outline_schema, base_data,
):
"""The elaborated talk.thesis must never appear — only the tldr does."""
data = copy.deepcopy(base_data)
data["talk"]["thesis"] = "An elaborated multi-paragraph thesis goes here."
outline = outline_schema.Outline.model_validate(data)
out = extract_narrative.render(outline)
assert "elaborated multi-paragraph thesis" not in out
assert "## TL;DR" in out
# ── extract-narrative.py — partial (narrative-phase) rendering ───────
def test_narrative_partial_tldr_only_stub(
extract_narrative, outline_schema, base_data,
):
"""Phase 1 stub: tldr renders, chapter body is a 'not yet authored' note."""
data = {"talk": copy.deepcopy(base_data["talk"])}
data["talk"]["tldr"] = "Treat context as a first-class artifact."
partial = outline_schema.PartialOutline.model_validate(data)
out = extract_narrative.render(partial)
assert "## TL;DR" in out
assert "Treat context as a first-class artifact." in out
assert "Narrative arc not yet authored" in out
assert "### The Setup" not in out # no chapters yet
def test_narrative_partial_renders_chapters_without_slides(
extract_narrative, outline_schema, base_data,
):
"""Phase 2: chapters present, slides absent — full chapter body renders."""
chapters = copy.deepcopy(base_data["chapters"])
for c in chapters:
for beat in c.get("argument_beats", []):
beat["slide_refs"] = [] # no slides exist yet in the narrative phase
data = {"talk": copy.deepcopy(base_data["talk"]), "chapters": chapters}
partial = outline_schema.PartialOutline.model_validate(data)
out = extract_narrative.render(partial)
assert "### The Setup" in out
assert "### The Turn" in out
assert "Open cold with the receipt" in out
assert "Narrative arc not yet authored" not in out
def test_narrative_cli_partial_renders_talk_only(
extract_narrative, base_data, tmp_path, capsys,
):
"""CLI --partial renders a talk-only outline to stdout."""
data = {"talk": copy.deepcopy(base_data["talk"])}
path = tmp_path / "partial.yaml"
path.write_text(yaml.safe_dump(data), encoding="utf-8")
rc = extract_narrative.main(["extract-narrative.py", "--partial", str(path)])
assert rc == 0
out = capsys.readouterr().out
assert out.startswith("# Demo Talk — Narrative Read")
def test_narrative_cli_full_mode_rejects_slideless_outline(
extract_narrative, base_data, tmp_path, capsys,
):
"""Without --partial, a slides-less outline fails full validation (exit 1)."""
data = {"talk": copy.deepcopy(base_data["talk"])}
path = tmp_path / "slideless.yaml"
path.write_text(yaml.safe_dump(data), encoding="utf-8")
rc = extract_narrative.main(["extract-narrative.py", str(path)])
assert rc == 1
assert "failed to load" in capsys.readouterr().err
# ── check-rhetorical.py — happy path ─────────────────────────────────
def test_rhetorical_clean_fixture_has_no_flags(check_rhetorical, outline):
content, flag_count = check_rhetorical.render(outline)
assert flag_count == 0
assert "✅ Summary — no FLAGs" in content
def test_rhetorical_passes_opening_punch(check_rhetorical, outline):
content, _ = check_rhetorical.render(outline)
assert "### Opening PUNCH — ✅ **PASS**" in content
def test_rhetorical_reports_big_idea_location(check_rhetorical, outline):
content, _ = check_rhetorical.render(outline)
# Fixture has big_idea on slide 5 (Call to Adventure)
assert 'slide 5: "Call to Adventure"' in content
def test_rhetorical_reports_thesis_ordering(check_rhetorical, outline):
content, _ = check_rhetorical.render(outline)
# Fixture: preview slide 5, payoff slide 11
assert "preview slide 5 → payoff slide 11" in content
def test_rhetorical_passes_sparkline_when_complete(check_rhetorical, outline_schema, base_data):
"""Fixture's architecture is sparkline with call-to-adventure, new-bliss,
star-moment. Add call-to-action to complete the set."""
data = copy.deepcopy(base_data)
# Slide 10 already has call-to-action in the fixture
outline = outline_schema.Outline.model_validate(data)
content, _ = check_rhetorical.render(outline)
assert "### Call to Adventure — ✅ **PASS**" in content
assert "### Call to Action — ✅ **PASS**" in content
assert "### New Bliss — ✅ **PASS**" in content
assert "### S.T.A.R. moments — ✅ **PASS**" in content
def test_rhetorical_reports_master_story_threading(check_rhetorical, outline):
content, _ = check_rhetorical.render(outline)
assert "### Master story threading — ✅ **PASS**" in content
assert "`pandy`" in content
assert "introduce@slide 3" in content
def test_rhetorical_reports_callback_chains(check_rhetorical, outline):
content, _ = check_rhetorical.render(outline)
assert "`receipt-motif`" in content
def test_rhetorical_includes_duration_accounting(check_rhetorical, outline):
content, _ = check_rhetorical.render(outline)
assert "### Duration accounting" in content
# ── check-rhetorical.py — FLAG cases via mutation ────────────────────
def test_rhetorical_flags_missing_opening_punch(check_rhetorical, outline_schema, base_data):
data = copy.deepcopy(base_data)
# Remove opening-punch from slide 1
data["slides"][0]["applied_patterns"] = [
p for p in data["slides"][0]["applied_patterns"]
if p.get("id") != "opening-punch"
]
outline = outline_schema.Outline.model_validate(data)
content, flag_count = check_rhetorical.render(outline)
assert flag_count >= 1
assert "### Opening PUNCH — ⚠️ **FLAG**" in content
def test_rhetorical_flags_sparkline_missing_call_to_action(
check_rhetorical, outline_schema, base_data,
):
data = copy.deepcopy(base_data)
# Remove call-to-action from slide 10
slide_10 = next(s for s in data["slides"] if s["n"] == 10)
slide_10["applied_patterns"] = [
p for p in slide_10["applied_patterns"]
if p.get("id") != "call-to-action"
]
outline = outline_schema.Outline.model_validate(data)
content, flag_count = check_rhetorical.render(outline)
assert flag_count >= 1
assert "### Call to Action — ⚠️ **FLAG**" in content
def test_rhetorical_flags_too_many_inoculations(
check_rhetorical, outline_schema, base_data,
):
data = copy.deepcopy(base_data)
# Fixture has 1 inoculation; add 3 more to push past the ≤3 limit
for slide_n, vector in [(8, "fear"), (10, "obstacles"), (11, "comfort-zone")]:
slide = next(s for s in data["slides"] if s["n"] == slide_n)
slide.setdefault("applied_patterns", []).append({
"id": "inoculation",
"resistance_vector": vector,
})
outline = outline_schema.Outline.model_validate(data)
content, flag_count = check_rhetorical.render(outline)
assert flag_count >= 1
assert "Inoculation count — ⚠️ **FLAG**" in content
def test_rhetorical_na_for_non_sparkline_arch(
check_rhetorical, outline_schema, base_data,
):
data = copy.deepcopy(base_data)
data["talk"]["architecture"] = "talklet"
# Remove sparkline-only patterns since they only make sense in sparkline talks
# (test would otherwise fail because call-to-adventure has big_idea_text and
# we'd need to find another slide to be the big_idea)
# For this test, just verify the N/A is emitted regardless
outline = outline_schema.Outline.model_validate(data)
content, _ = check_rhetorical.render(outline)
assert "Sparkline elements — — *N/A*" in content
def test_rhetorical_strict_mode_returns_flag_count(check_rhetorical, outline_schema, base_data):
"""Verify the render() function returns the flag count for --strict gating."""
data = copy.deepcopy(base_data)
data["slides"][0]["applied_patterns"] = [
p for p in data["slides"][0]["applied_patterns"]
if p.get("id") != "opening-punch"
]
outline = outline_schema.Outline.model_validate(data)
_, flag_count = check_rhetorical.render(outline)
assert flag_count >= 1.github
eval-resources
humor-postmortem-blind-spots
qr-bitly-slug-from-outline
qr-missing-shortener-detection
shownotes-publisher-omit-placeholder
shownotes-publisher-publish-no-date
shownotes-publisher-publish-with-date
shownotes-publisher-update-add-video
video-extraction-diagnostics
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
rules
scripts
skills
illustrations
presentation-creator
references
patterns
build
deliver
prepare
scripts
shownotes-publisher
vault-clarification
vault-ingress
vault-profile
tests