Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions .agents/skills/close-read-sheet/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
name: close-read-sheet
description: Create a guided-learning Close Read scrollytelling sheet (data/*.json) from a base text reference (e.g. "Pirkei Avot 2:5"), a Sefaria source sheet (URL or numeric ID), or a Sefaria topic. Use whenever the user asks to "build a Close Read on …", "make a guided learning of …", "turn this sheet into a scrolly", "create a close-read for [Mishnah/parsha/sugya/topic]", or similar. Produces a fully populated data/<slug>.json plus an index.json entry, verified in the browser.
---

# Close Read sheet generator

This skill turns a piece of Torah/Mishnah/Talmud — or a Sefaria source sheet or topic — into a scrollytelling **Close Read** sheet (`data/<slug>.json`) that runs on the engine in this repo.

## When to invoke

The user gives one of four inputs:

1. **A base text reference** — e.g. `Pirkei Avot 2:5`, `Genesis 22:1-19`, `Berakhot 7a:14`, `Mishnah Berakhot 1:1`.
2. **A Sefaria source sheet** — a URL like `https://www.sefaria.org/sheets/160545` or a bare numeric ID.
3. **A Sefaria topic** — e.g. `Akedah`, `Prayer`, `Tochacha`.
4. **A parsha name with "Nechama" framing** — e.g. "Nechama on Beha'alotcha", "Nechama Leibowitz for Shelach", "build the Nasso experience". This is the multi-decade-rendering Mode 4 — see `references/pedagogical-arcs.md`.

Output: a complete `data/<slug>.json` and an entry appended to `data/index.json`. Verify in the browser before reporting done.

For Mode 4 (Nechama parsha), there is also a research pack at
`research/parshiyot/<parsha>.md` — produce it as the Stage-1/2 deliverable and **stop for
user confirmation before harvesting** all selected gilyonot.

## Required reading before you start

Open these in this order. They're short:

- [docs/data-format.md](../../../docs/data-format.md) — full schema for the JSON
- [docs/conversion-guide.md](../../../docs/conversion-guide.md) — practical conversion recipe
- [docs/architecture.md](../../../docs/architecture.md) — how the engine renders the JSON, including comparison mode and verse-change semantics
- [references/pedagogical-arcs.md](references/pedagogical-arcs.md) — how to design the 4–5 section arc for each input mode
- [references/hard-rules.md](references/hard-rules.md) — non-negotiable rules: Hebrew cleaning, word-group constraints, source-color mapping, when to use comparison vs inline quote

Canonical examples to imitate (read at least one before drafting):

- [data/pirkei-avot-1-11.json](../../../data/pirkei-avot-1-11.json) — five sub-sections sharing a single pinned Mishnah; commentators old-to-new; Hasidic + modern closing
- [data/yitro-5730.json](../../../data/yitro-5730.json) — three sections including one comparison-mode pair of biblical verses
- [data/shemini-5731.json](../../../data/shemini-5731.json) — two sections drilling into one verse from multiple angles

## Workflow

### Phase 1 — Fetch source material

Use the Sefaria MCP tools (`get_text`, `get_links_between_texts`, `get_topic_details`, `get_english_translations`, `text_search`). For source sheets the MCP may not have a direct tool. Fall back to `curl -sL -A "Sefaria/close-read" https://www.sefaria.org/api/sheets/<id>`, or Python `urllib` with the same header, rather than `WebFetch`, whose User-Agent we don't control. `Sefaria/<service>` is the in-house User-Agent convention. Never send a bare `Mozilla/5.0`: Cloudflare challenges it with a 403.

What to grab depends on the input mode:

- **Base text ref:** the text in both languages; `get_links_between_texts` to discover available commentary; then fetch each commentary you plan to use.
- **Source sheet:** the sheet JSON (sources, author commentary, structure). The sheet's *order* and *highlighting* are usable directly — the author has already done the pedagogical sequencing.
- **Topic:** `get_topic_details` for the anchor texts; then fetch each anchor text and the most-cited commentaries for it.

### Phase 2 — Design the arc, then **stop and confirm with the user**

Before writing 30+ steps of content, present:

- Title, subtitle (English; Hebrew if the corpus supports it)
- Section titles in order (typically 3–5)
- Per section: primary text, the 2–3 commentators or sources you'll use, and the question that closes the section
- Total step estimate

Use `AskUserQuestion` if there's a meaningful design decision (which commentators to feature, whether to include a Hasidic/modern voice, how dense the sheet should be, whether to address the original Hebrew or stay English-only).

See [references/pedagogical-arcs.md](references/pedagogical-arcs.md) for the arc templates for each input mode.

### Phase 3 — Write the JSON

Build the file in one pass using a template based on the closest canonical example. Hold to these invariants:

- **Hebrew text must be cantillation-stripped, nikkud-preserved.** Python recipe in [references/hard-rules.md](references/hard-rules.md). Texts from `Torat Emet 357` (Mishnah/Avot) already lack cantillation; Tanakh versions from `Miqra according to the Masorah` need stripping.
- **Word groups within one verse cannot overlap.** Section A might split a Mishnah into 8 phrase-keys; sub-sections that re-use the same `primaryText` MUST use compatible (non-overlapping) word maps. The engine wraps once and reuses across the group.
- **Consecutive sections that share `primaryText.ref` + `he` collapse into one sticky panel.** The user reads the same text continuously while the sidebar advances through sub-sections. This is the preferred shape for a deep dive on a single base text.
- **Short prooftexts (e.g. a single biblical verse quoted by a commentary) stay inline within the commentary card or its annotation.** Do not use `verse-change` to take over the main panel for them.
- **`verse-change` is for substantive parallel/contrast texts** the user will study at length. Use sparingly. Always return to the base text before the next section ends.
- **Comparison mode is for short verse pairs only** (biblical pasuk vs pasuk). Do not use it for full mishnayot or long passages — it cramps the laptop layout. For parallel mishnayot, embed the second one as a commentary card.
- **Closing section, when the corpus allows it,** carries the idea forward through one Hasidic voice (Likutei Moharan, Toldot Yaakov Yosef, Ba'al Shem Tov) and one modern voice (Rav Uziel's Hegyonei Uziel, Rav Kook, Nechama Leibowitz, etc.). Two cards plus framing narration.
- **Source-label colors:** existing CSS tokens are `Rashi`, `Ramban`, `Ibn Ezra`, `Bereshit Rabbah` / `Aggadat Bereishit` / `Tanchuma` (midrash purple), `Mekhilta`, `Talmud`. Other sources fall back to neutral warm-brown. Don't add CSS tokens in this skill — keep it data-only.
- **Narration voice:** didactic guide. The narrator names the puzzles; the commentaries do the work. Don't moralize. Don't summarize commentaries before quoting them.
- **English-only is fine for narration/annotation.** If the corpus has a strong Hebrew tradition (Mishnah, Tanakh, Talmud) keep the primary text and direct commentary quotations bilingual; the rest can be English-only. The engine's title/section/subtitle/source-label/annotation/narration fields all gracefully omit the `.he` field if absent.

### Phase 4 — Register and verify

1. Append a new entry to `data/index.json` with `{slug, title, author, description}`. Author is the original Sefaria author/anthologist when there is one (e.g. for a sheet); otherwise the text's tradition (e.g. `"Avtalyon"` for Avot 1:11) or omit if unclear.
2. Validate the JSON parses: `python3 -c "import json; json.load(open('data/<slug>.json'))"`.
3. Validate every word-group Hebrew phrase actually occurs in its verse text — script in [references/hard-rules.md](references/hard-rules.md).
4. Start the preview server (via `preview_start` against the launch config) and load `?sheet=<slug>`. Use `preview_eval` to confirm:
- The sheet renders (no `undefined` in DOM)
- All sections appear (check `.cr-section` count and `.section-dot` count)
- Primary panel stays pinned across same-text sub-sections (sample `panelVisible` at several scroll positions)
- Highlights fire on a card with a `highlight` array (check `.word-group.highlighted`)
- No errors in console (`preview_console_logs` filtered to `error`)

### Phase 5 — Report

Tell the user the slug, the section structure, and the URL `?sheet=<slug>`. Note any commentaries you considered but dropped, and why.

## Things to skip

- Adding new CSS color tokens. Use the fallback for un-tokenized sources. Mention it as a follow-up if a sheet leans heavily on a single un-tokenized commentator.
- Modifying the engine. The data format is expressive; if you find yourself wanting a new step type, the answer is almost always to use existing types differently.
- Long Hebrew narration cards. The engine renders them but reading bilingual narration alongside primary text overloads the eye. Hebrew belongs in the primary panel and in direct commentary quotations.
- Adding Hebrew titles when none are natural. Section titles can be English-only (`{ "en": "..." }`).
158 changes: 158 additions & 0 deletions .agents/skills/close-read-sheet/references/hard-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Hard rules — what the engine does and does not tolerate

Read this once before writing your first card.

## Hebrew text cleaning

The engine renders `he` fields as-is into RTL HTML. Cantillation marks distract; nikkud is essential.

**Strip cantillation, keep nikkud.** Unicode ranges to strip:

- U+0591 – U+05AF (most cantillation)
- U+05BD (meteg, when it's a cantillation marker)
- U+05C0 (paseq)
- U+05C3 (sof pasuq, ׃)
- U+05C6 (nun hafukha)

Keep U+05B0 – U+05BC (sheva, nikkud, dagesh).

Python recipe:

```python
import re
def strip_cantillation(text):
return re.sub(r'[֑-ֽ֯׀׃׆]', '', text)
```

Which Sefaria versions need stripping:

| Version | Cantillation present? |
|---|---|
| `Torat Emet 357` (Mishnah, Avot) | No — use as-is |
| `Tanach with Nikkud` | No |
| `Miqra according to the Masorah` | **Yes — strip** |
| `Tanach with Ta'amei Hamikra` | **Yes — strip** |
| `Tanach with Text Only` | No (also has no nikkud, less ideal) |

Commentary texts (Rashi, Rambam, Bartenura, etc.) usually have no cantillation. The Vilna edition Rashi may include some markers in the verse-headers (the bold-formatted lemma fragments) — strip those.

## Word groups within a verse

The engine wraps each phrase in `<span class="word-group" data-word="<key>">`. Two rules:

1. **No overlapping phrases within the same verse's `words` map.** If `chachamim` is `חֲכָמִים` (one word) and `chachamim-hizaharu` is `חֲכָמִים, הִזָּהֲרוּ` (two words), the second wrap will collide with the existing span around the first. Result: one of them silently fails to wrap, and its highlight will not render. Pick *one* granularity per phrase.

2. **English phrases must be unique substrings within the verse's English.** The wrap uses a global regex; if the phrase occurs twice, both get wrapped — and the highlight will hit both, which is almost always wrong. Test before relying on a generic phrase like `"the children"` or `"and die"`.

For comparison-mode word maps, add `"side": "left"` or `"side": "right"` to disambiguate phrases that appear on both sides.

## Sections sharing a primary text

The engine collapses consecutive sections whose `primaryText.ref` and `he` are identical into one sticky panel. This is the default smooth-continuity behavior — usually what you want.

When sections share `primaryText`, their `words` maps are **merged into a union** for the single rendered panel. Therefore:

- Same key across sections must have the same `he` value. If section A defines `mayim-raim: וְתִגְלוּ לִמְקוֹם מַיִם הָרָעִים` and section B redefines `mayim-raim: לִמְקוֹם מַיִם הָרָעִים`, the union takes whichever wins the object-merge — which is hard to predict. Just keep them consistent.
- Don't introduce a wider key in a later section that overlaps an existing narrower key (the overlap rule above). If section A has `chachamim: חֲכָמִים` and section E wants to highlight `חֲכָמִים, הִזָּהֲרוּ בְדִבְרֵיכֶם` together, the right move is to give section E a *highlight array* of `["chachamim", "hizaharu-bedivreikhem"]`, not a new wider key.

## Source labels and colors

The engine routes `source` → CSS color via `data-source` attribute. Existing tokens:

| `source` value | Visual family |
|---|---|
| `Rashi` | warm brown (the default Rashi token) |
| `Ramban` | green |
| `Ibn Ezra` | blue |
| `Bereshit Rabbah`, `Aggadat Bereishit`, `Tanchuma` | midrash purple |
| `Mekhilta` | (neutral) |
| `Talmud` | (neutral) |
| `Abarbanel`, `Shem Olam` | (neutral) |
| *anything else* | neutral warm-brown fallback |

Don't add new tokens in this skill. If a sheet leans hard on a single un-tokenized commentator (e.g. Bartenura is the workhorse), mention it as a follow-up — the user can add a CSS rule later.

For `sourceLabel`:
- Use the commentator's Hebrew name when the corpus is bilingual: `{ "he": "רש״י", "en": "Rashi" }`
- English-only label is fine when the corpus is mostly English (modern voices, source-sheet derivations)
- Use the label to disambiguate when the same `source` field carries multiple voices: `"Mekhilta DeRabbi Yishmael"` vs `"Mekhilta DeRashbi"` both with `source: "Mekhilta"`

## When to use comparison mode vs inline quotation

| Situation | Format |
|---|---|
| Two short biblical verses in direct tension (e.g. Ex 20:5 vs Deut 24:16) | Comparison mode in `primaryText` |
| One short biblical verse cited by a commentary as a prooftext | Inline within the commentary's `text` or `annotation` |
| A full parallel mishnah or sugya being discussed | Inline as a `commentary` card with its own `sourceLabel` |
| A substantial parallel passage the user will dwell on | `verse-change` to a new single-mode primary, then return |

Comparison mode crowds the laptop layout when either side runs longer than ~3 short lines. For full mishnayot or paragraphs of Tanakh, always use inline or `verse-change`.

## When to use verse-change

- The new text is **substantive** and the user will study it (not just glance at a prooftext)
- The new text is **short enough** to fit comfortably in the sticky panel (one verse, one short paragraph)
- You can return to the base text within the same section

Counter-cases (use inline instead):
- A one-line prooftext quoted to support a commentary's reading
- A long passage you only want to gesture at
- A text the user will *compare* rather than study (in which case: comparison mode, if both are short enough)

## Validation script

Before declaring the sheet done, run this against the JSON:

```python
import json, re

d = json.load(open('data/<slug>.json'))

def check_words(verse, label):
text = verse.get('he', '')
sides = {'left': verse.get('left', {}).get('he', ''),
'right': verse.get('right', {}).get('he', '')}
for key, w in verse.get('words', {}).items():
phrase = w['he']
side = w.get('side')
target = sides[side] if side else text
pattern = re.escape(phrase).replace(r'\ ', r'[\s​]+').replace(r'\-', r'[\s־\-]')
if not re.search(pattern, target):
print(f" MISSING in {label}: key={key!r} phrase={phrase!r}")

def check_en(verse, label):
text = verse.get('en', '')
sides = {'left': verse.get('left', {}).get('en', ''),
'right': verse.get('right', {}).get('en', '')}
for key, w in verse.get('words', {}).items():
phrase = w['en']
side = w.get('side')
target = sides[side] if side else text
n = target.count(phrase)
if n == 0:
print(f" EN MISSING in {label}: key={key!r} phrase={phrase!r}")
elif n > 1:
print(f" EN AMBIGUOUS in {label}: key={key!r} ({n} matches) phrase={phrase!r}")

for s in d['sections']:
check_words(s['primaryText'], s['id'] + ' primary')
check_en(s['primaryText'], s['id'] + ' primary')
for step in s['steps']:
if step.get('type') == 'verse-change':
check_words(step['newVerse'], s['id'] + ' verse-change ' + step['id'])
check_en(step['newVerse'], s['id'] + ' verse-change ' + step['id'])
print("done")
```

If anything prints, fix it before browser verification — most often the issue is a typo in the phrase or an English phrase that occurs twice.

## Browser verification checklist

Use `preview_start` then `preview_eval`. Confirm:

1. `document.querySelectorAll('.cr-section').length` — equals number of section *groups* (consecutive same-primary sections merge into one)
2. `document.querySelectorAll('.section-dot').length` — equals the total number of sections in JSON
3. `document.querySelectorAll('.section-divider').length` — equals total sections minus number of groups (the dividers shown for sub-sections beyond the first in each group)
4. `document.body.innerHTML.includes('undefined')` — must be `false`
5. Scroll to a card with a `highlight` array; verify `document.querySelectorAll('.word-group.highlighted, .word-group.glow, .word-group.pulse')` returns matching word keys
6. `preview_console_logs` filtered to `error` — must return nothing
Loading
Loading