docs(adr): ADR 0024, LanguageTool as a sidecar
The developer's decisions of 26 Oct (D1-D5) and the calls made building it; notes in ADR 0017 (point 10) and ADR 0021 (D54) on what it amends; Learn's AGENTS.md on the sidecar and the checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
d2eaef255e
commit
c23c0c80a1
3 changed files with 131 additions and 2 deletions
|
|
@ -17,6 +17,10 @@
|
|||
GitHub half, point 11's, the GitHub rows below and M1's end hold for nobody: an
|
||||
account without an address sets no password (`wrongCredentials`), and the GitHub IDs
|
||||
left are the operator's to delete (ADR 0020, D36, gives the SQL).
|
||||
And [ADR 0024](0024-languagetool-sidecar.md), D4 and D5 (26 Oct 2026): point 10's
|
||||
"checked by the instance's LanguageTool" holds no more. What the person types into the
|
||||
field's Check goes to it, and what they write in a private studyset's editors once they
|
||||
agreed for that studyset.
|
||||
|
||||
## Context
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@
|
|||
the developer amended on 26 Oct to what was built.
|
||||
- **Builds on:** ADR 0016 (the register), ADR 0019 (private studysets), ADR 0020
|
||||
(SimpleLogin), ADR 0022 (the hand-off and the connector gone).
|
||||
- **Amended by:** [ADR 0024](0024-languagetool-sidecar.md), D3 (26 Oct 2026): the field's
|
||||
Check (D54) goes to the instance's LanguageTool first where it checks the language,
|
||||
whose sheet offers to ask the assistant why; elsewhere it asks the assistant, as D54
|
||||
has it.
|
||||
- **Changes:** the rule of 10 Oct 2026 that Halkyon calls no language model with an API
|
||||
key, for an instance whose operator opts in.
|
||||
|
||||
|
|
|
|||
120
docs/adr/0024-languagetool-sidecar.md
Normal file
120
docs/adr/0024-languagetool-sidecar.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# ADR 0024: LanguageTool as a sidecar
|
||||
|
||||
- **Status:** decided 26 Oct 2026, by the developer (D1–D5 below); built on 26 Oct for
|
||||
v0.21.0, with ADR 0021's step 3.
|
||||
- **Amends:** ADR 0017, point 10 (a private studyset is never "checked by the instance's
|
||||
LanguageTool"): what the person writes in one is checked once they agree for it (D5),
|
||||
and what they type into the field's Check always (D4). ADR 0021, D54 (Check in the
|
||||
field asks the assistant): LanguageTool checks first where it can (D3).
|
||||
|
||||
## Context
|
||||
|
||||
Halkyon Learn is used to write in the language being learnt: flashcards, notes, answers
|
||||
in the field. A spelling and grammar checker helps there as much as anywhere, and
|
||||
LanguageTool is the open-source one (LGPL 2.1) that runs on one's own server: some 60
|
||||
languages, Korean not among them, Greek and Chinese unmaintained, German and English the
|
||||
best kept. Its HTTP API checks a text in a language, or tells the language itself, and
|
||||
answers with what it found: where, why, and what could stand there instead.
|
||||
|
||||
An instance may already run services beside itself (`deploy/sidecars/`, decided 14 Oct:
|
||||
Jellyfin), each a compose file switched on in `deploy/.env`. On 26 Oct, before the push
|
||||
of ADR 0021's three steps, the developer asked "to also include the open-source version
|
||||
of languagetool as a potential sidecar, similar to Jellyfin being an optional sidecar".
|
||||
|
||||
LanguageTool's server has no sign-in. Jellyfin has its own accounts, so the instance
|
||||
publishes it under a site of its own; LanguageTool published so would check anyone's
|
||||
text on the operator's machine. And unlike a person's Jellyfin, which the app reaches
|
||||
directly, a checker sees what people write while they write it.
|
||||
|
||||
## Decisions (26 Oct 2026, the developer's)
|
||||
|
||||
- **D1 The sidecar, a server endpoint, the field's Check and underlining in the editors**
|
||||
(the developer's choice, over the sidecar with Check alone, which was recommended as
|
||||
the smaller step, and over the sidecar with the endpoint alone): the checks reach every
|
||||
place people write.
|
||||
- **D2 The image is `meyay/languagetool`** 6.8-13, pinned by its digest (recommended, over
|
||||
`erikvl87/languagetool`, a month behind, and over building our own from LanguageTool's
|
||||
tag): LanguageTool's own downloads end at 6.6, and this image builds every release from
|
||||
its tag, runs as any user ID on a read-only filesystem and has a health check.
|
||||
- **D3 Check goes to LanguageTool first** (recommended, amending ADR 0021's D54): where the
|
||||
instance checks the language, the field's Check opens a sheet with what LanguageTool
|
||||
found, a tap taking a suggestion into the field, and "Ask <assistant> why" where one is
|
||||
offered; where it doesn't, Check asks the assistant as before. LanguageTool answers in
|
||||
a moment and costs the operator no tokens; the assistant explains, which LanguageTool
|
||||
can't.
|
||||
- **D4 What is typed into the field goes to LanguageTool in a private studyset too**
|
||||
(recommended, over checking it nowhere there): the person typed it to have it checked,
|
||||
as they paste a text to have flashcards proposed (D56), and the text is theirs, not the
|
||||
studyset's.
|
||||
- **D5 The editors of a private studyset check only once agreed, asked once per studyset**
|
||||
(recommended, as D9 and D17 ask for an assistant; over never checking there, as ADR
|
||||
0017's point 10 had it, and over the device's setting alone deciding): a banner above
|
||||
the editor, not a dialog, while it has the focus, says that what is written there goes
|
||||
to the instance readable, a paragraph at a time, and is kept nowhere, with "Check here"
|
||||
and "Not here". The answer holds for the studyset and its topics, follows the person
|
||||
(their sealed settings) and is taken back in the studyset's privacy dialog, which asks
|
||||
again.
|
||||
|
||||
## What else was built so (the agent's calls, each with its reason)
|
||||
|
||||
- **Reached only through the server.** The sidecar joins `edge` without a site in Caddy
|
||||
and publishes no port; the server's `grammar.check` (`requireLogin`) forwards a text and
|
||||
its language (`LANGUAGETOOL_URL`, set by the sidecar's compose file). The server checks
|
||||
who asks and how often (60 checks a minute per person, counted in memory), refuses a
|
||||
text over 20,000 characters, and keeps and logs neither the text nor what was found; a
|
||||
failure is logged without LanguageTool's answer. The endpoint is Learn's, not
|
||||
halkyon_base's: only Learn writes text to check so far, and a module's endpoint is for
|
||||
what every Halkyon app needs.
|
||||
- **No log in the container** (`logging: none`): LanguageTool's log names each check's
|
||||
length and language only, but a failing rule may quote its sentence.
|
||||
- **Memory**: Java's heap at most 1 GB (1.2 GB resident with fastText beside it and five
|
||||
languages loaded, measured), the container capped at 2 GB.
|
||||
- **fastText on**: the model telling languages apart (126 MB, fetched at the first start
|
||||
into `deploy/languagetool/fasttext`) makes the recognised language of a note far more
|
||||
reliable than LanguageTool's own guess; it costs 135 MB of memory.
|
||||
- **Languages**: the instance asks its LanguageTool which it checks as it starts and an
|
||||
hour after, and `instance.info()` names them (`grammarLanguages`, primary subtags), so
|
||||
the app offers checks only where they work. A tag maps to LanguageTool's code
|
||||
(`languageToolCode`): `en` and `de` to `en-US` and `de-DE`, since the bare codes check no
|
||||
spelling; traditional Chinese to none, as LanguageTool only knows simplified; an empty
|
||||
one to `auto`, whose guess prefers `en-US`, `de-DE`, `pt-PT` and `ca-ES`.
|
||||
- **What is checked in which language**: a card's front in the studyset's front language
|
||||
(a character card's front not at all: one character is no text), its back and its
|
||||
answers in the back's, its hints and explanation, notes, steps, a PDF page's note,
|
||||
descriptions and discussions in the language LanguageTool recognises, since they mix
|
||||
the studyset's two.
|
||||
- **How the editors check** (`core/grammar/`): `GrammarCheckController` checks once the
|
||||
writer pauses for 1.5 s and as the editor loses the focus, never at each key; a
|
||||
paragraph at a time, so what is found maps to the node's offsets, and only paragraphs
|
||||
whose text changed (the rest kept by their text); one check at a time; formulas and
|
||||
code masked as spaces, so they are neither checked nor underlined. An edit drops what
|
||||
was found at once, so an underline never points at the wrong word; moving the caret
|
||||
doesn't. A language LanguageTool refuses ends the checking in that editor. Nothing is
|
||||
written into the document: `GrammarOverlay` draws red wavy lines over the editor where
|
||||
the editor would draw a selection, and follows its scrolling and wrapping; a tap opens
|
||||
a menu of what is wrong and the suggestions, and the one chosen is an edit like any
|
||||
other. A check that found nothing shows a muted check in the corner.
|
||||
- **Settings › Writing**: "Check grammar while writing", a device setting, on unless
|
||||
turned off (`checkGrammar`), saying so where the instance checks nothing yet. It
|
||||
covers the editors; the field's Check is asked for each time.
|
||||
- **Private studysets' answers**: an agreement is kept as an assistant's is
|
||||
(`privateAgreements`, the key `grammar`), so two devices' settings fold it the same way
|
||||
(only where both agreed); a refusal in `grammarDeclined` (settings schema 14), folded
|
||||
where either refused, so an older row never sends what was refused since.
|
||||
- **The privacy page** says that what is checked, and written while checking is on, goes
|
||||
to the instance's LanguageTool, a paragraph at a time, in a private studyset only once
|
||||
agreed, and that neither keeps it.
|
||||
|
||||
## Consequences
|
||||
|
||||
- An operator switches it on as Jellyfin: a user ID of its own (61081), its folder, the
|
||||
compose file in `COMPOSE_FILE` (`deploy/sidecars/README.md`). Without it nothing
|
||||
changes: the instance names no languages, the editors check nothing, and Check asks the
|
||||
assistant.
|
||||
- What people write while checking is on is sent to the instance as they write, readable
|
||||
to it, as their studysets that aren't private are anyway; in a private studyset only
|
||||
where they agreed. A modified instance could keep it: the agreement says where it goes,
|
||||
not that the operator can't read it.
|
||||
- A paragraph is checked whenever its text changes after a pause; a person typing a long
|
||||
note sends some checks a minute. The limit of 60 a minute is above what writing needs
|
||||
and below what would let one person keep the checker busy.
|
||||
Loading…
Reference in a new issue