Skip to content

Add Dirshu Daf HaYomi B'Halacha daily learning schedule - #18

Merged
mjradwin merged 16 commits into
mainfrom
claude/dirshu-daf-halacha-schedule-u6v5b8
Aug 23, 2026
Merged

Add Dirshu Daf HaYomi B'Halacha daily learning schedule#18
mjradwin merged 16 commits into
mainfrom
claude/dirshu-daf-halacha-schedule-u6v5b8

Conversation

@mjradwin

Copy link
Copy Markdown
Member

Adds support for Dirshu's Daf HaYomi B'Halacha, a daily Mishnah Berurah learning program that covers one amud Sunday through Thursday with review (chazarah) on Friday and Shabbat.

Summary

This PR introduces a complete implementation of the Dirshu Daf HaYomi B'Halacha schedule as a new daily learning calendar. Unlike the perpetual cycles elsewhere in the package, this is a finite table transcribed from Dirshu's printed schedules, covering cycle 3 from February 20, 2022 through August 31, 2027.

Key Changes

  • Core schedule data (src/dirshuDafHalacha.json): 1,443 daily readings with amud metadata extracted from Dirshu's published luach booklets and calendars
  • Base implementation (src/dirshuDafHalachaBase.ts): Lookup function and constants for the non-perpetual schedule with clear documentation of its finite nature
  • Event class (src/DirshuDafHalachaEvent.ts): Rendering and formatting for display, including Sefaria URL generation and Hebrew gematriya references
  • Registration (src/dirshuDafHalacha.ts): Integration with the DailyLearning registry
  • Comprehensive test suite (test/dirshuDafHalacha.spec.ts): 20 tests covering cycle boundaries, amud tracking, and event rendering
  • Extraction pipeline (tools/dirshu-luach/): Python and Node.js tools to extract and normalize readings from multiple source formats (English booklets, Hebrew luachs, wall calendars, spreadsheets)
  • Source transcriptions (tools/dirshu-luach/transcriptions/): Human-readable markdown audit trail of all extracted data, enabling verification against printed sources
  • Documentation (dirshuDafHalacha.md): Detailed working notes on schedule construction, data sources, and maintenance procedures

Implementation Details

  • The schedule is stored as a dense JSON array indexed by learning day (0–1442), with amud metadata and special handling for review days
  • Multiple extraction tools handle different source formats: tabular English booklets, prose Hebrew luachs with grid layouts, and wall calendars with varying column positions
  • A normalization pipeline converts prose readings ("מסעיף ג' עד סעיף ו'") to the siman:seif model used by the JSON
  • Transcription files serve as an audit trail, allowing any discrepancy in the JSON to be traced back to its printed source
  • Localization strings added for Hebrew and Ashkenazi transliteration

The schedule returns null for dates outside its range, making the finite nature explicit to callers.

https://claude.ai/code/session_01QbE51eFKgcRHPai3t8mUDC

claude and others added 16 commits August 23, 2026 15:15
Dirshu's daily Mishnah Berurah program: one amud of the Dirshu edition
on each of Sunday through Thursday, with Friday and Shabbat reviewing
(chazarah) that week's five days. Yom Tov does not interrupt it.

Readings are transcribed from the two published Dirshu luach booklets
(2024 and 2025 editions) and cover 11 June 2024 through 29 August 2026;
dates past the last booklet return null. Each entry is a Shulchan Arukh,
Orach Chayim siman:seif reference, so events link to Sefaria there --
Sefaria indexes Mishnah Berurah itself by se'if katan, which is a
different numbering.

The daf/side of the Dirshu edition is derived arithmetically rather than
stored: the amud advances by exactly one every learning day, restarting
at 2a with each volume of the edition.

