CtrlK
BlogDocsLog inGet started
Tessl Logo

jbaruch/speaker-toolkit

Seven-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, create and publish talk-content Agent Skills with talk pages to a Jekyll shownotes site, and verify a recorded screencast against its storyboard. Includes a 113-entry Presentation Patterns taxonomy (83 observable: 64 patterns + 19 antipatterns; 30 unobservable: 21 patterns + 9 antipatterns) for scoring, brainstorming, and go-live preparation.

75

Quality

94%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

check-rhetorical.pyskills/presentation-creator/scripts/

#!/usr/bin/env python3
"""check-rhetorical.py — structural gap-check over the closed pattern taxonomy.

Walks the validated Outline and reports whether the talk has the
structural elements its declared architecture requires. Output is
deterministic (script territory per rules/script-delegation.md) — every
check returns PASS, FLAG, or N/A with a short reason. The agent can
synthesize commentary on top of this report; the report itself is
purely mechanical.

Usage:
    check-rhetorical.py <outline.yaml>              # report mode (exit 0)
    check-rhetorical.py <outline.yaml> --strict     # exit 1 if any FLAG

Output: rhetorical-review.md content to stdout.
"""

from __future__ import annotations

import sys
from dataclasses import dataclass
from pathlib import Path

_HERE = Path(__file__).resolve().parent
if str(_HERE) not in sys.path:
    sys.path.insert(0, str(_HERE))

import yaml  # noqa: E402
from pydantic import ValidationError  # noqa: E402

import outline_schema as _os  # noqa: E402


@dataclass
class CheckResult:
    name: str
    status: str  # PASS | FLAG | N/A | INFO
    detail: str


def _all_applied_patterns(outline: _os.Outline) -> list[tuple[str, _os.AppliedPattern]]:
    """Walk every applied_pattern with a location label."""
    out: list[tuple[str, _os.AppliedPattern]] = []
    for p in outline.talk.applied_patterns:
        out.append(("talk", p))
    for s in outline.slides:
        for p in s.applied_patterns:
            out.append((f"slide {s.n}", p))
    for il in outline.interludes:
        for p in il.applied_patterns:
            out.append((f"interlude {il.id}", p))
    return out


# ── Individual checks ────────────────────────────────────────────────


def _check_opening_punch(outline: _os.Outline) -> CheckResult:
    """At least one slide in the first 10% of the deck declares opening-punch
    with at least one flavor."""
    cutoff = max(1, int(0.1 * len(outline.slides)) + 1)
    opening_slides = outline.slides[:cutoff]
    flavored: list[tuple[int, list[str]]] = []
    flavorless: list[int] = []
    for s in opening_slides:
        for p in s.applied_patterns:
            if p.id == "opening-punch":
                flavors = [f.value for f in (p.flavors or [])]
                if flavors:
                    flavored.append((s.n, flavors))
                else:
                    flavorless.append(s.n)
    if not flavored:
        if flavorless:
            return CheckResult(
                "Opening PUNCH",
                "FLAG",
                f"`opening-punch` declared on slide(s) {flavorless} but with "
                "no `flavors:` set — pick at least one of "
                "{personal, unexpected, novel, challenging, humorous}.",
            )
        return CheckResult(
            "Opening PUNCH",
            "FLAG",
            f"No `opening-punch` declared in the first {cutoff} slides. "
            "Audiences grant ~2 min before forming a verdict — the opening "
            "needs at least one PUNCH flavor.",
        )
    where = "; ".join(f"slide {n}: flavors={flavors}" for n, flavors in flavored)
    return CheckResult("Opening PUNCH", "PASS", where)


def _check_big_idea(outline: _os.Outline) -> CheckResult:
    """Exactly one slide carries big_idea=true (already enforced by the
    schema validator — this report-side check surfaces *where* it lives)."""
    marked = [s for s in outline.slides if s.big_idea]
    if len(marked) != 1:
        return CheckResult(
            "Big Idea singleton",
            "FLAG",
            f"Found {len(marked)} big_idea slides "
            f"(schema requires exactly 1) — this should have been caught by "
            "schema validation.",
        )
    slide = marked[0]
    # Look for the actual text on the call-to-adventure pattern, if present
    big_idea_text = None
    for p in slide.applied_patterns:
        if p.id == "call-to-adventure" and p.big_idea_text:
            big_idea_text = p.big_idea_text
            break
    detail = f'slide {slide.n}: "{slide.title}"'
    if big_idea_text:
        detail += f" — text: {big_idea_text!r}"
    return CheckResult("Big Idea singleton", "PASS", detail)


