feat(studyset): prepare for an exam, with plans, a date and the pace to it

A studyset plans for exams: each plan names what an exam covers, the
topics it takes whole (with their flashcards, learning paths and PDFs)
and the tags whose flashcards it takes from anywhere in the studyset.
The author's plans are published with the studyset (payload schema 7,
`examPlans`); copying a release and taking a newer one point their
topics at the copy's own studysets. Plans are also offered from level
tags: JLPT N5 to N1, HSK 1 to 7-9 and KRDict's levels, each taking the
levels below it, as the exams do.

The person chooses a plan and the exam's day (`exam`), which stays
theirs: the server leaves it out of a release, as a PDF's deadline.
The study plan then shows the days left, how many of the plan's
flashcards were started, its paths' steps done and its PDFs' pages
read, and the pace that gets everything new learnt a week before the
exam (by the exam when two weeks or fewer are left): new flashcards a
day, steps a week, pages a day. Study for the exam studies only what
the plan covers, its new cards at that pace (counted as the day began)
and every review due.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-14 14:26:08 +02:00
commit dd6c567ad0
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 2351 additions and 20 deletions

View file

@ -399,11 +399,12 @@ final _assetUrl = RegExp('asset:($_uuidPattern)');
/// What a payload of each kind holds that is only its author's, never
/// published: a card's report that something on it is wrong (their own
/// reminder; reports to an author are another thing, ADR 0005 rule 6) and
/// a PDF's reading deadline.
/// reminder; reports to an author are another thing, ADR 0005 rule 6), the
/// exam a studyset's author prepares for with its date (its plans are
/// published) and a PDF's reading deadline.
const _personal = {
'card': {'report'},
'studyset': <String>{},
'studyset': {'exam'},
'pdf': {'deadline'},
};

View file

@ -163,6 +163,14 @@ void main() {
'licenseHolder': 'Alice',
'study': {'newPerDay': 10},
'published': ?copyOf,
'examPlans': [
{
'id': 'n5',
'name': 'JLPT N5',
'tags': ['JLPT-N5'],
},
],
'exam': {'plan': 'n5', 'date': '2026-12-06'},
}),
),
transaction: transaction,
@ -448,6 +456,19 @@ void main() {
// The studysets' own study options are a suggestion to learners.
final root = await fileAt(published, 'v1', 'studyset.json');
expect(root['payload'], containsPair('study', {'newPerDay': 10}));
// Its exam plans are published; the exam its author prepares for,
// with its date, is theirs alone.
expect(
root['payload'],
containsPair('examPlans', [
{
'id': 'n5',
'name': 'JLPT N5',
'tags': ['JLPT-N5'],
},
]),
);
expect((root['payload']! as Map).containsKey('exam'), isFalse);
expect(root.containsKey('position'), isFalse);
final files = await fileAt(published, 'v1', 'files.json');
expect(files.length, 2);