<feed xmlns='http://www.w3.org/2005/Atom'>
<title>org-drill/tests, branch main</title>
<subtitle>Spaced-repetition flashcards for Org Mode
</subtitle>
<id>https://git.cjennings.net/org-drill/atom?h=main</id>
<link rel='self' href='https://git.cjennings.net/org-drill/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/org-drill/'/>
<updated>2026-05-31T16:18:49+00:00</updated>
<entry>
<title>feat: optionally show the card's outline path in the drill prompt</title>
<updated>2026-05-31T16:18:49+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-31T16:18:49+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/org-drill/commit/?id=e201ba696ed27e407d934321a5f4c5874ea4fee5'/>
<id>urn:sha1:e201ba696ed27e407d934321a5f4c5874ea4fee5</id>
<content type='text'>
A new defcustom org-drill-show-outline-path-during-drill (default off) prepends the card's ancestor path to the mini-buffer prompt, for example [Spanish &gt; Greetings], so during a drill you can see where the current card sits in the deck. With it off the prompt is byte-for-byte unchanged.

I ported this from m.galimski's fork (commit c6d0c850) and gated it behind the defcustom rather than leaving it always on. The path comes from org-get-outline-path through a small org-drill--outline-path-string helper. Tests cover the helper (nested and top-level), the prompt with the switch on and off, and the default value.
</content>
</entry>
<entry>
<title>feat: add statistics dashboard CSV export and docs</title>
<updated>2026-05-31T15:46:18+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-31T15:46:18+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/org-drill/commit/?id=27385697fbf60e3319a598bb509197f5579c9405'/>
<id>urn:sha1:27385697fbf60e3319a598bb509197f5579c9405</id>
<content type='text'>
org-drill-statistics-export-csv, bound to e in the dashboard and now implemented, writes three files into a chosen directory honoring the active scope and range: sessions.csv (one row per recorded session), cards.csv (one row per drill card in scope with its scheduling properties and computed status), and daily.csv (per-day reviews, passes, fails, pass percent, and duration). Fields are quoted per RFC 4180 by a small csv-quote helper, since csv-mode isn't a dependency. This is the dashboard's last piece, step 3 of the spec.

The row builders for the three views are pure and unit-tested with deterministic fixtures, and csv-quote covers the comma, quote, and newline cases. I documented the dashboard and the export in org-drill.org (a new section with the keymap, the CSV columns, and the settings table) and added a feature bullet to the README.

I also removed the now-redundant declare-function forward reference for export-csv. It named this file as the source, so once the real defun landed the byte-compiler counted the function twice and warned.
</content>
</entry>
<entry>
<title>feat: add the org-drill statistics dashboard renderer</title>
<updated>2026-05-31T13:35:16+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-31T13:35:16+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/org-drill/commit/?id=26cc4472dea261a1ad13fbee8fb6a91b019f77bb'/>
<id>urn:sha1:26cc4472dea261a1ad13fbee8fb6a91b019f77bb</id>
<content type='text'>
Step 1 shipped the session-log data layer. This is the renderer on top of it.

org-drill-statistics opens a read-only dashboard with five sections: an overview (card counts plus a last-session recap), trends (reviews-per-day and pass-rate-per-day quadrant-block sparklines over the trend window, plus a 12-week table), a quality histogram, a needs-attention view (leech candidates, long-overdue, and forgotten-new cards), and a 7-day forecast counted from SCHEDULED dates. A buffer-wide filter (scope, range, algorithm) sits in the header and cycles with s/r/a. The other keys are q to bury, g to refresh, e for the CSV-export hook that lands next, and RET to follow the card link at point.

The aggregation math lives in pure helpers (day-bucketing, sparkline scaling, weekly aggregates, the histogram, the attention selectors, forecast bucketing). The render helpers are thin string formatters over them, so the logic is unit-tested independently of the UI. New defcustoms tune the views: org-drill-statistics-trend-days, -forecast-days, -attention-row-limit, and -leech-quality-threshold.