def _check_thesis_ordering(outline: _os.Outline) -> CheckResult:
    """thesis: preview must come before thesis: payoff (when both present)."""
    previews = [s.n for s in outline.slides if s.thesis == "preview"]
    payoffs = [s.n for s in outline.slides if s.thesis == "payoff"]
    if not previews and not payoffs:
        return CheckResult(
            "Thesis preview/payoff",
            "N/A",
            "No thesis preview or payoff declared.",
        )
    if previews and not payoffs:
        return CheckResult(
            "Thesis preview/payoff",
            "FLAG",
            f"Preview at slide {previews} but no payoff declared.",
        )
    if payoffs and not previews:
        return CheckResult(
            "Thesis preview/payoff",
            "FLAG",
            f"Payoff at slide {payoffs} but no preview declared.",
        )
    if max(previews) >= min(payoffs):
        return CheckResult(
            "Thesis preview/payoff",
            "FLAG",
            f"Preview slides {previews} not strictly before payoff slides {payoffs}.",
        )
    return CheckResult(
        "Thesis preview/payoff",
        "PASS",
        f"preview slide {previews[0]} → payoff slide {payoffs[0]}",
    )


def _check_sparkline_requirements(outline: _os.Outline) -> list[CheckResult]:
    """When architecture == sparkline, the talk must declare:
    - exactly one call-to-adventure
    - exactly one call-to-action
    - exactly one new-bliss
    - at least one star-moment
    """
    if outline.talk.architecture != "sparkline":
        return [
            CheckResult(
                "Sparkline elements",
                "N/A",
                f"Architecture is `{outline.talk.architecture}`, not sparkline.",
            )
        ]
    counts: dict[str, list[str]] = {
        "call-to-adventure": [],
        "call-to-action": [],
        "new-bliss": [],
        "star-moment": [],
    }
    for location, p in _all_applied_patterns(outline):
        if p.id in counts:
            counts[p.id].append(location)
    out: list[CheckResult] = []
    for pid, expected_min, expected_max, label in [
        ("call-to-adventure", 1, 1, "Call to Adventure"),
        ("call-to-action", 1, 1, "Call to Action"),
        ("new-bliss", 1, 1, "New Bliss"),
        ("star-moment", 1, None, "S.T.A.R. moments"),
    ]:
        n = len(counts[pid])
        if n < expected_min:
            out.append(
                CheckResult(
                    label,
                    "FLAG",
                    f"Sparkline requires ≥{expected_min}, found {n}.",
                )
            )
        elif expected_max is not None and n > expected_max:
            out.append(
                CheckResult(
                    label,
                    "FLAG",
                    f"Sparkline requires exactly {expected_max}, found {n} at "
                    f"{counts[pid]}.",
                )
            )
        else:
            out.append(
                CheckResult(
                    label,
                    "PASS",
                    f"{n} declared at {counts[pid]}",
                )
            )
    return out


