From f96216d5d36c94aa1cf22363b2a9c2cb0150adb5 Mon Sep 17 00:00:00 2001 From: Anton Wiklund Date: Sun, 8 Nov 2026 16:33:28 +0100 Subject: [PATCH] fix(application): set aside an answer the log can never add The answer log moved past the head of an instance's queue only when the write check refused it. An answer refused for another reason before it was sent, such as a term longer than any read takes in, stayed at the head for good: every later answer of the instance waited behind it, was never logged and was lost when the page closed, while the review states were still saved. Such an answer could come from a course: a wrong option is named in the deck's cards document by the release's distractor id, and an id that fits under the release's short address can be too long under the deck's. Keeping a guest's study re-roots the same option at the instance's address, both on the card and in the answer, so the whole keep failed every time. A card's own id, from a release too, can fit under the guest's short address and not under the instance's, with the same result. The queue now sets aside every refusal made before sending (dataNotConforming, unwritableTerm, termTooLong), and an answer the pod refuses as a bad request (400, 413, 414, 422) a third time. It keeps retrying any other failure, such as no answer, a failed login or a server error, which says nothing of the answer itself. A course answer, and an answer kept from a guest's study, name the wrong option only when its IRI there can be written, and are logged without it otherwise. A card kept from a guest's study is added without such an option, the rest of the card as it was. A card whose own IRI, or the longest name of its review states, would be too long in the new deck is left out with its review states and its answers, so the rest of the study is kept. Refs: GHSA-9fv5-c6xw-wqx5 --- docs/data-model.md | 42 ++++--- docs/guest-mode.md | 23 +++- packages/application/src/answerStore.test.ts | 66 +++++++++++ packages/application/src/answerStore.ts | 55 ++++++++- packages/application/src/courses.test.ts | 16 +++ packages/application/src/courses.ts | 5 +- packages/application/src/session.test.ts | 40 +++++++ packages/application/src/session.ts | 23 +++- packages/domain/src/answer.test.ts | 11 ++ packages/domain/src/answer.ts | 22 +++- packages/domain/src/guest.test.ts | 72 ++++++++++++ packages/domain/src/guest.ts | 61 ++++++++-- packages/solid/src/courseAnswer.test.ts | 50 ++++++++ packages/solid/src/guestStudy.test.ts | 115 ++++++++++++++++++- 14 files changed, 559 insertions(+), 42 deletions(-) diff --git a/docs/data-model.md b/docs/data-model.md index f5eeaef6..46b9f039 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -1338,16 +1338,19 @@ nothing could rebuild it. An answer to a [course](courses.md) question says how it was given (`sm:answerMode sm:multipleChoice`) and, when it was wrong, which wrong option was chosen (`sm:chosenDistractor`, the distractor in the - deck's cards document). An answer given in study says neither, which - means recalled. Since vocabulary 1.19 an answer may say how long it - took, in milliseconds the app was shown. That's from the prompt to the - answer revealed, or to the option chosen (`sm:recallMillis`), and - from there to the grade (`sm:gradeMillis`), each left out above ten - minutes. It - may say the day the prompt was due before it (`sm:priorDue`, absent - on a first answer), and the device's offset from UTC then, in minutes - east of Greenwich (`sm:utcOffsetMinutes`). Each is optional, and an - older app ignores them ([migrations.md](migrations.md)). + deck's cards document). It's left out when that IRI couldn't be + written: longer than a term may be, or not one Turtle writes as it is + (`chosenDistractorIn`). A release's own address may be much shorter + than the deck's, and the answer is logged without it. An answer given + in study says neither, which means recalled. Since vocabulary 1.19 an + answer may say how long it took, in milliseconds the app was shown. + That's from the prompt to the answer revealed, or to the option chosen + (`sm:recallMillis`), and from there to the grade (`sm:gradeMillis`), + each left out above ten minutes. It may say the day the prompt was due + before it (`sm:priorDue`, absent on a first answer), and the device's + offset from UTC then, in minutes east of Greenwich + (`sm:utcOffsetMinutes`). Each is optional, and an older app ignores + them ([migrations.md](migrations.md)). - **The intervals and due day are those of the scheduler that set them.** The grade is always the SM-2 quality, but the interval before and after an answer and the day it was due come from what set the @@ -1364,7 +1367,10 @@ nothing could rebuild it. 64 KiB (`appendAll`). Each id is the guest's followed by the new deck's, so adding it again to the same deck changes nothing. Adding it to another deck (the guest's deck added anew) makes an entry of - its own, never one naming two decks. + its own, never one naming two decks. A wrong option chosen that would + be too long in the new deck's longer address is left out, not the + answer. An answer to a card left out, whose IRI there would be too + long, isn't added. - **After the review, not in its way**: `recordReview` saves the review state, then queues the answer, with its response times ([statistics.md](statistics.md#response-times)), to be added in the @@ -1375,10 +1381,16 @@ nothing could rebuild it. session in it, its statistics, resetting a day. An add the server hasn't answered within 10 seconds is given up and tried again later, which changes nothing if it was saved (`APPEND_DEADLINE_MS`). An - answer the write check refuses (`dataNotConforming`) would be refused - every time, so only that answer is set aside, never logged, and those - after it go on. An answer still queued when the tab closes is lost; - its review isn't. + answer refused before it's sent would be refused every time: by the + write check (`dataNotConforming`), or for a term Turtle can't write + as it is (`unwritableTerm`) or longer than a term may be + (`termTooLong`). Only that answer is set aside, never logged, and + those after it go on. So is one the pod refuses as a bad request + (400, 413, 414 or 422) a third time. Any other failure, such as no + answer from the pod, a failed login or a server error, is tried + again: it says nothing of the answer, and those after it would fail + alike. An answer still queued when the tab closes is lost; its + review isn't. - **Resetting the day removes that deck's answers of the day** (`removeDay`: read, remove, save with If-Match, again on 412). **Undoing an answer in a session removes that one answer** by its id diff --git a/docs/guest-mode.md b/docs/guest-mode.md index 620d7462..4fe987d4 100644 --- a/docs/guest-mode.md +++ b/docs/guest-mode.md @@ -169,6 +169,15 @@ It runs these steps: part of the deck. Its review states keep their SM-2 fields and snapshots, each naming its card, direction and scheduler; its FSRS-6 states come as they are; + - a card's id, or a distractor's, comes from a release and may fit + under the guest's short address but not under the new deck's. A + card whose IRI there, or the longest its review states take + (`#@back-to-front@fsrs6`), would be longer than a term may be + is left out, with its review states and its answers: it could be + neither written nor studied there (`cardFitsDeck`). Such a + distractor is left out, not the card (`movedCard`). A course still + asks the card with its release's options, which it reads from the + release; - the cards document, then the reviews document, is created with one PUT only where nothing is, then the deck's catalog entry, with `If-Match`, read and added again on 412 a few times at most. @@ -184,10 +193,12 @@ It runs these steps: added to the instance's log for its month, insert-only, unread (`appendAll`, one PATCH per month, more where one would pass 64 KiB). Its deck, card and chosen wrong option are named in the new - deck's documents, by the same fragment ids; its grade, intervals - and response times stay as given. Its id is the guest's followed by - the new deck's (`answer-