diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-21 06:42:54 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-21 06:42:54 -0500 |
| commit | 24f8c5aac8873837824ca6d61e566e3e24c96ad5 (patch) | |
| tree | 40336f31bf714ab8fc39596ce5324dda035467fc /docs | |
| parent | e201ba696ed27e407d934321a5f4c5874ea4fee5 (diff) | |
| download | org-drill-24f8c5aac8873837824ca6d61e566e3e24c96ad5.tar.gz org-drill-24f8c5aac8873837824ca6d61e566e3e24c96ad5.zip | |
docs: reflow spec tables, fix docstrings, add code-scan findings report
The two design-spec tables now follow the org table standard. I shortened three docstring first lines past the 80-column byte-compile warning and corrected the two visible-cloze face docstrings, which carried the hidden-cloze wording verbatim. The new findings report (docs/design/2026-07-21-code-scan-findings.org) records a full scan of org-drill.el: 22 graded bugs and 5 refactor groups with file:line evidence and fix directions. The gitignore gains the task-archive path so the archive stays untracked like the todo file.
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/design/2026-07-21-code-scan-findings.org | 141 | ||||
| -rw-r--r-- | docs/design/fsrs-spec.org | 209 | ||||
| -rw-r--r-- | docs/design/stats-dashboard.org | 118 |
3 files changed, 378 insertions, 90 deletions
diff --git a/docs/design/2026-07-21-code-scan-findings.org b/docs/design/2026-07-21-code-scan-findings.org new file mode 100644 index 0000000..85b7ea6 --- /dev/null +++ b/docs/design/2026-07-21-code-scan-findings.org @@ -0,0 +1,141 @@ +#+TITLE: Code Scan Findings — org-drill.el +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-21 + +* About + +Full-file bug and refactoring scan of =org-drill.el= (6003 lines), run 2026-07-21 during the overnight sentry session. Method: mechanical metrics in the main thread (function lengths via sexp parsing, parameter counts, literal duplication, dead-code grep), then four scoped read-only review agents over line regions 1-1380, 1380-2440, 2440-3660, 3660-6003, each required to quote verbatim code for every claim. The highest-impact claims were re-verified against the source by hand before this report was written. Line numbers refer to the tree at commit =26091e7= (sentry/2026-07-21-ratio). + +Severity grading uses the severity × frequency matrix in the global todo-format rule. Both ERT suites (unit + integration) pass at scan time; the bugs below live on paths the suites don't exercise (interactive flows, legacy data formats, default-config live use). + +* Bugs + +** [A] BUG-1 — Statistics dashboard scans its own buffer, not the deck (VERIFIED) +=org-drill.el:5730-5735= (=org-drill-statistics--render=). The dashboard body — overview counts, attention tables, forecast, cards CSV rows, all of which call =org-drill-map-entries= — is computed inside =with-current-buffer buffer= (the =*Org Drill Statistics*= buffer). With the default =org-drill-scope= of ='file=, =org-drill-current-scope= returns nil ("current buffer"), so every card scan traverses the empty dashboard buffer. Result with default config: Total cards 0, all attention tables empty, forecast all zeros, empty cards.csv — regardless of deck contents. Bonus crash: cycling scope to ='directory= calls =(file-name-directory (buffer-file-name))= with nil in the file-less dashboard buffer → wrong-type-argument. +Fix direction: capture the originating deck buffer (or resolve the scope to a concrete file list) in =org-drill-statistics= before rendering, and run all card traversals in that context. +Grading: Major severity (new feature non-functional in default config, no data loss) × every dashboard user, every time = P1 = [#A]. + +** [B] BUG-2 — copy-entry fallback pastes a destination subtree instead of the source card (VERIFIED) +=org-drill.el:3888-3901= (=org-drill-copy-entry-to-other-buffer=). =org-copy-subtree= runs before =switch-to-buffer dest=; when =org-find-olp= fails (path only partially exists in DEST), the recursive fallback re-executes the whole function while DEST is current — re-copying whatever subtree point sits on in DEST. DEST gains a stripped "imported" copy of one of its own subtrees; the source card is silently lost. +Trigger: =org-drill-merge-buffers= where SRC has a new item whose outline path only partially exists in DEST. +Fix direction: copy once up front and pass the text (or keep SRC current); use =with-current-buffer= instead of =switch-to-buffer=. +Grading: Critical severity (deck corruption + silent card loss) × rare edge = P2 = [#B]. + +** [B] BUG-3 — Dashboard card links use an unregistered link type (VERIFIED) +=org-drill.el:5280-5281= emits =[[org-drill-card:POS][...]]= links, but no =org-link-set-parameters= call exists anywhere in the package (grep confirmed). RET (bound to =org-open-at-point=) treats the type as a fuzzy link and signals "No match" — dashboard navigation never works. A bare POS also cannot identify the buffer under multi-file scopes. +Fix direction: register the type with a :follow handler and encode buffer/file identity alongside POS. +Grading: Major × every dashboard user attempting navigation = P2 = [#B]. Fix alongside BUG-1. + +** [B] BUG-4 — Leitner rating 1 on a box-0 card writes box -1; card vanishes (VERIFIED) +=org-drill.el:4495-4501= (=org-drill-leitner-rebox=). Box 0 is a valid state (capture accepts 0-5; rebox defaults an absent property to 0), but the demotion floors at box 1 only when current-box = 1, so box 0 yields "-1", which matches neither the unboxed nor the boxed branch of =org-drill-map-leitner-capture= — the card silently leaves the system. Related: the prompt says "0 - Completely forgot. (Back to Zero)" while the ?0 branch writes box "1". +Fix direction: clamp the demotion at the minimum box; reconcile the ?0 branch with its prompt. +Grading: Critical (silent card loss) × rare edge = P2 = [#B]. + +** [C] BUG-5 — Tags key at the answer prompt reveals the answer (VERIFIED) +=org-drill.el:2096=: =(while (memq ch '(nil org-drill--tags-key)))= quotes the symbol, but =ch= is a character, so the memq never matches the tags key; after =org-set-tags-command= runs, control falls through to =(t t)= and reveals the answer instead of re-prompting. Contrast the correct =(list ...)= form at 1800-1804. +Fix: =(memq ch (list nil org-drill--tags-key))=. +Grading: Major (tag-editing at prompt broken, card prematurely revealed) × some users sometimes = P3 = [#C]. + +** [C] BUG-6 — "No longer due" skip leaves current-item set; session ends in error (VERIFIED shape) +=org-drill.el:3163=: the not-due skip branch (unlike the 'skip route at 3120) leaves =(oref session current-item)= pointing at the skipped marker, so =org-drill-entries-pending-p= stays true with empty queues; the next iteration pops nil and hits "Unexpectedly ran out of pending drill items". Trigger: last card of a session fails for the 16th time → tagged leech → under default =org-drill-leech-method= 'skip it is no longer due → session aborts with an error; the stats record and SM5 persist are skipped. +Fix: clear current-item in the skip branch (or treat a nil pop as clean exit). +Grading: Major (session record lost, error instead of report) × rare edge = P3 = [#C]. + +** [C] BUG-7 — store-item-data crashes on nil meanq/ease from legacy cards (VERIFIED shape) +=org-drill.el:1373-1378=: =org-drill-round-float= is applied to =meanq= / =ease= slots that =org-drill-get-item-data= can legitimately leave nil (legacy LEARN_DATA path at 1331; property path at 1352-1353 when DRILL_AVERAGE_QUALITY / DRILL_EASE are absent) → wrong-type-argument. Reachable via =org-drill--copy-scheduling-to-marker= (get → store directly). +Fix: guard nil in the store (skip or default), or never emit nil for rated cards in get. +Grading: Major (crash) × rare (legacy-format cards) = P3 = [#C]. + +** [C] BUG-8 — Empty-string org-drill-match crashes every session (VERIFIED) +=org-drill.el:999-1003=: =(elt org-drill-match 0)= on ="\"\""= signals args-out-of-range; the defcustom's string widget makes "" easy to set via customize. Every command routed through =org-drill-map-entries= then errors. +Fix: require non-zero length before the elt (treat "" as nil). +Grading: Major (session can't start) × rare = P3 = [#C]. + +** [C] BUG-9 — SM2 e-factor floor clamps input, not output (VERIFIED) +=org-drill.el:1441-1443=: canonical SM2 clamps after the update; here the check runs before, so returned EF can dip below 1.3, and at exactly 1.3 the quality argument is ignored next call — a hard card's ratings stop mattering near the floor. +Fix: compute the update, then =(max 1.3 result)=. +Grading: Minor (scheduling drift near floor) × some users sometimes = P3 = [#C]. + +** [C] BUG-10 — Weight clamping diverges between smart-reschedule and the hypothetical path +=org-drill.el:1685-1689= vs =1734-1738=: smart-reschedule clamps the whole weighted sum at 1.0; the hypothetical-next-review-date form clamps only the delta/weight quotient (forcing ≥1 day growth). Since =org-drill-reschedule= always passes numeric days-ahead, the hypothetical formula is what actually schedules interactively — the two entry points schedule the same card differently (e.g. last 10, computed 13, weight 4 → 10.75 vs 11). +Fix: extract one shared weight-adjust helper; pick one clamp semantics. +Grading: Minor × some (weighted cards) = P3 = [#C]. + +** [C] BUG-11 — Early/late-repetition adjustment is inert in the shipped call graph +=org-drill.el:1674-1680= (+ 1726-1730): sm5's delta-days argument is never passed by either caller, and simple8's delta-days-adjusted interval is discarded whenever days-ahead is numeric — which it is for both in-tree callers. Setting =org-drill-adjust-intervals-for-early-and-late-repetitions-p= changes nothing interactively. +Fix: thread delta-days through the hypothetical path (or compute the adjustment where days-ahead is derived). +Grading: Major (documented option does nothing) × some users sometimes = P3 = [#C]. + +** [C] BUG-12 — org-drill-leitner-completed never resets (VERIFIED) +Defvar at 4316, incremented at 4525, read at 4335/4387 — no reset anywhere. It accumulates for the life of the Emacs process, so =org-drill-sm-or-leitner='s dispatch (pending > completed) increasingly favors Leitner over due SM sessions. +Fix: reset to 0 at the start of each Leitner session. +Grading: Major (wrong session dispatch) × some (Leitner users, long-lived daemon) = P3 = [#C]. + +** [C] BUG-13 — Cloze overlays leak on C-g in hide-n / hide-nth +=org-drill.el:2759= (and 2782-2784): cleanup sits in a =prog1= tail, skipped on nonlocal exit; the surrounding macros' unwind handlers delete only the hidden-text overlay category, not cloze overlays. C-g at the prompt of any multicloze card leaves [...] masks in the org buffer permanently. +Fix: unwind-protect around prompt + cleanup — falls out of the shared presenter helper (REF-2). +Grading: Minor × some = P3 = [#C]. + +** [C] BUG-14 — replace-entry-text-multi assert too weak; overlays can run past point-max +=org-drill.el:2494=: layout consumes 2 chars per replacement (needs ≥ 2N-1) but the assert requires only N; a short body at buffer end → args-out-of-range mid-presentation; mid-buffer, the overlay spills past the next heading. Odd-offset characters between one-char overlays also show through. +Fix: strengthen the assert to 2N-1 and clamp overlay ends to p-max. +Grading: Major (crash) × rare = P3 = [#C]. + +** [C] BUG-15 — show1-lastmore's uncommon branch never reveals the first piece +=org-drill.el:2849=: passes force-hide-first, guaranteeing the first piece stays hidden — but the comment says "show any item except the last" and the docstring says "any random piece". Behavior matches neither. +Fix: plain =(org-drill-present-multicloze-hide-n session -1)= for "any random piece" (or implement except-last); fix the comment. +Grading: Minor × some = P3 = [#C]. + +** [C] BUG-16 — "All time" CSV export truncates daily.csv to 90 days +=org-drill.el:5983-5984= (clamp at 5927): the "all time" preset maps days to nil, which --daily-rows replaces with =org-drill-statistics-trend-days= (90); daily.csv silently drops older days while sessions.csv covers the whole log. +Fix: derive days from the oldest record when the range is unbounded (or document the floor). +Grading: Minor × some = P3 = [#C]. + +** [C] BUG-17 — Trends renders as level-1 heading, swallowing later sections +=org-drill.el:5527-5529=: "* Trends" among "** ..." siblings — TAB-collapsing Trends hides Distribution, Attention, and Forecast. +Fix: "** Trends". +Grading: Cosmetic × every dashboard user = P3 = [#C]. + +** [D] BUG-18 — Merge leaves point moved in DEST +=org-drill.el:3977-3981=: the strip maphash has no save-excursion; after merge, point in DEST sits on the last unmatched entry. Fix: wrap in save-excursion. Cosmetic × some = P4 = [#D]. + +** [D] BUG-19 — free-markers call in prepare-fresh-session is a no-op +=org-drill.el:3511= runs on the brand-new empty session (constructed at 3634); the previous session's queue markers are never explicitly freed (GC-bounded impact). Fix: free the old session's markers before reassigning, or drop the call. Minor internal × every = [#D]. + +** [D] BUG-20 — "Drill session finished!" on quit, doubled on completion +=org-drill.el:3653=: unconditional after =org-drill-entries=, including quit/edit exits; on normal completion the same message + sit-for repeats in --show-end-message. Fix: delete; let show-end-message own it. Cosmetic × most = P4 = [#D]. + +** [D] BUG-21 — Hidden-cloze display string baked at load time +=org-drill.el:279-284=: the "[...]" display string is computed once at load from the delimiter defcustoms, while the cloze regexp is recomputed per buffer — customized delimiters mismatch the mask. Fix: compute at overlay creation. Minor × rare = P4 = [#D]. + +** [D] BUG-22 — card-type/card-tags alist :type specs don't match value shape +=org-drill.el:414-416= (and 446-447): values are lists, spec says =function= — customize edits produce malformed entries. Fix: correct the :type. Minor × rare = P4 = [#D]. + +* Refactoring opportunities + +** [C] REF-1 — Scheduler duplication: meanq update triplicated, normalize/assert prologue duplicated +=org-drill.el:1503-1506= (verbatim at 1402-1405, 1608-1610); n/ef defaulting + quality asserts duplicated across sm2/sm5. Extract =org-drill--update-meanq= + a shared state-normalize prologue. Pairs naturally with fixing BUG-9/10/11. Also: SM5 failure path writes an OF-matrix entry keyed by a next-ef the card never adopts (1520-1528) — decide and document, or move the write into the success branch. SM2's second-repetition noise table (1420-1427) carries unreachable arms given the failure boundary. + +** [C] REF-2 — Presenter boilerplate: one shared present-and-prompt helper +The latex/inline-images/hide-drawers/prompt/re-hide closing sequence is near-verbatim in six presenters (2573-2578, 2615-2620, 2643-2648, 2660-2665, 2755-2761, 2778-2784). Extract one helper with an unwind-protect — which also fixes BUG-13. The weight-validation cond is additionally triplicated across firstmore/lastmore/firstless (2817-2831, 2841-2855, 2866-2880) — extract a 'fallback/'uncommon/'common dispatcher. + +** [C] REF-3 — Statistics attention pipeline duplicated +=org-drill-statistics--render-attention= (5316-5399) re-implements the filter+sort+cap+row pipeline of the three selectors (5131-5195), which production code never calls (tests only); comparators exist in two copies each. Drive both from one (predicate, comparator) table. Also delete: dead =count= in org-drill-leitner (4354-4364), vestigial let-bindings in --render-all (5679-5681), inert same-file declare-function block (5596-5600). + +** [C] REF-4 — org-drill-get-item-data duplication +The seven-line property-path constructor appears verbatim twice (1335-1341, 1347-1353) and the virgin-state constructor twice (1343-1345, 1355-1357); the inner if re-implements the outer cond's arms. Extract =org-drill--card-state-from-properties= + a virgin-state helper. Pairs with fixing BUG-7. + +** [D] REF-5 — Small cleanups batch +- =org-drill-pop-random= macro evaluates PLACE five times (967-972) — bind once via macroexp-let2. +- =(or default nil)= → =default= in the DRILL_AVERAGE_QUALITY getter (1201-1204). +- Dead =again-entries= arm in entries-pending-p (3003) — outer or already covers it. +- DRILL_* property-name literals repeat 4-9× each — consider named constants. +- =org-drill-with-replaced-entry-text-multi= has zero in-tree callers but is public API for custom card-type authors — keep, do not remove. + +* Metrics summary + +Longest defuns (true sexp lengths): render-attention 75, org-drill 71, simple8 69, --read-rating-key 66 — all under the 100-line critical band; ten functions in the 51-100 "split recommended" band, most either schedulers (REF-1) or renderers (REF-3). No functions with 5+ positional parameters remain (card-state refactor). Byte-compile is warning-free as of =26091e7=. + +* Verification status + +VERIFIED = re-checked by hand against the source during consolidation. VERIFIED shape = quoted code confirmed present and mechanism plausible; the full trigger chain still wants a reproduction test. Unmarked = agent-reported with verbatim quotes that matched spot-checks; write the failing ERT reproduction first per the project's TDD convention before fixing any of them. diff --git a/docs/design/fsrs-spec.org b/docs/design/fsrs-spec.org index 76ec4db..0b0d9eb 100644 --- a/docs/design/fsrs-spec.org +++ b/docs/design/fsrs-spec.org @@ -78,20 +78,40 @@ Locked product decisions for v1. These resolve the design questions, but implementation is still blocked on the research prerequisites in =Status= above (source pin, equation cross-check, reference vectors). -| # | Decision | Value | -|---+----------+-------| -| 1 | FSRS version pin | FSRS-4.5 (17 parameters), with the v4.5 default tuple from the fsrs4anki algorithm wiki | -| 2 | Default parameters | See =Version pin= below — the exact v4.5 tuple | -| 3 | Forgetting curve | v4.5: =DECAY = -0.5=, =FACTOR = 19/81= | -| 4 | Quality mapping | Again iff quality ≤ =org-drill-failure-quality=; otherwise Hard (≤3), Good (4), Easy (5). At the default threshold of 2 this is 0/1/2 → Again, 3 → Hard, 4 → Good, 5 → Easy | -| 5 | State shape | =cl-defstruct org-drill-fsrs-state= | -| 6 | Result shape | =cl-defstruct org-drill-fsrs-result= | -| 7 | DRILL_CARD_WEIGHT applies to FSRS | Yes — same delta interpolation as SM/Simple8: =next = max(1.0, last + (computed − last) / weight)= on success intervals; weight 2 = more frequent review. The =Again= same-day path (interval 0) bypasses weight | -| 8 | Failure semantics | =Again= schedules same-day (=next-interval = 0=); D/S/R state still updates | -| 9 | First-FSRS-review cold-start message | No message (silent transition) | -| 10 | Malformed =DRILL_FSRS_*= behavior | User-facing error; scheduling untouched (data-loss safety) | -| 11 | =org-drill-fsrs-desired-retention= as file-local | Yes — safe-local, bounded 0 < x < 1 | -| 12 | DRILL_FSRS_* property ownership | Add to =org-drill-scheduling-properties=; undo/strip/copy/migration cover them via the existing list | +| # | Decision | Value | +|----+--------------------------------------------------+--------------------------------------------------------------| +| 1 | FSRS version pin | FSRS-4.5 (17 parameters), with the v4.5 default tuple from | +| | | the fsrs4anki algorithm wiki | +|----+--------------------------------------------------+--------------------------------------------------------------| +| 2 | Default parameters | See =Version pin= below — the exact v4.5 tuple | +|----+--------------------------------------------------+--------------------------------------------------------------| +| 3 | Forgetting curve | v4.5: =DECAY = -0.5=, =FACTOR = 19/81= | +|----+--------------------------------------------------+--------------------------------------------------------------| +| 4 | Quality mapping | Again iff quality ≤ =org-drill-failure-quality=; otherwise | +| | | Hard (≤3), Good (4), Easy (5). At the default threshold of 2 | +| | | this is 0/1/2 → Again, 3 → Hard, 4 → Good, 5 → Easy | +|----+--------------------------------------------------+--------------------------------------------------------------| +| 5 | State shape | =cl-defstruct org-drill-fsrs-state= | +|----+--------------------------------------------------+--------------------------------------------------------------| +| 6 | Result shape | =cl-defstruct org-drill-fsrs-result= | +|----+--------------------------------------------------+--------------------------------------------------------------| +| 7 | DRILL_CARD_WEIGHT applies to FSRS | Yes — same delta interpolation as SM/Simple8: =next = | +| | | max(1.0, last + (computed − last) / weight)= on success | +| | | intervals; weight 2 = more frequent review. The =Again= | +| | | same-day path (interval 0) bypasses weight | +|----+--------------------------------------------------+--------------------------------------------------------------| +| 8 | Failure semantics | =Again= schedules same-day (=next-interval = 0=); D/S/R | +| | | state still updates | +|----+--------------------------------------------------+--------------------------------------------------------------| +| 9 | First-FSRS-review cold-start message | No message (silent transition) | +|----+--------------------------------------------------+--------------------------------------------------------------| +| 10 | Malformed =DRILL_FSRS_*= behavior | User-facing error; scheduling untouched (data-loss safety) | +|----+--------------------------------------------------+--------------------------------------------------------------| +| 11 | =org-drill-fsrs-desired-retention= as file-local | Yes — safe-local, bounded 0 < x < 1 | +|----+--------------------------------------------------+--------------------------------------------------------------| +| 12 | DRILL_FSRS_* property ownership | Add to =org-drill-scheduling-properties=; | +| | | undo/strip/copy/migration cover them via the existing list | +|----+--------------------------------------------------+--------------------------------------------------------------| * Open questions @@ -145,10 +165,14 @@ The four-rating scale FSRS uses internally: | Rating | Name | Meaning | |--------+-------+------------------------| -| 1 | Again | failed recall | -| 2 | Hard | recalled with struggle | -| 3 | Good | recalled normally | -| 4 | Easy | trivial recall | +| 1 | Again | failed recall | +|--------+-------+------------------------| +| 2 | Hard | recalled with struggle | +|--------+-------+------------------------| +| 3 | Good | recalled normally | +|--------+-------+------------------------| +| 4 | Easy | trivial recall | +|--------+-------+------------------------| org-drill uses a 0–5 quality scale. The mapping (locked, see Agreed decisions #4) honors =org-drill-failure-quality= for the Again boundary, @@ -167,9 +191,13 @@ At the default threshold of 2 this is the familiar table: | org-drill quality | FSRS rating | |-------------------+-------------| | 0, 1, 2 | Again (1) | +|-------------------+-------------| | 3 | Hard (2) | +|-------------------+-------------| | 4 | Good (3) | +|-------------------+-------------| | 5 | Easy (4) | +|-------------------+-------------| A custom =org-drill-failure-quality= shifts the Again boundary the same way it does for the SM-family schedulers: a threshold of 3 sends quality 3 to @@ -183,13 +211,18 @@ could let users remap the success grades; v1 keeps them fixed. Per-card persisted state lives in four properties plus the existing =DRILL_LAST_REVIEWED=: -| Property | Type | Meaning | -|-------------------------+--------+----------------------------------------| -| =DRILL_FSRS_STABILITY= | float | =S=, the memory-stability estimate | -| =DRILL_FSRS_DIFFICULTY= | float | =D=, the difficulty estimate (1..10) | -| =DRILL_FSRS_REVIEWS= | int | review count under FSRS (≥ 0) | -| =DRILL_FSRS_LAPSES= | int | failures (rating = Again) under FSRS | -| =DRILL_LAST_REVIEWED= | time | reused from the existing SM property | +| Property | Type | Meaning | +|-------------------------+-------+--------------------------------------| +| =DRILL_FSRS_STABILITY= | float | =S=, the memory-stability estimate | +|-------------------------+-------+--------------------------------------| +| =DRILL_FSRS_DIFFICULTY= | float | =D=, the difficulty estimate (1..10) | +|-------------------------+-------+--------------------------------------| +| =DRILL_FSRS_REVIEWS= | int | review count under FSRS (≥ 0) | +|-------------------------+-------+--------------------------------------| +| =DRILL_FSRS_LAPSES= | int | failures (rating = Again) under FSRS | +|-------------------------+-------+--------------------------------------| +| =DRILL_LAST_REVIEWED= | time | reused from the existing SM property | +|-------------------------+-------+--------------------------------------| =DRILL_LAST_REVIEWED= is written by the shared reschedule flow (=org-drill.el=:1930, unconditional after the algorithm dispatch), not by @@ -291,14 +324,22 @@ participate in every workflow that already handles SM scheduling properties. V1 adds them to =org-drill-scheduling-properties= so the existing helpers cover them without per-call plumbing: -| Workflow | Helper / call site | Behavior | -|----------+---------------------+----------| -| Rating snapshot for undo | =org-drill--snapshot-entry-data= | Captures =DRILL_FSRS_*= alongside SM properties | -| Undo last rating | =org-drill-undo-last-rating= → =org-drill--restore-entry-data= | Restores =DRILL_FSRS_*= verbatim | -| Strip single entry | =org-drill-strip-entry-data= | Removes =DRILL_FSRS_*= | -| Strip all entries | =org-drill-strip-all-data= | Removes =DRILL_FSRS_*= across the scope | -| Copy / share to marker | =org-drill--copy-scheduling-to-marker= | Migrates =DRILL_FSRS_*= | -| Org property completion | =org-drill-scheduling-properties= consumers | =DRILL_FSRS_*= appear in completion | +| Workflow | Helper / call site | Behavior | +|--------------------------+---------------------------------------------+---------------------------------------------| +| Rating snapshot for undo | =org-drill--snapshot-entry-data= | Captures =DRILL_FSRS_*= alongside SM | +| | | properties | +|--------------------------+---------------------------------------------+---------------------------------------------| +| Undo last rating | =org-drill-undo-last-rating= → | Restores =DRILL_FSRS_*= verbatim | +| | =org-drill--restore-entry-data= | | +|--------------------------+---------------------------------------------+---------------------------------------------| +| Strip single entry | =org-drill-strip-entry-data= | Removes =DRILL_FSRS_*= | +|--------------------------+---------------------------------------------+---------------------------------------------| +| Strip all entries | =org-drill-strip-all-data= | Removes =DRILL_FSRS_*= across the scope | +|--------------------------+---------------------------------------------+---------------------------------------------| +| Copy / share to marker | =org-drill--copy-scheduling-to-marker= | Migrates =DRILL_FSRS_*= | +|--------------------------+---------------------------------------------+---------------------------------------------| +| Org property completion | =org-drill-scheduling-properties= consumers | =DRILL_FSRS_*= appear in completion | +|--------------------------+---------------------------------------------+---------------------------------------------| *Implementation.* Add the four FSRS property names to the =org-drill-scheduling-properties= defvar list. No new branching; @@ -414,15 +455,22 @@ Two new helpers parallel the existing item-data round-trip: The implementation separates pure functions from property IO so each layer is independently testable: -| Layer | Functions | Tested in | -|-------+-----------+-----------| -| Pure quality mapping | =org-drill--fsrs-rating-from-quality= | mapping tests | -| Pure retrievability | =org-drill--fsrs-retrievability= | reference-vector tests | -| Pure initial state | =org-drill--fsrs-initial-state= | reference-vector tests | -| Pure update step | =org-drill--fsrs-update= | reference-vector tests | -| Pure interval calc | =org-drill--fsrs-interval= | reference-vector tests | -| Property IO | =org-drill-get-fsrs-state=, =org-drill-store-fsrs-result= | round-trip tests | -| Orchestration | =org-drill-determine-next-interval-fsrs= | integration tests | +| Layer | Functions | Tested in | +|----------------------+-----------------------------------------------------------+------------------------| +| Pure quality mapping | =org-drill--fsrs-rating-from-quality= | mapping tests | +|----------------------+-----------------------------------------------------------+------------------------| +| Pure retrievability | =org-drill--fsrs-retrievability= | reference-vector tests | +|----------------------+-----------------------------------------------------------+------------------------| +| Pure initial state | =org-drill--fsrs-initial-state= | reference-vector tests | +|----------------------+-----------------------------------------------------------+------------------------| +| Pure update step | =org-drill--fsrs-update= | reference-vector tests | +|----------------------+-----------------------------------------------------------+------------------------| +| Pure interval calc | =org-drill--fsrs-interval= | reference-vector tests | +|----------------------+-----------------------------------------------------------+------------------------| +| Property IO | =org-drill-get-fsrs-state=, =org-drill-store-fsrs-result= | round-trip tests | +|----------------------+-----------------------------------------------------------+------------------------| +| Orchestration | =org-drill-determine-next-interval-fsrs= | integration tests | +|----------------------+-----------------------------------------------------------+------------------------| The =cl-case= branch in =org-drill-smart-reschedule= is the orchestration layer. Everything below the branch is pure or @@ -648,13 +696,76 @@ updated to "not implementation-ready as of Review 2" before this response. * Review and iteration history -| Iteration | Date | Contributor | Role | What changed or was recommended | Why | -|------------+------------+---------------+-----------+----------------------------------+-----| -| Draft v0 | 2026-05-27 | Craig Jennings | Author | Initial draft with 6 DECIDE markers and recommendations baked in for each. Same shape as the stats-dashboard spec. | Establish a written design before any FSRS code lands. | -| Review 1 | 2026-05-28 | External reviewer | Reviewer | Four blocking findings (version/tuple/equation mismatch, scheduler/state result shape, property ownership across strip/undo/copy/migration, learning/relearning scope) plus two medium, plus UX/Architecture/Robustness observations and full test-strategy expansion. | An external read of the spec against the existing codebase caught the v4-vs-v4.5 drift and several missing secondary-workflow paths. | -| Response 1 | 2026-05-28 | Claude Code | Responder | All four blocking findings resolved; medium-priority items accepted; new sections added (Agreed decisions, Scheduling property ownership, Validation and malformed-state behavior); test strategy expanded to four files; v4 → v4.5 tuple and equations corrected; struct shapes locked; DRILL_CARD_WEIGHT confirmed to apply (Craig's call). | Convert the spec from "not implementation-ready" to "implementation-ready" via the spec-response workflow. Removes invented-product-behavior risk for the implementer. | -| Review 2 | 2026-05-28 | Codex | Reviewer | Marked the spec as needs-research before implementation; identified unresolved =py-fsrs= source pin/formula/vector prerequisites, =DRILL_CARD_WEIGHT= semantics reversed against current org-drill, and a quality-mapping conflict with =org-drill-failure-quality=. | Applying the updated spec-review workflow to the moved =docs/design/= spec caught contradictions introduced while preserving Review 1 decisions. | -| Response 2 | 2026-05-31 | Claude Code | Responder | Corrected =DRILL_CARD_WEIGHT= to the real SM/Simple8 delta-interpolation (Craig's call to match existing semantics); resolved the quality mapping to honor =org-drill-failure-quality= for the Again boundary with a fixed success sub-mapping (Craig's call); reframed status/open-questions to "Needs research" with the three blocking prerequisites stated honestly; fixed =DRILL_LAST_REVIEWED= ownership (M1) and the algorithm-symbol coverage (M2); flagged the update equations as paraphrased-and-unverified. | Fix the three blockers and two mediums from Review 2. The B2 reversal traced to a wrong verbal description of SM weight handling in Response 1; this response grounds every claim in the cited =org-drill.el= line. | +| Iteration | Date | Contributor | Role | What changed or was | Why | +| | | | | recommended | | +|------------+------------+-------------------+-----------+------------------------------+-----------------------------| +| Draft v0 | 2026-05-27 | Craig Jennings | Author | Initial draft with 6 DECIDE | Establish a written design | +| | | | | markers and recommendations | before any FSRS code lands. | +| | | | | baked in for each. Same | | +| | | | | shape as the stats-dashboard | | +| | | | | spec. | | +|------------+------------+-------------------+-----------+------------------------------+-----------------------------| +| Review 1 | 2026-05-28 | External reviewer | Reviewer | Four blocking findings | An external read of the | +| | | | | (version/tuple/equation | spec against the existing | +| | | | | mismatch, scheduler/state | codebase caught the | +| | | | | result shape, property | v4-vs-v4.5 drift and | +| | | | | ownership across | several missing | +| | | | | strip/undo/copy/migration, | secondary-workflow paths. | +| | | | | learning/relearning scope) | | +| | | | | plus two medium, plus | | +| | | | | UX/Architecture/Robustness | | +| | | | | observations and full | | +| | | | | test-strategy expansion. | | +|------------+------------+-------------------+-----------+------------------------------+-----------------------------| +| Response 1 | 2026-05-28 | Claude Code | Responder | All four blocking findings | Convert the spec from "not | +| | | | | resolved; medium-priority | implementation-ready" to | +| | | | | items accepted; new sections | "implementation-ready" via | +| | | | | added (Agreed decisions, | the spec-response workflow. | +| | | | | Scheduling property | Removes | +| | | | | ownership, Validation and | invented-product-behavior | +| | | | | malformed-state behavior); | risk for the implementer. | +| | | | | test strategy expanded to | | +| | | | | four files; v4 → v4.5 tuple | | +| | | | | and equations corrected; | | +| | | | | struct shapes locked; | | +| | | | | DRILL_CARD_WEIGHT confirmed | | +| | | | | to apply (Craig's call). | | +|------------+------------+-------------------+-----------+------------------------------+-----------------------------| +| Review 2 | 2026-05-28 | Codex | Reviewer | Marked the spec as | Applying the updated | +| | | | | needs-research before | spec-review workflow to the | +| | | | | implementation; identified | moved =docs/design/= spec | +| | | | | unresolved =py-fsrs= source | caught contradictions | +| | | | | pin/formula/vector | introduced while preserving | +| | | | | prerequisites, | Review 1 decisions. | +| | | | | =DRILL_CARD_WEIGHT= | | +| | | | | semantics reversed against | | +| | | | | current org-drill, and a | | +| | | | | quality-mapping conflict | | +| | | | | with | | +| | | | | =org-drill-failure-quality=. | | +|------------+------------+-------------------+-----------+------------------------------+-----------------------------| +| Response 2 | 2026-05-31 | Claude Code | Responder | Corrected | Fix the three blockers and | +| | | | | =DRILL_CARD_WEIGHT= to the | two mediums from Review 2. | +| | | | | real SM/Simple8 | The B2 reversal traced to a | +| | | | | delta-interpolation (Craig's | wrong verbal description of | +| | | | | call to match existing | SM weight handling in | +| | | | | semantics); resolved the | Response 1; this response | +| | | | | quality mapping to honor | grounds every claim in the | +| | | | | =org-drill-failure-quality= | cited =org-drill.el= line. | +| | | | | for the Again boundary with | | +| | | | | a fixed success sub-mapping | | +| | | | | (Craig's call); reframed | | +| | | | | status/open-questions to | | +| | | | | "Needs research" with the | | +| | | | | three blocking prerequisites | | +| | | | | stated honestly; fixed | | +| | | | | =DRILL_LAST_REVIEWED= | | +| | | | | ownership (M1) and the | | +| | | | | algorithm-symbol coverage | | +| | | | | (M2); flagged the update | | +| | | | | equations as | | +| | | | | paraphrased-and-unverified. | | +|------------+------------+-------------------+-----------+------------------------------+-----------------------------| * References diff --git a/docs/design/stats-dashboard.org b/docs/design/stats-dashboard.org index 6bb18d9..ba01ae3 100644 --- a/docs/design/stats-dashboard.org +++ b/docs/design/stats-dashboard.org @@ -24,17 +24,26 @@ what each card carries in its own =DRILL_*= properties. The currently-retained per-card data is real and usable: -| Property | Source | -|----------+--------| -| =DRILL_LAST_INTERVAL= | scheduler output, updated every review | -| =DRILL_REPEATS_SINCE_FAIL= | review counter resetting on a lapse | -| =DRILL_TOTAL_REPEATS= | lifetime review count for the card | -| =DRILL_FAILURE_COUNT= | lifetime lapses | -| =DRILL_AVERAGE_QUALITY= | running mean of the 0–5 quality scale | -| =DRILL_EASE= | SM-family easiness factor | -| =DRILL_LAST_QUALITY= | quality at the most recent review | -| =DRILL_LAST_REVIEWED= | inactive timestamp of the most recent review | -| =DATE_ADDED= | inactive timestamp of card creation | +| Property | Source | +|----------------------------+----------------------------------------------| +| =DRILL_LAST_INTERVAL= | scheduler output, updated every review | +|----------------------------+----------------------------------------------| +| =DRILL_REPEATS_SINCE_FAIL= | review counter resetting on a lapse | +|----------------------------+----------------------------------------------| +| =DRILL_TOTAL_REPEATS= | lifetime review count for the card | +|----------------------------+----------------------------------------------| +| =DRILL_FAILURE_COUNT= | lifetime lapses | +|----------------------------+----------------------------------------------| +| =DRILL_AVERAGE_QUALITY= | running mean of the 0–5 quality scale | +|----------------------------+----------------------------------------------| +| =DRILL_EASE= | SM-family easiness factor | +|----------------------------+----------------------------------------------| +| =DRILL_LAST_QUALITY= | quality at the most recent review | +|----------------------------+----------------------------------------------| +| =DRILL_LAST_REVIEWED= | inactive timestamp of the most recent review | +|----------------------------+----------------------------------------------| +| =DATE_ADDED= | inactive timestamp of card creation | +|----------------------------+----------------------------------------------| What's missing is the *temporal axis*: there's no record that the user reviewed N cards on a given day, or that the pass percentage trended @@ -116,18 +125,28 @@ Each entry is an `org-drill-session-record' struct.") =org-drill-session-record= is a =cl-defstruct=: -| Slot | Type | Meaning | -|------+------+---------| -| =start-time= | float | =float-time= at session start | -| =end-time= | float | =float-time= at session end | -| =scope= | symbol or list | =org-drill-scope= value at session start | -| =algorithm= | symbol | =org-drill-spaced-repetition-algorithm= at start | -| =qualities= | vector of int | every quality 0–5 entered, in order | -| =pass-percent= | int | qualities > =org-drill-failure-quality=, / total | -| =new-count= | int | size of =(oref session new-entries)= at end | -| =mature-count= | int | =young-mature= + =old-mature= entries at end | -| =failed-count= | int | =failed-entries= at end | -| =cram-mode= | bool | =cram-mode= at session start | +| Slot | Type | Meaning | +|----------------+----------------+--------------------------------------------------| +| =start-time= | float | =float-time= at session start | +|----------------+----------------+--------------------------------------------------| +| =end-time= | float | =float-time= at session end | +|----------------+----------------+--------------------------------------------------| +| =scope= | symbol or list | =org-drill-scope= value at session start | +|----------------+----------------+--------------------------------------------------| +| =algorithm= | symbol | =org-drill-spaced-repetition-algorithm= at start | +|----------------+----------------+--------------------------------------------------| +| =qualities= | vector of int | every quality 0–5 entered, in order | +|----------------+----------------+--------------------------------------------------| +| =pass-percent= | int | qualities > =org-drill-failure-quality=, / total | +|----------------+----------------+--------------------------------------------------| +| =new-count= | int | size of =(oref session new-entries)= at end | +|----------------+----------------+--------------------------------------------------| +| =mature-count= | int | =young-mature= + =old-mature= entries at end | +|----------------+----------------+--------------------------------------------------| +| =failed-count= | int | =failed-entries= at end | +|----------------+----------------+--------------------------------------------------| +| =cram-mode= | bool | =cram-mode= at session start | +|----------------+----------------+--------------------------------------------------| A single record is small (a few hundred bytes). At one session per day, the log holds a year of history in well under 100 KB. At three @@ -275,12 +294,19 @@ helper. Users who want TSV can run a one-line sed pipe. The expensive paths and their bounds: -| Path | Cost | Mitigation | -|------+------+------------| -| Session log save | one =persist-save= per session | already wrapped in =condition-case=; cost is a few KB write | -| Scope walk on dashboard open | =org-map-entries= over scope | same cost as a session open; cached for the dashboard's lifetime, refreshes on =g= | -| CSV export | one walk + one file write | one-off; user-triggered | -| Sparkline rendering | bucket the in-memory log by day | log is bounded; bucket-by-day is linear in log length | +| Path | Cost | Mitigation | +|------------------------------+---------------------------------+-----------------------------------------------------| +| Session log save | one =persist-save= per session | already wrapped in =condition-case=; cost is a few | +| | | KB write | +|------------------------------+---------------------------------+-----------------------------------------------------| +| Scope walk on dashboard open | =org-map-entries= over scope | same cost as a session open; cached for the | +| | | dashboard's lifetime, refreshes on =g= | +|------------------------------+---------------------------------+-----------------------------------------------------| +| CSV export | one walk + one file write | one-off; user-triggered | +|------------------------------+---------------------------------+-----------------------------------------------------| +| Sparkline rendering | bucket the in-memory log by day | log is bounded; bucket-by-day is linear in log | +| | | length | +|------------------------------+---------------------------------+-----------------------------------------------------| The dashboard does *not* run at session open or close — only when the user invokes =M-x org-drill-statistics=. Session-end pays one =persist-save= @@ -393,18 +419,28 @@ the export + docs + polish. All 10 open questions resolved as recommended. Implementation can proceed against this spec. -| # | Question | Resolution | -|---+----------+------------| -| 1 | Persistence shape | =persist-defvar=, mirroring the SM5 matrix | -| 2 | Corrupted-load recovery | warn, fresh-start, rename to =.corrupt-YYYY-MM-DD= | -| 3 | Sparkline character set | quadrant blocks (▁▂▃▄▅▆▇█) | -| 4 | Filter scope | single buffer-wide filter | -| 5 | CSV delimiter | =,= with proper quoting | -| 6 | Dashboard-open mode | sync | -| 7 | Aborted-session recording | record nothing; =unwind-protect= deferred | -| 8 | Dashboard keymap | =q g e s r a RET= | -| 9 | Leech-quality threshold default | 2.5 | -| 10 | Defcustom group placement | sibling group =org-drill-statistics= | +| # | Question | Resolution | +|----+---------------------------------+----------------------------------------------------| +| 1 | Persistence shape | =persist-defvar=, mirroring the SM5 matrix | +|----+---------------------------------+----------------------------------------------------| +| 2 | Corrupted-load recovery | warn, fresh-start, rename to =.corrupt-YYYY-MM-DD= | +|----+---------------------------------+----------------------------------------------------| +| 3 | Sparkline character set | quadrant blocks (▁▂▃▄▅▆▇█) | +|----+---------------------------------+----------------------------------------------------| +| 4 | Filter scope | single buffer-wide filter | +|----+---------------------------------+----------------------------------------------------| +| 5 | CSV delimiter | =,= with proper quoting | +|----+---------------------------------+----------------------------------------------------| +| 6 | Dashboard-open mode | sync | +|----+---------------------------------+----------------------------------------------------| +| 7 | Aborted-session recording | record nothing; =unwind-protect= deferred | +|----+---------------------------------+----------------------------------------------------| +| 8 | Dashboard keymap | =q g e s r a RET= | +|----+---------------------------------+----------------------------------------------------| +| 9 | Leech-quality threshold default | 2.5 | +|----+---------------------------------+----------------------------------------------------| +| 10 | Defcustom group placement | sibling group =org-drill-statistics= | +|----+---------------------------------+----------------------------------------------------| * References |
