Repository navigation
ADR 0038 Review Proposals
Source of truth: this wiki page.
- Status: Accepted
- Date: 2026-10-09
-
Amends: ADR-0036 (a card may carry
proposedchanges), ADR-0037 (the index counts a file's proposals;mainnow also carries what reviewers propose). -
Amended: 2026-10-10 (#444): the index also gives each file's
content_sha256, its hash with its proposals taken out. A learner's phone is offered an update only when that changes, and files whose proposals alone changed come along with the next real update; a phone in reviewer mode is offered one whensha256changes. Every download is still checked againstsizeandsha256. This resolves the third consequence below.content_sha256drops aproposed:key's items whether they are indented under it, as the bot writes them, or at its own indent. -
Amended: 2026-10-10 (#449, owner's decision of the same day for
v0.4): each card records who checked it. A card entry may carry
checked_by, the rater codes of the reviewers who marked it "Looks right", which the review mail's PR adds, one line, never naming a code twice. Once every card entry of a deck file lists a code, the same PR tags the filereviewedin place ofunreviewed.content_sha256leaveschecked_byout as it does proposals; the tag is not left out, since learners see it. See "Who checked a card" below.
Reviewers review in the app and send their reviews by mail
(docs/plans/deck-browser.md, "Review in the app, by mail"). Until now a
suggested change reached the decks only when the owner read the mail and
the decks were edited with him. On 2026-10-09 the owner asked for it to be
automatic (#441, parent #403):
Make it totally automated then. The first review auto merges the PR and shows all changes to all applicable reviewers. If anyone agrees, that gets an instant merge to learner decks. This number can be a github secret so that I can update it easily. Like maybe once the app gains traction, i change it to two reviewers.
This replaced, the same day, a design in which the owner accepted a change
provisionally and the bot waited three days. Three things constrain it.
main is protected: every change needs an approving review and three
green checks, and agents never merge their own PRs. main is what learners
download (ADR-0037), so a change merged there reaches phones the next day.
And many reviews can arrive about the same card, while the owner, or
another review, may change it first.
-
A proposal is a fact, not a diff, kept on its card in the deck file
that holds the field: a
proposedlist, each item one line of YAML,{ id, field, now, text, by, date, why?, accepted? }.fieldistarget,reading,ipa,nativeornotes(a plain-text note only);nowis the text the proposer saw, "" for a field the card did not have;textthe new text;bythe proposer's rater code;datethe review file's date;whythe reason, if given;acceptedthe other rater codes that accepted it verbatim.idis the first ten hex digits of the SHA-256 of the card id, field, now, text and code, joined by U+001F, so the same proposal always has the same id and two never collide in practice. Suggestions on an example, a picture or a base word's meaning stay for the owner. A base word's meaning (#410) is suggested withpart: baseand awordfield naming the base by the word as it stands in the card's target, since a card may have several; only a base written in full on the card can be suggested, as a base by ref takes its meaning from the card it names. -
Learners never see a proposal. The app reads
proposedonly in reviewer mode. The validator checks its shape and nothing else in it: a proposal is not deck content until it is applied, and then the field is checked as any field is. -
A review mail becomes proposals at once. The hourly mail job opens
one PR per review mail that adds each suggestion as a proposal and
records each acceptance in the proposal's
accepted, then merges it as soon as the required checks pass. A suggestion whose field no longer holds what the reviewer saw, or whose card is gone, is left out as outdated. Only a mail whose sender passed the sender check, with one good rater code, is taken; the others stay with the owner. -
Agreement merges the change to learners. When
REVIEW_AGREEMENTS_NEEDEDrater codes other than the proposer's (a repository variable, default 1 when unset or not a whole number of at least 1) have accepted a proposal verbatim, the bot opens a PR that writestextinto the field, removes that proposal and every other proposal on the same field of the card, and merges it once the checks pass. The first proposal on a field to reach the number wins; the others are closed as outdated with it. The bot does not record when an acceptance came, so "first" is the proposal whose agreement PR opened first: while it is open no other on that field is applied, and several that reach the number before any PR opens go in the order they were proposed, which is their order in the file. - Accept, edit or reject. In reviewer mode a proposal shows Accept, Edit and Reject, and the review file records the answer. An edit is a suggestion of the reviewer's own, which becomes a new proposal; the old one keeps waiting. A rejection does not block anything: the mail's issue lists the rejected proposals, for the owner.
-
Every bot PR starts afresh. A fresh branch from
main; the card found by id across the language (ids are permanent, so a card that moved is still found); the field written only if it still holdsnow;decks/index.jsonrebuilt bytools/deck_index.py; the validator run before anything is pushed. A proposal whose field has changed is closed as outdated, by a bot PR that removes it, never written. A branch that falls behindmainis rebuilt frommainthe same way, and a PR is never merged unless its branch holds all ofmain, so a change the owner made meanwhile is never merged over. An agreement or outdated PR thatmainno longer calls for is closed. Branch names come from the mail, or from the proposals' ids and dates, so an hourly rerun finds its own PR and never opens a second; a PR the owner closes is never reopened, while the same proposal made again on a later day is a new attempt with a branch of its own. Only the three checks branch protection requires gate a merge, named intools/review_bot.pyand tested against.github/workflows/ci.yml. -
A GitHub App merges. The bot's PRs are opened and merged with an
App's installation token (
FLUENOUGH_BOT_APP_ID,FLUENOUGH_BOT_PRIVATE_KEY), the one actor allowed to skip the approval rule; the three required checks still apply to it. Without the secrets the job files issues as before, says in its log that proposals wait for the App, and leaves review mails to be proposed once it is set up. - The owner's override is any later commit: reverting a bot PR, editing the card, or deleting a proposal. Every bot PR names the rater codes involved.
-
The index counts proposals: a file's entry in
decks/index.jsongivesproposed, the number of proposals in it, when it has any.
-
A sign-off is a fact on the card, like a proposal:
checked_by: ["FL-…", …]on the entry, in the file of the deck the reviewer reviewed, and, for a layer, on its core's card too, since a reviewer of the merged deck checked the word as well as the meaning. Written as one line: its own line on a card written as a block,, checked_by: [...]before the closing brace of a card written in flow style. A code already listed is not added again. -
The review mail's PR writes it, beside the mail's proposals and
acceptances, from each card the review file marks
looks_right, and is merged the same way once the checks pass. A card the reviewed deck does not hold, or an entry the bot cannot edit one line at a time, is left for the owner and counted in the log. -
The deck's tag follows. When every card entry of a file the PR
looked at lists a code, the PR rewrites the file's
tagsline withreviewedin place ofunreviewed. A core and a layer are tagged each on its own; the merged deck dropsreviewedwhile either saysunreviewed(ADR-0036), so it reads as reviewed only once both are checked. A file with no card entries, a grammar or rules deck, is never tagged by the bot. -
No update prompt for a sign-off.
content_sha256leaves outchecked_bylines, and thechecked_bypart of a flow-style card's line, so the file with every sign-off removed hashes as it did before the first. Thereviewedtag counts: learners see it, on the unit's screen, and are offered the update. -
The app shows "Checked by N speakers" on a word's card and in
review, and the thanks on a unit, and Settings' decks you helped build,
read the codes in
checked_by(and inauthors, as decks listed them before).
- Native speakers change the decks learners download without the owner, within an hour or two of the agreeing mail: the time the checks take, twice. One agreeing reviewer is enough by default, so two people can change a card; raising the variable to 2 or more is how the owner tightens it.
- Every app released before this change refuses a deck file with a
proposedfield as an unknown field, and, as ADR-0037 has it, keeps the decks it has rather than use a batch with a file that fails. The owner's guide says to set the App up only once a release that readsproposedis out. -
A proposal changes the file's hash, so learners of that language are offered a deck update when a proposal lands, though nothing they see has changed.Resolved by the 2026-10-10 amendment (#444): the update check of a learner's phone comparescontent_sha256, the hash with proposals taken out, so a change to proposals only offers no update. Only reviewer mode is offered one. - Every app released before v0.4 refuses a deck file with
checked_byas an unknown field, as withproposed: the App is set up only once a release that reads both is out. - A sign-off is not undone when a card changes later, by an agreed
proposal or the owner's edit: the codes still say who checked the card,
not its present text. The owner deletes a code, or the line, to take a
sign-off back; a deck tagged
reviewedis not tagged back by the bot. - Rater codes, card ids and proposal ids appear in public PRs and issues, and the proposed text appears in the decks, which are public, as the plan allows. Mail addresses never do.
- The mail job now pushes branches and merges PRs. Its log says counts, rater codes, card ids and PR numbers, never an address or a suggestion's text.
- Proposals are one line each, written and removed by the bot without touching any other line of the file, so they never reformat a deck (AGENTS.md rule 8). A proposal on a field written over several lines, or on a card written in flow style, cannot be written by the bot, and stays for the owner.
- Review mails that came before the App was set up are proposed once it
is, unless the owner gives them the Gmail label
fluenough-proposedfirst.
-
Proposals in files of their own,
decks/<lang>/proposals/: they would not change the decks' hashes, but a card's proposals would live apart from the card, and an older app would refuse an unknown file kind just the same. The owner's plan puts them on the card. -
Storing a diff, or the reviewer's whole file, in the PR: any edit to
the deck in between would conflict. A fact about one field can be
checked against
mainat any time. -
Counting agreements from the mailbox each run: no record in the
repository, but every run would read every review mail ever sent, and
the count would be invisible to reviewers.
acceptedin the deck is public and cheap to read. - GitHub's auto-merge: needs a repository setting the bot cannot change; the bot merges when it sees the checks green, on its hourly run and when CI finishes on one of its branches.
- The threshold as a secret, as the owner first said: a secret cannot be read back, and GitHub hides its value everywhere in the logs, which for a value such as 1 hides every 1. The owner chose a variable; the workflow falls back to a secret of the same name if one is set.
-
The default
GITHUB_TOKEN: PRs it opens run no workflows, so the required checks would never run, and it cannot be allowed past the approval rule alone.
See also: Contributing · ADR-Index · Home
1. Learner guides
- Learner Guide
- Getting Started
- Today and Daily Lessons
- Drills and Skills
- Courses and Paths
- Proficiency Levels
- Scripts and Readings
- Readings ISO 15919
- Speaking and Listening
- Progress and Stats
- Profiles and Settings
- Backup and Restore
- Reporting a Problem
2. Deck building guides
- Deck Building Guide
- Deck Format
- Romanising Indic Languages
- Deck Licences and Quoted Books
- Deck Tools
3. Review guides
4. Translation guide
5. Building on iOS
6. Roadmap
7. Sources and attributions
8. Contribution and agentic coding