feat(learn): spelling and grammar checked as people write (ADR 0024)

Where the instance's LanguageTool checks the language, the editors
underline what it finds after a pause, a paragraph at a time, a tap
offering its suggestions (GrammarHost, GrammarCheckController,
GrammarOverlay; nothing written into the document). A card's front is
checked in the studyset's front language, its back and answers in the
back's, everything else in the language recognised.

In a private studyset nothing is sent until the person agrees for it,
asked once in a banner above the editor and kept in their settings
(an agreement under privateAgreements, a refusal in grammarDeclined,
settings schema 14), taken back in the studyset's privacy dialog.
Settings > Writing turns it off on the device.

The field's Check asks LanguageTool first where it checks the
studyset's language, in a sheet offering to ask the assistant why;
elsewhere it asks the assistant as before (amends ADR 0021, D54).

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-25 23:51:45 +01:00
commit d2eaef255e
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -692,4 +692,82 @@ void main() {
expect(both.foldedWith(both), both);
});
});
group('checking what is written in a private studyset (ADR 0024)', () {
test('is asked until answered; agreed, it is an agreement as an '
"assistant's, declined it is declined, and taken back it is asked "
'again', () {
const settings = PersonSettings();
expect(settings.grammarAnswerOf('ja'), isNull);
final agreed = settings.withGrammarAnswer('ja', check: true);
expect(agreed.grammarAnswerOf('ja'), isTrue);
expect(agreed.agreedToSend('ja', grammarAgreementKey), isTrue);
expect(agreed.grammarAnswerOf('ko'), isNull);
final declined = agreed.withGrammarAnswer('ja', check: false);
expect(declined.grammarAnswerOf('ja'), isFalse);
expect(
declined.agreedToSend('ja', grammarAgreementKey),
isFalse,
reason: 'one answer at a time',
);
expect(declined.withoutGrammarAnswer('ja').grammarAnswerOf('ja'), isNull);
expect(agreed.withoutGrammarAnswer('ja').grammarAnswerOf('ja'), isNull);
});
test('survives a round trip, in the fourteenth schema, the declined '
'left out while there are none; a payload from before declined '
'none', () {
final settings = const PersonSettings()
.withGrammarAnswer('ja', check: true)
.withGrammarAnswer('ko', check: false)
.withGrammarAnswer('de', check: false);
expect(settingsSchemaVersion, greaterThanOrEqualTo(14));
final read = decodeSettingsPayload(encodeSettingsPayload(settings));
expect(read, settings);
expect(read.grammarAnswerOf('ja'), isTrue);
expect(read.grammarAnswerOf('ko'), isFalse);
expect(
decodePayload(
encodeSettingsPayload(const PersonSettings()),
newestSchema: settingsSchemaVersion,
).keys,
isNot(contains('grammarDeclined')),
);
expect(
decodeSettingsPayload(
encodePayload(13, {'studyTags': <String, Object?>{}}),
).grammarDeclined,
isEmpty,
);
});
test("is checked, as two devices' settings fold, only where both "
'agreed, and declined where either declined: an older row never '
'sends what was declined since', () {
final agreed = const PersonSettings().withGrammarAnswer(
'ja',
check: true,
);
final declined = const PersonSettings().withGrammarAnswer(
'ja',
check: false,
);
for (final folded in [
agreed.foldedWith(declined),
declined.foldedWith(agreed),
]) {
expect(folded.grammarAnswerOf('ja'), isFalse);
}
expect(
agreed.foldedWith(const PersonSettings()).grammarAnswerOf('ja'),
isNull,
reason: 'a row not agreeing asks again',
);
});
});
}