Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ not a signed store release. Platform support beyond those checks should be verif
| Problem | Implementation | Regression tests |
|---|---|---|
| Persist meaning-level learning progress | [learning_repository.dart](lib/services/learning_repository.dart) | [learning_repository_test.dart](test/learning_repository_test.dart) |
| Keep unanswered questions bound to imported content | [persisted review contract](docs/persisted-review.md) | [disk, migration and failure tests](test/persisted_review_content_test.dart) |
| Preserve imported content across refreshes | [dictionary_import.dart](lib/services/dictionary_import.dart) | [home import](test/home_import_widget_test.dart), [dictionary validation](test/dictionary_import_test.dart) |
| Bound a slow speech download | [community_gateway.dart](lib/services/community_gateway.dart) | [gateway lifecycle](test/community_gateway_test.dart) |
| Prepare a usable review from available content | [review_preparation.dart](lib/services/review_preparation.dart) | [review_preparation_test.dart](test/review_preparation_test.dart) |
Expand All @@ -67,6 +68,12 @@ Reimporting an entry updates its content while preserving learning progress. A
storage failure may leave earlier entries imported; retrying is supported. The
import is not represented as an all-or-nothing database transaction.

An unanswered review is checked against its saved choices and current content
before it can advance learning progress. If an import changes that content, the
page prepares a fresh question. Temporary database failures leave the round
retryable. See the [persisted review contract](docs/persisted-review.md), including
the schema 3 to 4 upgrade and preservation of previously earned progress.

Refreshing the home screen seeds only missing sample meanings. It does not
overwrite an imported replacement or reset its learning state.

Expand Down
2 changes: 2 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ python3 scripts/check_public_tree.py

示例:`examples/words.json`。请确认导入内容的分发与使用许可。生产环境的第三方词库和音频没有随本仓库分发。

未回答的题目会在计分事务中核对已保存选项和当前内容;导入改变题目内容后,页面会重新出题。临时数据库错误保留轮次供重试。数据库从 schema 3 升级到 4 时保留已有进度,旧题快照会在再次使用时重建。设计与磁盘回归测试见 [持久化题目说明](docs/persisted-review.md)。

## 可选语音网关

默认不联系网络服务。设备离线语音是否可用取决于系统安装的语音包。需要下载发音时,自行实现 [网关协议](docs/speech-gateway.md),再显式配置自己的地址:
Expand Down
70 changes: 70 additions & 0 deletions docs/persisted-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Persisted review questions

Review progress belongs to a local user, query, and sense. Reimporting a dossier
updates its content without resetting earned stages, attempt counts, or learning
timestamps. A content update is not an automatic relearning policy.

An unanswered question has a narrower lifetime. Its displayed content and answer
must still match the dictionary when the answer is recorded. SQLite schema version
4 stores a `content_fingerprint` alongside each question's choices and correct
index. This is a SHA-256 hash of a versioned JSON representation containing:

- The target ID, learning stage, and normalized review language.
- The target word (also used for pronunciation), part of speech, and that
language's correct meaning, with answer whitespace normalized.
- For a context question, its English example and highlighted target form.
- All four saved choice strings in order and the saved correct index.

An independent question does not display the example. Changes to its example do
not invalidate that question. Unused translations, other language meanings,
attempt counters, and the dossier's global content revision are also excluded.
Changing a displayed dependency invalidates the unanswered question even when the
correct option's text stays the same.

Distractors are frozen text, not references to live dictionary entries. Editing
another word or replacing an external candidate pool does not change the choices
or correct index of a saved question. The complete frozen choices are included
in its fingerprint. Candidate selection's existing language and duplicate checks
still apply when generating a new question.

## Transaction boundaries

Restoring a saved question reads the current target and snapshot in one SQLite
transaction. Generating a new question may load candidates outside a transaction;
the insertion transaction then rereads the session position and the target's
content dependencies. If the content changed while candidates loaded, preparation
reports a changed question instead of saving the obsolete one or skipping the
target. Another valid snapshot already saved for that position wins.

Submitting an answer checks the active session position, saved choices/index,
request content fingerprint, and current target fingerprint in the same
transaction as the progress, attempt, and session updates. A stale request changes
none of those records. A duplicate submission at an already advanced position
also changes none of them. If an answer commits before an import, its earned
progress is retained by that later import.

`ReviewQuestionChanged` asks the review page to prepare its current position
again. A failure during this preparation uses the existing Retry state. A
temporary database failure is propagated separately; it is not evidence that a
question or session contains invalid data. Session retirement for invalid data
only follows a failure to decode the stored session payload.

## Existing databases and verification

The schema 3 to 4 migration adds a nullable column without changing progress,
attempts, or session positions. A legacy NULL fingerprint cannot establish what
content the old question tested. The question is rebuilt from current content
when requested; the migration does not backfill an invented fingerprint onto
its old choices. Malformed or stale snapshots are likewise rebuilt. Only the
question snapshot is discarded, and database read/delete failures remain errors.

`test/persisted_review_content_test.dart` uses fresh file-backed SQLite databases,
closes and reopens them, exercises the schema upgrade, injects one-shot read and
retirement failures, interrupts an attempt write with a SQLite abort trigger, and
holds candidate loading across an import. Widget regressions cover updating a
visible question and retrying after a preparation failure.

These checks establish application-level snapshot identity and transaction
behavior. They do not simulate a power cut or establish filesystem durability on
every device. The fingerprint is a consistency check, not authentication against
a party able to rewrite the local database.
12 changes: 12 additions & 0 deletions lib/pages/flash_cards/flash_cards_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,18 @@ class _FlashCardsWidgetState extends State<FlashCardsWidget>
await _requestExit();
return;
}
if (error is ReviewQuestionChanged) {
// Content can change without advancing the session or learning stage.
// Re-prepare this position; a storage failure during preparation uses
// the existing Retry state instead of leaving a stale answer enabled.
setState(() {
_submitting = false;
_selectedIndex = null;
_question = null;
});
await _advanceToNext(showPreparation: true);
return;
}
if (await _recoverAdvancedAnswer(session, question)) return;
if (!mounted) return;
if (!_accountIsCurrent) {
Expand Down
Loading