I added require 'calendar for the Monday week-start arithmetic in the weekly aggregates. CSV export and the manual and README entries are the step-3 follow-on.
</content>
</entry>
<entry>
<title>feat: add org-drill-treat-headline-as-card-p for empty-bodied cards</title>
<updated>2026-05-31T11:53:06+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-31T11:53:06+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/org-drill/commit/?id=bafa281b9c0b3ccc78b4a8420a817662d50ca86f'/>
<id>urn:sha1:bafa281b9c0b3ccc78b4a8420a817662d50ca86f</id>
<content type='text'>
A drill entry with an empty body is skipped unless its card type opts into empty bodies via the DRILL-EMPTY-P slot of org-drill-card-type-alist. That left no global way to drill headline-only items, or hierarchical-notes decks where the heading is the question and the answer lives in child entries (upstream #30, #41).

I added org-drill-treat-headline-as-card-p, default nil so existing decks are unchanged. When it's on, the empty-skip gate in org-drill--entry-empty-and-not-empty-friendly-p short-circuits, so every empty-bodied entry is drilled with its heading as the question regardless of card type. I added the safe-local-variable booleanp declaration alongside the other booleans and documented the switch in org-drill.org.

Tests pin the predicate and the classify-status outcome both on and off, and confirm the per-card-type DRILL-EMPTY-P path stays independent of the new switch.
</content>
</entry>
<entry>
<title>fix: detect #+FILETAGS decks so org-drill-mode auto-enables</title>
<updated>2026-05-31T03:12:23+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-31T03:12:23+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/org-drill/commit/?id=4fae47ee8c268a4f530f60996741a7871d096b13'/>
<id>urn:sha1:4fae47ee8c268a4f530f60996741a7871d096b13</id>
<content type='text'>
org-drill-buffer-has-cards-p only scanned for a per-heading :drill:/:leitner: tag, so a deck tagged through #+FILETAGS: had no match and org-drill-mode never auto-enabled. Those files opened without cloze highlighting.

I extended the predicate to also scan #+FILETAGS: lines, handling both the space-separated and colon-delimited syntaxes, with [: \t] boundaries so a value like drilldown can't false-match drill. The inherited-top-level-tag case already worked, since the ancestor heading line carries the literal tag and the per-heading scan catches it.

Tests cover filetag-only decks (space, colon, leitner), the inherited-top-level lock, the substring boundary, and auto-enable on a filetag-only buffer.
</content>
</entry>
<entry>
<title>docs: relocate v0 design specs to docs/design/</title>
<updated>2026-05-28T07:09:11+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-28T07:09:11+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/org-drill/commit/?id=636c18341c1f9131bfabdd547cd60797d844a601'/>
<id>urn:sha1:636c18341c1f9131bfabdd547cd60797d844a601</id>
<content type='text'>
I moved both v0 design specs out of working/ and into docs/design/. That's the conventional permanent home for project documentation, where engineers will look during implementation. working/ is meant for transient in-progress artifacts that file away once the work ships, and these specs are long-lived design docs that don't fit that contract.

Files moved:
- working/stats-dashboard/stats-dashboard.org → docs/design/stats-dashboard.org
- working/fsrs-spec/fsrs-spec.org → docs/design/fsrs-spec.org

The git rename detection picked both up, so file history follows the move.

I also dropped the stale /docs entry from .gitignore. The Makefile doesn't write to docs/ and nothing else references it as a build output, so the ignore was inherited cruft that would have silently dropped any tracked file under docs/.

I updated path references in seven spots: three docstring/comment refs in org-drill.el, one in tests/test-org-drill-session-record.el (the Commentary block), and three inside the specs themselves. Two refs in fsrs-spec.org now point at the correct location for its defcustom docstring and option description. One in stats-dashboard.org's References section points at the sister spec.

Full make test-unit green. eask compile clean.
</content>
</entry>
<entry>
<title>feat: persist a session log for the stats dashboard</title>
<updated>2026-05-28T06:36:08+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-28T06:36:08+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/org-drill/commit/?id=e373ba9e6dc1b288946ce57e3f29bf5372126dee'/>
<id>urn:sha1:e373ba9e6dc1b288946ce57e3f29bf5372126dee</id>
<content type='text'>
I added the persist + recording layer that the future stats dashboard reads, plus the v0 design spec at working/stats-dashboard/stats-dashboard.org that scoped the work. Every completed (non-suspended) drill session now appends one org-drill-session-record to a persisted org-drill-session-log via persist-defvar, mirroring the SM5-matrix pattern that's already in place. A new sibling defgroup org-drill-statistics groups the dashboard's customs.

The record carries: start-time, end-time, scope, algorithm, qualities (as a vector), pass-percent, new-count, mature-count, failed-count, cram-mode. Scope and algorithm are captured at session-start time onto two new org-drill-session slots (scope-at-start, algorithm-at-start) so a mid-session defcustom flip doesn't misrepresent what was actually drilled. Both org-drill--prepare-fresh-session and the org-drill-again resume path populate the slots.

I extracted pass-percent into org-drill--compute-pass-percent and call it from both org-drill-final-report and org-drill-session-record-from-session, so the user-visible report and the persisted record can't drift on the rounding or the failure-quality threshold.

I wrapped recording in condition-case at the call site, not ignore-errors. Any recorder bug or persist-save IO failure gets messaged so silent data loss leaves a forensic trail.

Corrupt-load recovery follows the SM5 path's condition-case fallback and adds an org-drill--session-log-quarantine helper that renames the bad file to a .corrupt-YYYY-MM-DDTHHMMSS sibling so the next save doesn't overwrite it. The seconds-granularity suffix prevents a same-day double corruption from clobbering the earlier quarantine. The helper depends on persist--file-location, an internal symbol guarded behind fboundp and documented inline.

The spec at working/stats-dashboard/stats-dashboard.org is ratified, with all 10 originally-open decisions resolved in the Ratified Decisions table at the bottom: persist-defvar, warn-and-rename corrupt-load recovery, quadrant-block sparklines, a single buffer-wide filter, CSV with proper quoting, sync dashboard open, defer aborted-session recording, the q/g/e/s/r/a/RET keymap, leech-quality threshold default 2.5, and a sibling org-drill-statistics defcustom group. The remaining work (dashboard renderer, CSV export, docs) lands in follow-up commits.

I followed TDD throughout. 24 tests in tests/test-org-drill-session-record.el cover struct construction, the shared pass-percent helper, the builder including scope/algorithm capture and mature-count summing, log appending with newest-first ordering and persist-save call-through, the symbol-bound smoke check, the quarantine rename plus its seconds-granularity contract, and the end-message hook (records on normal completion, skips on suspend, logs to message on recorder error). Full make test-unit green. eask compile clean.
</content>
</entry>
<entry>
<title>feat: change default scheduling algorithm to simple8 with ADR</title>
<updated>2026-05-28T03:32:47+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-28T03:32:47+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/org-drill/commit/?id=ed27738341654ea1ae9d6f8b9a7f51e3919456a5'/>
<id>urn:sha1:ed27738341654ea1ae9d6f8b9a7f51e3919456a5</id>
<content type='text'>
Closes upstream #46. I changed the default for org-drill-spaced-repetition-algorithm from sm5 to simple8. The defcustom now carries an ADR-style comment recording the reasoning: simple8 gives most of sm5's per-user adaptation value via its per-card difficulty learning, without sm5's dependency on a persisted optimal-factor matrix that can rot (the upstream #45 fragility), and it adjusts intervals for early or late reviews, a real-world concern sm2 and sm5 don't address.

Existing users with the option set in their config see no change. Existing users on the previous default get simple8 on their next session, with no state migration. Simple8 reads what it can from the SM-style item-data and carries on. Sm5's persisted optimal-factor matrix stays on disk and is available if the user switches back.

I added a test that pins the default value, so an accidental flip surfaces in CI.
</content>
</entry>
<entry>
<title>refactor: take card-state in org-drill-determine-next-interval-simple8</title>
<updated>2026-05-28T02:47:19+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-28T02:47:19+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/org-drill/commit/?id=e45fec6778c1e483a73fb0f3652435f13b223f79'/>
<id>urn:sha1:e45fec6778c1e483a73fb0f3652435f13b223f79</id>
<content type='text'>
Stage 5 of #147, closing the scheduler migration. simple8 now takes (state quality &amp;optional delta-days) instead of seven positional args, binding the recall fields from the struct at the top so the algorithm body is unchanged. simple8 doesn't use ease, so the binding skips that slot.

Both call-site branches collapse to (state quality [delta-days]), dropping the per-branch accessor unpacking. The testutil adapter test-scheduler--call-simple8 keeps the simple8 test calls a one-symbol rename per site. One direct simple8 call in tests/test-org-drill-prompt-and-format-helpers.el now uses the new struct API inline.

With this stage landed, all three schedulers, the item-data round-trip, and every test caller go through the org-drill-card-state struct, finishing #147.
</content>
</entry>
<entry>
<title>refactor: take card-state in org-drill-determine-next-interval-sm5</title>
<updated>2026-05-28T02:40:43+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-28T02:40:43+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/org-drill/commit/?id=a2a7471f88a8c0f5c710d5ffb90511fc54b432d7'/>
<id>urn:sha1:a2a7471f88a8c0f5c710d5ffb90511fc54b432d7</id>
<content type='text'>
Stage 4 of #147. sm5 now takes (state quality of-matrix &amp;optional delta-days) instead of nine positional args, binding the recall fields from the struct at the top so the algorithm body is unchanged. Both call-site branches pass the state they already hold, dropping the per-branch accessor unpacking.

The testutil adapter test-scheduler--call-sm5 keeps the sm5 test calls a one-symbol rename per site. I also kept the return as the existing list, matching the stage-3 refinement: the goal is reducing the input signature, and changing the return shape would force the shared return-extractors and every return-read to change for no real gain.

Also folds in two stage-3 follow-ons I missed when sm2 landed: a direct sm5 call in tests/test-org-drill-small-branch-coverage.el now uses the new struct API inline, and five direct sm2 calls in the simple-workflow integration test now go through the testutil adapter (the integration file picks up the testutil-scheduler require). Caught by running make test-integration this stage, which I should have run on the sm2 stage.
</content>
</entry>
</feed>