def _check_register_coverage(outline: _os.Outline) -> list[CheckResult]:
    """Enforce the `walk-around` cover-or-match decision declared at intake.

    - `heterogeneous` — the union of registers declared across every
      walk-around application must cover all four. A mixed room contains all
      four questions; leaving one unanswered leaves that slice of the room
      waiting.
    - `homogeneous` — the declared `dominant_register` must actually appear
      among the declared registers. A room matched on paper and not in the
      talk is matched nowhere.

    Zero walk-around applications FLAGs under either spread — a declared
    spread with no audit is an undefended claim, and `N/A` would let a
    homogeneous talk name a dominant register it never answers.

    A `walk-around` declared without `registers:` also FLAGs, naming the
    locations, rather than being silently skipped (mirrors
    `_check_opening_punch`'s treatment of a flavorless `opening-punch`).
    """
    spread = outline.talk.audience_spread
    declared: dict[str, list[str]] = {}
    unannotated: list[str] = []
    for location, p in _all_applied_patterns(outline):
        if p.id != "walk-around":
            continue
        if not p.registers:
            unannotated.append(location)
            continue
        for r in p.registers:
            declared.setdefault(r.value, []).append(location)

    # Label tracks the spread so the section header stays stable for readers
    # and for anything parsing the report.
    label = (
        "Register match"
        if spread == _os.AudienceSpread.homogeneous
        else "Register coverage"
    )

    if unannotated:
        return [
            CheckResult(
                label,
                "FLAG",
                f"`walk-around` declared at {unannotated} with no `registers:` set "
                f"— name which of {{A, B, C, D}} each claim answers, or the audit "
                f"cannot be checked.",
            )
        ]

    if spread == _os.AudienceSpread.homogeneous:
        dom = outline.talk.dominant_register
        assert dom is not None  # schema validator guarantees this
        if not declared:
            return [
                CheckResult(
                    "Register match",
                    "FLAG",
                    f"Room declared homogeneous on register `{dom.value}`, but no "
                    f"claim declares a walk-around answering it. Matching a room "
                    f"is a claim about the talk, not just about the audience — "
                    f"answer `{dom.value}` on the load-bearing claims, or "
                    f"re-declare the spread as heterogeneous.",
                )
            ]
        if dom.value not in declared:
            return [
                CheckResult(
                    "Register match",
                    "FLAG",
                    f"Room declared homogeneous on register `{dom.value}`, but no "
                    f"walk-around answers it (declared: "
                    f"{sorted(declared)}). Match the room or re-declare the spread.",
                )
            ]
        return [
            CheckResult(
                "Register match",
                "PASS",
                f"Homogeneous room matched on `{dom.value}` at {declared[dom.value]}",
            )
        ]

    missing = [r.value for r in _os.Register if r.value not in declared]
    if not declared:
        return [
            CheckResult(
                "Register coverage",
                "FLAG",
                "Room declared heterogeneous, but no claim declares a walk-around. "
                "A mixed room contains all four questions — audit the load-bearing "
                "claims, or re-declare the spread as homogeneous.",
            )
        ]
    if missing:
        return [
            CheckResult(
                "Register coverage",
                "FLAG",
                f"Room declared heterogeneous; registers {missing} unanswered "
                f"(covered: {sorted(declared)}). Every register left unanswered is "
                f"a slice of the room whose question the talk never reaches.",
            )
        ]
    by_register = dict(sorted(declared.items()))
    return [
        CheckResult(
            "Register coverage",
            "PASS",
            f"All four registers answered: {by_register}",
        )
    ]


def _check_master_story_threading(outline: _os.Outline) -> CheckResult:
    """Each master-story id must have `introduce` before any `recall-N`,
    and recall indexes should appear in increasing order."""
    threads: dict[str, list[tuple[int, str]]] = {}
    for s in outline.slides:
        for p in s.applied_patterns:
            if p.id == "master-story" and p.story_id:
                threads.setdefault(p.story_id, []).append(
                    (s.n, p.beat.value if p.beat else "?"),
                )
    if not threads:
        return CheckResult(
            "Master story threading",
            "N/A",
            "No master-story patterns declared.",
        )
    problems: list[str] = []
    summary: list[str] = []
    for story, beats in threads.items():
        beats.sort(key=lambda x: x[0])
        beat_seq = " → ".join(f"{b}@slide {n}" for n, b in beats)
        summary.append(f"`{story}`: {beat_seq}")
        kinds = [b for _, b in beats]
        if "introduce" not in kinds:
            problems.append(f"`{story}` missing `introduce` beat")
            continue
        intro_idx = kinds.index("introduce")
        if intro_idx != 0:
            problems.append(
                f"`{story}` introduce is not the first beat (kinds={kinds})",
            )
        recalls = [k for k in kinds if k.startswith("recall-")]
        if recalls != sorted(recalls):
            problems.append(
                f"`{story}` recall beats out of order: {recalls}",
            )
    if problems:
        return CheckResult(
            "Master story threading",
            "FLAG",
            "; ".join(problems) + ". Threads: " + " | ".join(summary),
        )
    return CheckResult(
        "Master story threading",
        "PASS",
        " | ".join(summary),
    )