All 578 transcribed learning days round-trip against the booklets, as do
114 of the 115 printed weekly review ranges (the 2025-04-12 row in the
luach prints its own week's Thursday start instead of its end).
CLAUDE.md captures what was learned deriving this schedule so a resumed
session does not rediscover it: that the two booklets are consecutive
segments of one cycle rather than two cycles, the Sunday-Thursday timing
rule, that the references are Shulchan Arukh rather than Mishnah Berurah
numbering, the two PDF text-layer traps, which sources are reachable, and
the open coverage question that is holding this back from merge.

tools/dirshu-luach/extract_luach.py consolidates the throwaway extraction
scripts into one pipeline that reproduces src/dirshuDafHalacha.json
byte-identically from the two booklets, validates every invariant, and
allowlists the two known-benign diagnostics so new booklets can be folded
in without rebuilding the tooling.

CLAUDE.md documents one in-progress branch and should be removed before
this ever merges to main.
CLAUDE.md §6/§7 said dafhalacha.com was fully captcha-walled; a re-probe
found the wall down (curl pulled the 17.9 MB booklet, REST API answered).
Record that reachability is session-dependent, that the media library only
goes back to 2024-06-10 regardless (no pre-2024 booklets hosted), that all
three web-archive routes are blocked here, and promote the archived
limud-schedule page as the strongest lead for the missing simanim 1-241.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three Hebrew luachs (5780/81, 5781, 5782) answer the question §7 had flagged
as unanswerable from the English booklets alone. luach57821 straddles the
changeover and shows it outright: cycle 2's last learning day is Thu
2022-02-17, reading "from 696:8 until the end of the Mishnah Berurah", and
cycle 3 opens Sun 2022-02-20 "from the beginning of siman 1". In cycle 3's own
Sunday-Thursday counting cycle 2's final day sits at index -1, so the cycles
run back-to-back with no gap and no restart offset, and a future % cycleLen is
sound once the table is complete.

The Sunday-Thursday learning / Friday-Shabbat chazarah rule also replays over
cycle 2 with zero mismatches across the 290 days of the 5780/81 booklet, so
the timing half of the pattern is now confirmed across two cycles.

A reader's spreadsheet transcription independently reproduces all 264
overlapping learning days -- reference, daf, side and printed page -- and
carries 10 days past the last booklet, taking the horizon to 2026-09-12.
extract_luach.py grows an --xlsx flag that merges such a source and
cross-checks the overlap rather than trusting it.

Coverage is still the blocker: cycle-3 indices 156-601 (simanim ~53-242) are
missing, so the 0-155 segment extracted from luach57821 is deliberately NOT
spliced into the dense array -- a hole would be worse than a late start.
extract_hebrew_luach.py and date_hebrew_luach.mjs are committed so that
segment can be normalised in one pass when the 5783/5784 luachs arrive.
One file per source, one table per page of the original, so a value in
src/dirshuDafHalacha.json can be traced back to the page it was read from
and disputed without the PDFs in hand. Deliberately redundant with the
JSON: the JSON is the schedule, these are the audit trail.

The English booklets and the spreadsheet carry the parsed reading beside
the printed cell, so a transcription error shows up as a mismatch between
the two columns. The Hebrew luachs are reproduced verbatim, since their
prose readings are not yet normalised into the siman:seif model.

Reworking extract_hebrew_luach.py to locate rows on a positional grid,
anchored on each panel's first date and stepped by the row pitch, fixes
two things the previous nearest-date matching got wrong. Rows whose date
cell lands at a stray y are no longer lost -- that had been dropping the
single most important row in the corpus, cycle 2's last learning day
"from 696:8 to the end of the Mishnah Berurah". And a chazarah cell,
merged across the Friday and Shabbat rows and so sitting half a row low,
is now attributed to the Friday. The 5782 booklet went from 2 rule
mismatches to 0 and the 5781 from 97 to 21; the 5780/81 booklet stays at 0.

Lives under tools/dirshu-luach/ rather than docs/, which is typedoc's
gitignored output directory.
…tions

Only the reading carried both forms before, and the amud carried opposite
halves in different files -- decoded-only in the English booklets, printed-only
in the spreadsheet. Now the Hebrew date, the amud and the reading each sit
beside their decoded value, so a misread gematriya shows up as a mismatch
between two adjacent cells instead of needing a diff against the JSON.

The Hebrew luachs gain a decoded date and a column listing the simanim their
prose names, which is a reading aid rather than the normalised range.

Decoded dates come from @hebcal/hdate via the new hebrew_dates.mjs, so the
library the calendar itself uses stays the single authority rather than a
second implementation in Python.

Pairing the columns immediately earned its keep: 21 printed dates in the
tashpa booklet disagreed with the row they had been attached to, all by a
constant offset, which is that booklet's stray date runs landing on the wrong
row. make_transcripts.py now decodes each printed date and falls back to the
computed one where they disagree, so no date is shown on a row it does not
belong to. Every date shown as printed has been verified to decode to the
value beside it: 1,584 checked, 0 mismatches, 166 flagged as reconstructed.
A third source artifact, distinct from the dafhalacha.com booklets and the
Hebrew pocket luachs: one row per day with a Gregorian date, readings mostly
as prose, and a daf halacha column at a different x in every year.

Two joining rules matter and cost real time to find. The date column carries
M/D/YYYY, MM/DD/YY, and at least one "8/18//2023" typo. And each quote mark
arrives as its own text run, where whether a space belongs after it depends on
which mark it is: a gershayim sits inside a Hebrew numeral, so קנ + " + ט is
קנ"ט = 159, while a geresh closes one, so תכ"ו + ' + א is 426 then 1 -- running
those together would read as the single number 427.

Validated against the 5787 calendar, whose 20 learning days overlapping the
shipped schedule match it exactly on siman/se'if, daf, side and printed page.
It carries 273 further learning days beyond the current horizon. The 5784
calendar agrees too, expressing the same readings as prose.

Years that transpose the table so each day is a column rather than a row
(5781, 5783) are not handled yet.
5782, 5784 and 5787 as Markdown, one table per page, with each Hebrew value
beside its decoded form like the existing transcriptions.

Two fixes were needed to read them. The 5782 calendar emits each reading as a
single run in visual rather than logical order, so segments came out back to
front; a reading always opens with a Hebrew word and never with a quote mark,
which makes a leading quote a reliable signal to reverse them back. And the
decoder has to recognise prose before parsing, because every Hebrew letter run
looks like a numeral to the gematria -- "מסימן קנ\"ט …" was decoding as
"200:159-220:4" rather than naming siman 159.

Where a calendar prints the tabular amud and range (5787) both are decoded in
full; where it prints prose, the Decoded column lists only the simanim named,
which is a reading aid rather than the normalised range.
The 5783 calendar lays each month out as a grid where a day is a column and
each program is a band of rows. Within a day's column the reading wraps over
many short lines, and after a wide vertical gap the next program's band begins,
so a day's reading is every line in its column between the date row and that
gap -- top to bottom, right to left within each line. Locating the gap per page
rather than hardcoding a band is what makes it work across pages whose readings
wrap to different depths.

384 days, contiguous, zero Sunday-Thursday rule mismatches, simanim running
42 -> 162. Its overlap with the Hebrew 5782 luach agrees: where the luach
carries the siman forward implicitly ("מסעיף ה' עד תחילת סימן מ''ד") this
calendar names it ("מסימן מ''ג סעיף ה' עד תחילת סימן מ''ד").

That was the last unreadable source. Cycle-3 learning days 0-1465 are now
covered contiguously, with overlap at every join -- the 446-day hole is closed.
The booklets print a reading as the se'ifim it touches ("245:3-245:5"); the
luachs and wall calendars print its start and end POINTS ("מסעיף ג' עד סעיף
ו'"). Those are not the same thing -- that reading runs up to the start of
se'if 6, so it touches 3 to 5, and the next day opens at 6. Getting it backwards
would shift six hundred days by one se'if, so every rule is calibrated against
the 79 days where the 5784 calendar and the shipped booklets overlap and the
answer is already known.

Two subtleties the calibration caught. A Hebrew numeral must not be allowed to
run on, or the citation right after it is swallowed -- "סעיף א'"ואם היתה"" was
decoding as a single number, 48. And a siman is written bare only when the prose
says the reading both opens at its start and runs to its end; covering se'if 1
through the last se'if is not sufficient, since a day can open at a siman's
start and stop halfway through its only se'if, which the booklets still print
as "242:1".

68 of the 79 calibration days now reproduce the shipped values exactly. The
other 11 are one fault rather than eleven: three rows are missing from the 5784
PDF's text layer, and since a day whose prose omits its siman inherits it from
the day before, a lost row leaves the carry stale. Merging sources by date
before normalising fixes that -- all 602 cycle-3 learning days in the former gap
have prose from at least one source, and 181 of them from two or more.
src/dirshuDafHalacha.json now holds 1443 consecutive learning days -- cycle-3
indices 0 to 1442, from 2022-02-20 to 2027-08-31 -- assembled by
build_schedule.py from the Hebrew luachs, the transposed 5783 grid, the 5784
and 5787 wall calendars, the dafhalacha.com booklets and a reader's
spreadsheet. Every join overlaps and 181 days carry two or more independent
sources. The previous 578-entry array is preserved byte-for-byte at indices
602-1189, so nothing already verified has moved.

Three fixes the rebuild forced out, each of which had been corrupting readings
silently:

- NUM is an alternation, and interpolating it bare let its branches escape
  whatever group it was placed in. The se'if-katan pattern was therefore
  matching any one to four Hebrew letters anywhere, which swallowed the se'if
  numbers and flattened the first week to "1:1, 1:1, 1:1".
- "until the start of siman Y" ends at the end of Y-1, which is not always the
  siman the reading began in: "מסעיף ט"ו עד סימן רמ"ב" skips over siman 241
  entirely if you stop at the opening siman.
- The gershayim inside a numeral is written " in some sources and as two
  apostrophes in others, so י''א was reading as 10 rather than 11.

daf/side are now given only from index 602, where the first source that prints
a page begins; earlier years have the reading but not the page. One day covers
more than a single amud -- 2026-11-15 prints "ל. לא." -- and every amud after
it shifts by a full daf, which is recorded in the JSON rather than left to skew
silently.

The array stops at index 1442 because the next learning day prints a volume's
הקדמה, an introduction rather than a siman reference, which the model cannot
express. The cycle still does not reach its end, so % cycleLen remains out of
reach; CLAUDE.md §7 records the alignment experiment that could settle it.
Aligning the normalised cycle-2 readings against cycle 3 gives one sharp signal,
at a cycle length near 1804 learning days: 23 consecutive days match exactly,
compound ranges included, against roughly zero at every other offset. The two
cycles are plainly the same schedule.

But the match then breaks. From cycle-3 index 1409 on, cycle 2 lines up with
cycle 3 shifted by one day: cycle 3 spends two days on 503:1-504:1 and
504:1-504:2 where cycle 2 spent one on 503:1-504:2. So cycle 3 holds at least
one learning day cycle 2 did not, which is what re-typesetting a volume between
cycles would do to the amud breaks.

This closes the question the branch has been carrying since the start, and
closes it the other way: the schedule cannot be made perpetual by repeating one
cycle's table. Recorded in CLAUDE.md §7 so nobody adds % cycleLen later.
main dropped the local isHebrewLocale from common.ts in favour of
Locale.isHebrewLocale, which takes the caller's locale directly rather than a
lower-cased copy, and moved from the greg.* namespace to the bare greg2abs.
It also added a LearningDate alias for the HDate | Date | number union.

Adopts all three here, so this schedule reads like the ones around it.
The wall calendars print a one-letter day of the week to the right of the
Hebrew date -- alef through vav for Sunday through Friday, shin for
Shabbat. extract_calendar.py took everything within a fixed 90pt window
right of the Gregorian date as the printed Hebrew date, and where that
letter fell inside the window it was swallowed into it:

  * 5787 prints the day letter 79pt right of the date, inside the window,
    so every row carried it and no row's Hebrew date decoded -- the
    reported symptom, a blank "Hebrew date" beside a printed `א כ"ג תשרי`;
  * 5784 prints it at 91-94pt, straddling the cutoff, so 63 of 412 rows
    leaked it and the rest did not;
  * 5782 prints no Hebrew date at all, so its entire "Hebrew date" column
    was in fact the day letter.

Split the two by content rather than by coordinate, and keep the letter as
its own field. Rendering it decoded beside the weekday computed from the
row's Gregorian date turns a silent leak into a cross-check: all 1,237
printed letters agree, and the two rows whose letter did not survive
extraction are flagged.

Also regenerate the Gregorian -> Hebrew map over the full span of the
sources (2019-08-01 to 2027-12-31); it had been built to 2026-10-01, which
is the other reason the 5787 rows past that date showed no Hebrew date.

The reading column is untouched: re-extracting all three calendars with the
old code and the new gives byte-identical `text`, so src/dirshuDafHalacha.json
is unaffected. Full suite still 186 passed.
Put the reason this calendar cannot be computed into the file it applies
to, as a header on src/dirshuDafHalachaBase.ts: the content mapping is a
lookup table because page breaks in a printed edition are not a rule, and
-- the part that would otherwise be rediscovered the hard way -- the cycles
do not repeat exactly, so `% cycleLen` would drift. It carries the worked
example: cycle 3 spends two learning days (503:1-504:1, 504:1-504:2) where
cycle 2 spent one (503:1-504:2), which is why cycle 2 lines up one day
shifted from cycle-3 index 1409 on.

Also split the notes. CLAUDE.md was a handoff for one in-progress branch --
branch name, two PDFs, a status header -- none of which survives a merge.
It is now a normal repo guide: layout, the three-module split every schedule
follows, the wiring steps a new calendar needs, and the conventions that
have teeth (generated files, the differential checksum suite, prettier
churn). The Dirshu-specific material moves to dirshuDafHalacha.md, updated
for all ten sources rather than the original two, with a pointer from
CLAUDE.md.
@mjradwin
mjradwin merged commit 7c82f46 into main Aug 23, 2026
5 checks passed
@mjradwin
mjradwin deleted the claude/dirshu-daf-halacha-schedule-u6v5b8 branch August 23, 2026 15:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants