Build a study plan from a syllabus and a calendar — three courses of action, exported to the learner's calendar.
A learner has a syllabus (what's due, and when) and a finite amount of time. Cadence connects the two things that usually live in separate files: it reads where the learner stands, then lays out day-by-day study blocks across three courses of action —
- Catch up (behind) — triage the essentials, front-loaded to the earliest days, no time wasted on review.
- Maintain (on track) — a steady pace with spaced review baked in.
- Get ahead (ahead) — deeper study, spaced review, and extra practice on the heaviest topics.
Availability is a hard ceiling: no day — study, review, or practice — is ever scheduled past the
minutes you actually have, and closed days stay empty. Work that doesn't fit is spread across more
days and, if it still can't land in time, counted in lateRisk rather than silently dropped.
Everything is pure and deterministic — no model, no network, no key. And the plan exports to a real
.ics calendar (Outlook / Google / Apple) with reminders, plus a flat list for a text or email nudge.
npm install cadenceRequires Node 18+. ES modules only.
import { plan, assessStatus } from 'cadence';
const syllabus = [
{ id: 'A', title: "Ohm's law", due: '2026-09-20', hours: 2, weight: 2 },
{ id: 'B', title: 'Series & parallel circuits', due: '2026-09-25', hours: 3, weight: 3 },
{ id: 'C', title: 'Soldering practical', due: '2026-10-02', hours: 1, weight: 1 },
];
assessStatus(syllabus, { asOf: '2026-09-15' }); // 'behind'
const p = plan({ syllabus, asOf: '2026-09-15', availability: 90 });
p.status; // 'behind'
p.recommended; // 'catch_up'
p.coas.catch_up; // { label, recommended: true, blocks: [...], totalMinutes, days, lateRisk }
p.coas.catch_up.blocks[0];
// { date: '2026-09-15', itemId: 'A', task: "Ohm's law", minutes: 90, kind: 'study' }Each COA gives you the scheduled blocks, the totalMinutes and days it spans, and a lateRisk
count — how many items can't be finished before their due date at the given pace, plus any work that
can't be placed in the available time at all.
Items are keyed by id (falling back to title); a duplicate is given a unique id so every task is
scheduled — none is dropped.
| field | meaning |
|---|---|
title |
what to learn |
due |
'YYYY-MM-DD', when it's assessed |
hours |
estimated study hours to master it (falls back to weight, else 1) |
weight |
relative importance (default 1) — breaks due-date ties and drives status |
completed |
already done (also accepts done) — skipped in the plan |
availability is either a flat number of minutes per day, or a weekday map keyed 0 (Sun) .. 6 (Sat).
A weekday absent from the map counts as 0 — a day off — so a partial map only opens the days you list:
plan({ syllabus, asOf: '2026-09-15', availability: { 0: 0, 1: 90, 2: 90, 3: 90, 4: 90, 5: 90, 6: 45 } });
// Sundays off, 90 min on weekdays, 45 on Saturday — no block of any kind ever lands on a closed day.
plan({ syllabus, asOf: '2026-09-15', availability: { 1: 60, 3: 60, 5: 60 } });
// Only Mon/Wed/Fri open at 60 min; every other day is 0 (off).assessStatus compares the weighted share of work completed against the share due by asOf.
Outside a tolerance band (default 0.15) it's behind or ahead; inside, on_track. Pass a
status to plan() to override the read.
import { toICS, toReminders } from 'cadence';
const ics = toICS(p.coas[p.recommended].blocks, {
calendarName: 'BEC Study Plan',
reminderMinutes: 30, // a VALARM this many minutes before each block
startHour: 18, // blocks start at 18:00, floating local time
});
// → a valid VCALENDAR string: one VEVENT + VALARM per block. Write it to study-plan.ics and import.
// Blocks sharing a day are sequenced back-to-back from startHour (no overlap); a block that runs
// past midnight rolls its DTEND onto the next day; every line is folded to ≤75 octets per RFC 5545.
toReminders(p.coas[p.recommended].blocks);
// → [ { date: '2026-09-15', text: "Ohm's law — 90 min", minutes: 90, kind: 'study' }, ... ]| export | purpose |
|---|---|
plan({ syllabus, availability?, asOf, status? }) |
status + a schedule for all three COAs |
assessStatus(syllabus, { asOf, tolerance? }) |
'behind' | 'on_track' | 'ahead' |
toICS(blocks, opts?) |
study blocks → an iCalendar (.ics) string with reminders |
toReminders(blocks) |
study blocks → flat reminder rows for text/email |
COAS |
the three COAs and their scheduling parameters |
Every function validates its input and throws TypeError on a bad shape or date — including dates
that aren't real calendar days ('2026-02-30' throws rather than rolling into March).
node example/demo.mjsApache-2.0.