def _check_inoculation_count(outline: _os.Outline) -> CheckResult:
    """phase3-content.md: inoculations should be ≤3 per talk; reserved
    for objections that would derail the room."""
    count = 0
    where: list[str] = []
    for location, p in _all_applied_patterns(outline):
        if p.id == "inoculation":
            count += 1
            vector = p.resistance_vector.value if p.resistance_vector else "?"
            where.append(f"{location} ({vector})")
    if count == 0:
        return CheckResult(
            "Inoculation count",
            "INFO",
            "No inoculation beats declared. Persuasive talks usually want "
            "1–3 to preempt the strongest derailing objection.",
        )
    if count > 3:
        return CheckResult(
            "Inoculation count",
            "FLAG",
            f"{count} inoculations (recommended ≤3 — overuse makes the talk "
            f"feel defensive): {where}",
        )
    return CheckResult(
        "Inoculation count",
        "PASS",
        f"{count}/3: {where}",
    )


def _check_progressive_lists(outline: _os.Outline) -> CheckResult:
    """Each progressive_list id's item indexes should be contiguous starting at 1."""
    lists: dict[str, list[int]] = {}
    for s in outline.slides:
        for pl in s.progressive_lists:
            lists.setdefault(pl.id, []).append(pl.item_index)
    if not lists:
        return CheckResult(
            "Progressive lists",
            "N/A",
            "No progressive lists declared.",
        )
    problems: list[str] = []
    summary: list[str] = []
    for lid, idxs in lists.items():
        idxs.sort()
        summary.append(f"`{lid}` items {idxs}")
        if idxs != list(range(1, len(idxs) + 1)):
            problems.append(f"`{lid}` indexes not contiguous from 1: {idxs}")
    if problems:
        return CheckResult(
            "Progressive lists",
            "FLAG",
            "; ".join(problems),
        )
    return CheckResult(
        "Progressive lists",
        "PASS",
        " | ".join(summary),
    )


def _check_running_gags(outline: _os.Outline) -> CheckResult:
    """A running gag should appear ≥2 times (otherwise it's a one-shot, not a gag)."""
    gags: dict[str, list[int]] = {}
    for s in outline.slides:
        for rg in s.running_gags:
            gags.setdefault(rg.id, []).append(rg.appearance_index)
    if not gags:
        return CheckResult(
            "Running gags",
            "N/A",
            "No running gags declared.",
        )
    problems: list[str] = []
    summary: list[str] = []
    for gid, idxs in gags.items():
        idxs.sort()
        summary.append(f"`{gid}` appearances {idxs}")
        if len(idxs) < 2:
            problems.append(
                f"`{gid}` only has {len(idxs)} appearance — gags need ≥2 escalations",
            )
        if idxs != list(range(1, len(idxs) + 1)):
            problems.append(f"`{gid}` indexes not contiguous from 1: {idxs}")
    if problems:
        return CheckResult(
            "Running gags",
            "FLAG",
            "; ".join(problems),
        )
    return CheckResult(
        "Running gags",
        "PASS",
        " | ".join(summary),
    )


def _check_duration_accounting(outline: _os.Outline) -> CheckResult:
    """Chapter target_min should sum to roughly the talk duration_min."""
    chapter_sum = sum(c.target_min for c in outline.chapters)
    talk_dur = outline.talk.duration_min
    diff = chapter_sum - talk_dur
    pct = abs(diff) / talk_dur * 100 if talk_dur else 0
    detail = (
        f"chapter sum {chapter_sum:g} min vs talk duration {talk_dur:g} min "
        f"(diff {diff:+.1f} min, {pct:.0f}%)"
    )
    if pct > 20:
        return CheckResult(
            "Duration accounting",
            "FLAG",
            detail + " — >20% off; Q&A budget aside, this needs reconciling.",
        )
    return CheckResult("Duration accounting", "PASS", detail)


def _check_callback_ledger(outline: _os.Outline) -> CheckResult:
    """Already enforced by schema validator; this surfaces the chains."""
    chains: dict[str, dict[str, list[str]]] = {}
    for s in outline.slides:
        for cb in s.callbacks:
            entry = chains.setdefault(cb.id, {"plant": [], "pay": []})
            entry[cb.kind].append(f"slide {s.n}")
    for il in outline.interludes:
        for cb in il.callbacks:
            entry = chains.setdefault(cb.id, {"plant": [], "pay": []})
            entry[cb.kind].append(f"interlude {il.id}")
    if not chains:
        return CheckResult(
            "Callback chains",
            "N/A",
            "No callbacks declared.",
        )
    summary = " | ".join(
        f"`{cid}`: plant {e['plant']} → pay {e['pay']}" for cid, e in chains.items()
    )
    return CheckResult("Callback chains", "PASS", summary)


# ── Report rendering ─────────────────────────────────────────────────


_STATUS_BADGE = {
    "PASS": "✅ **PASS**",
    "FLAG": "⚠️  **FLAG**",
    "N/A": "— *N/A*",
    "INFO": "ℹ️  *INFO*",
}


def render(outline: _os.Outline) -> tuple[str, int]:
    """Render rhetorical-review.md and return (content, flag_count)."""
    checks: list[CheckResult] = [
        _check_opening_punch(outline),
        _check_big_idea(outline),
        _check_thesis_ordering(outline),
        *_check_sparkline_requirements(outline),
        *_check_register_coverage(outline),
        _check_master_story_threading(outline),
        _check_inoculation_count(outline),
        _check_progressive_lists(outline),
        _check_running_gags(outline),
        _check_callback_ledger(outline),
        _check_duration_accounting(outline),
    ]
    flag_count = sum(1 for c in checks if c.status == "FLAG")

    lines: list[str] = []
    lines.append(f"# {outline.talk.title} — Rhetorical Review")
    lines.append("")
    lines.append(
        f"**Architecture:** `{outline.talk.architecture}` · "
        f"**{len(outline.slides)} slides** across "
        f"**{len(outline.chapters)} chapters** · "
        f"**{len(outline.interludes)} interludes**",
    )
    lines.append("")
    lines.append(
        "> Mechanical gap-check over the closed pattern taxonomy. Every "
        "row is one structural assertion the talk's architecture either "
        "satisfies (PASS), fails (FLAG), or doesn't apply (N/A). FLAGs "
        "indicate structural gaps — investigate, don't auto-fix.",
    )
    lines.append("")

    if flag_count == 0:
        lines.append(f"## ✅ Summary — no FLAGs ({len(checks)} checks)")
    else:
        lines.append(
            f"## ⚠️ Summary — {flag_count} FLAG"
            f"{'s' if flag_count != 1 else ''} of {len(checks)} checks",
        )
    lines.append("")

    for c in checks:
        badge = _STATUS_BADGE.get(c.status, c.status)
        lines.append(f"### {c.name} — {badge}")
        lines.append("")
        lines.append(c.detail)
        lines.append("")

    # Collapse blank runs
    cleaned: list[str] = []
    prev_blank = False
    for line in lines:
        is_blank = line.strip() == ""
        if is_blank and prev_blank:
            continue
        cleaned.append(line)
        prev_blank = is_blank
    while cleaned and cleaned[-1].strip() == "":
        cleaned.pop()
    cleaned.append("")
    return "\n".join(cleaned), flag_count


def main(argv: list[str]) -> int:
    strict = "--strict" in argv
    args = [a for a in argv[1:] if a != "--strict"]
    if len(args) != 1:
        print(
            "usage: check-rhetorical.py <outline.yaml> [--strict]\n"
            "       prints rhetorical-review.md to stdout\n"
            "       --strict: exit 1 if any FLAG (default: always exit 0)",
            file=sys.stderr,
        )
        return 2
    try:
        outline = _os.load_outline(args[0])
    except (OSError, yaml.YAMLError, ValidationError) as exc:
        print(f"failed to load {args[0]}: {exc}", file=sys.stderr)
        return 1
    content, flag_count = render(outline)
    sys.stdout.write(content)
    if strict and flag_count > 0:
        print(
            f"\n{flag_count} FLAG(s) found — strict mode exit non-zero",
            file=sys.stderr,
        )
        return 1
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))

skills

presentation-creator

SKILL.md

README.md

tile.json