Code and product review · 27 September 2026 · rechecked 30 September

EGRA Test Build Review

Sabeena's EGRA/EGMA test flow on the main Rumi bot's staging server, read against the plan we wrote for a five-minute, coach-run, AI-checked child assessment. What is done, what is not, what falls short, and what was done well.

Bot: main Rumi bot (hyasin270/whatsapp-ai-bot)Branch: staging, 10 commits, 16–28 SepNot on production · not in the NIETE botMeasured against: EGRA in Five Minutes

What changed since the review

On 28 September Sabeena pushed two more commits, now deployed on staging. They fix three of the five "fix first" items and the two worst coach-experience problems:

  • Public access to the results tables is revoked. Row-level security is still off, so the grants are the only lock.
  • Pictures now reach the coach. Links are signed before sending, and a failed send falls back to text. On 29 September five picture sends worked, with no errors.
  • The voice-note columns now exist. This came one day late. On 28 September a coach's listening marks were lost to exactly the failure the review predicted.
  • The listening-comprehension Flow is gone. The coach now reads the passage from a picture and marks the answers, and the voice note is optional and never blocks.
  • Both stories now offer an optional voice note, which is the plan's direction.
  • A drawn child must be confirmed. Backing out needs a reason (Absent or Other), and the reason is saved with the child's ID.

Still open: the letter formula, region-wide class access, the unsaved child ID on results, punctuation counted as words, numbered child cards, /cancel at typed prompts, the English-only coach text and missing answer key, and the untraced redraw through /cancel. The product-level gap from the plan is unchanged. One test session on 29 September took about 19 minutes for the Urdu block alone, which supports the 22-minute estimate. Full detail: the 30 September recheck.

The answer in one minute

Which bot. It is the main Rumi bot, not the NIETE bot. All ten commits are on the staging branch only, behind a switch that is on for staging and limited to Islamabad test accounts. Nothing has reached production.

What it is. A careful, well-documented first build that puts almost the whole May 2026 enumerator test into WhatsApp chat. The coach taps Correct or Incorrect for every item, one message at a time. No AI is used anywhere, and a database rule stops an AI score from ever being stored.

How it compares with the plan. It builds a different product. The plan is a five-minute test: the coach records one voice note per block while the child reads from a printed card, Rumi pre-fills a checking form, and the coach corrects it. This build takes about 22 minutes per child, which is today's enumerator time, and the one-message-per-item pattern is the one the field team rejected on 11 September.

Does it work today. Better than on 27 September: pictures arrive and saves with audio work. One session has completed the Urdu block. No session has finished all 14 subtests. Of the 6 saved results, none is linked to the child's record.

What to keep. The item bank, the code-drawn stimulus pictures, the random draw from the real roster, the switch-and-region gate, and the chat-loop fix are all good foundations for the plan's version.

~22min
per child, estimated from the code. The target is 5.
64taps
item-by-item marking taps per child, plus 11 typed answers
19min
for the Urdu block alone, in the one test session that finished it (29 Sep)
0 / 6
saved results linked to the child's record

Fix these first

These are small fixes, and each one matters before a real child is tested. Status as of 30 September.

  1. Fixed 28 Sep The results table was open to the public key. The public anon and authenticated roles could read, insert and delete rows, in the database production shares, and the table held a child's name. That access is now revoked on both EGRA tables. Row-level security is still off; switching it on would add a second lock at no cost.
  2. Fixed 28 Sep No stimulus picture could be delivered. All seven picture links point at the private storage address, which refuses the download (HTTP 400). The three real attempts, on 19, 21 and 24 September, all failed. The code only falls back to text when no link exists, not when the send fails, so the coach got a bare "Ready? Start" button with no letters, no story and no timing rules. Links are now signed and a failed send falls back to text.
  3. Fixed 28 Sep, after one loss The listening subtest would lose the coach's marks. The save always included the voice-note fields, but those columns had never been added to the live table, so the database rejected the whole row while the coach was told "Assessment complete". It happened once, on 28 September at 08:19 UTC, before the columns were added that evening.
  4. Open The letter score subtracts errors twice. The code computes (correct − incorrect) per minute, where "correct" already excludes errors. A child with 34 of 40 right scores 28, and a stopped child can score below zero. A test expects the negative number.
  5. Open Any Islamabad teacher can list every Islamabad class. Access is checked by region only, and 53 of the 56 Islamabad users are teachers. Any of them can type /egra, see every class and receive a random child's full name.

The goals, and what was built

The goals come from the 15 September call and the plan. "Contradicts" means the build does the opposite, or rules the plan's version out.

GoalVerdictWhat the build does
Five minutes per childContradictsAbout 22 minutes per child, the same as today's enumerator.
Five children in 30 minutes per coach per dayContradictsAbout 110 minutes for five children.
The coach records; Rumi pre-fills; the coach checks and correctsContradictsThe coach marks every item live. A database rule pins the AI-score field to "not attempted", so AI marks cannot be stored.
Keep the AI's answer, the coach's answer and the audio side by sideNot builtOnly the coach's answer is kept. An optional voice note is now offered for listening comprehension and both stories; none has been saved yet.
The five-minute test (Urdu story + questions, first sounds, made-up words, English story, maths: numbers aloud, quick sums, photo of written sums)ContradictsClose to the reverse. It keeps what the plan drops (listening comprehension, 100-letter grids, 50-word list, patterns) and leaves out what the plan keeps. Only the two stories and their questions overlap.
The child reads from a printed card; the phone only recordsContradictsThe child reads from the coach's phone, and the picture is the enumerator's numbered sheet.
A checking form in WhatsApp, pre-filled, in UrduNot builtNo Flow at all since 28 September: the instruction-only Flow was removed. Marking is still one chat message per item.
An algorithm picks the children; the coach does not choosePartlyThe child is random within a class, but the coach picks the class and the same child can be drawn again in a later session. Backing out now needs a saved reason; cancelling still redraws with no record.
Results linked to the child, school and grade, so repeat tests show growthNot builtThe code knows the child's database ID but never saves it. No school or grade is saved.
Reuse the existing assessment tables (no new tables)ContradictsA new table. The live database already has aser_sessions and reading_assessments with AI and coach columns, audio and word timings.
Islamabad first, off by default until the time target is metBuiltSwitch plus region gate; inert for everyone else, with tests.
Urdu, then English, then maths; based on EGRA, not ASERBuiltUses the real fielded EGRA/EGMA content in that order.
Several of these choices came from product-lead directions quoted in the commit messages: the roster draw ("school, teacher and randomize students"), the listening-comprehension Flow and voice-note pilot, and the passage picture. The gap from the plan is partly a gap in direction, not only in execution. The build was written in the same week the plan was.

What was done well

Safe to run dark

Off by default, limited to Islamabad accounts, inert for everyone else, with tests proving it. TESTING.md lists its own gaps honestly, including the class-choice bias.

Real test content

The fielded EGRA/EGMA item bank was recovered with answer keys, built by script and checked by tests. It is the most reusable piece of the branch.

Pictures drawn by code, not AI

Stimulus images are rendered from the exact item text, in a correctly embedded Nastaliq font, right to left, and checked back against the item bank. Using AI image generation for test items would have been wrong; this was the right call.

A real random draw

The coach no longer types a name. The child is drawn on the server, uniformly, from the real roster. That is a genuine step toward "the coach does not choose".

Audio handled with discipline

The voice note goes to our own private storage straight away, not left as a WhatsApp media ID that expires. Correctly, no audio is assumed inside the Flow.

Quick, well-aimed fixes

Within a day of the review, the three worst live problems were fixed, each traced in the logs first. Rethinking listening comprehension was right: there the coach reads the passage, so the recording belongs on the stories, where the child reads.

Careful routing, good debugging

Flow completions are matched to the right session; the voice claim only fires in the right step. The chat-loop fix was traced in the logs to its real cause, with a failing test written first.

Done and not done

Done

  • The /egra command and the switch-and-region gate
  • 14 subtests from the May instrument, in the real order
  • Class list from the real roster, random child per class
  • Coach marking: timed grids by Start, Stop and typed wrong items; questions by Correct/Incorrect buttons
  • Scoring code for rates per minute
  • Seven stimulus pictures rendered and uploaded
  • Optional voice notes for listening comprehension and both stories, saved to private storage
  • A confirm step for the drawn child, with saved skip reasons
  • Signed picture links with a text fallback
  • Results and skips tables, applied live, with public access revoked
  • A fix to stop general chat inventing a reading test
  • 132 tests, all passing

Not done

  • Any AI marking, pre-fill, transcript or confidence
  • The per-child checking form
  • The plan's five-minute battery, and its stop and fall-back rules
  • A 60-second countdown or stop alert
  • An answer key on the coach's screen
  • Urdu coach text, or use of the string catalog
  • Pictures for three maths items that refer to pictures
  • Named alternates, and a record of every draw (skips are recorded; tested children are not linked)
  • Saving the child's ID, school and grade
  • Any result for the coach beyond a text list, or for the teacher
  • A test with a real child

Time per child

The estimate walks the code message by message for one child: 113 bot messages and 96 coach actions (83 taps, 11 typed answers, 2 voice notes). The time with the child comes from the May enumerator medians for the same 14 subtests (13.8 minutes). WhatsApp round trips, typing wrong-item lists and re-asking questions for the voice note add the rest.

Target Today's enumerator This build (estimate) 5 min 21.1 min ~22 min Scale: 0 to about 28 minutes. Whisker shows the 18 to 25 minute range.
Five children would take about 110 minutes against a 30-minute target. Trimming subtests alone will not close the gap, because the one-message-per-item design adds minutes of its own.

Architecture and code

The code is organised sensibly: small single-purpose services, a gate, a state machine in one handler, and switches that keep it inert. The problems are in the edges: what happens when a send or save fails, what the tests fake, and a few formulas.

  • Failed saves are silent. A failed picture send now falls back to text. A failed save is still logged and ignored: the coach is told everything worked, and the marks cannot be recovered. This is how the 28 September listening marks were lost.
  • The tests cannot see the real failures. WhatsApp sends and database saves are faked to always succeed, and picture links are only checked for their shape. The voice-routing test searches the source text, and the Flow-response test covers a function nothing in production calls. All 132 tests pass while the build cannot complete a child.
  • Scoring does not follow the instrument. Beyond the letter formula: the English story counts full stops and commas as words (+13% for a child who finishes); there is no 60-second cap; the Discontinue button marks the first ten items wrong on every grid, including the stories, which have no such rule; comprehension has no "not asked" option and is asked even when the child read nothing.
  • Answers can land on the wrong question. Correct/Incorrect buttons do not say which item they belong to, so a double tap or a tap on an older message is recorded against the current item.
  • Coaches can get stuck. /cancel becomes the child's name at the name prompt, "not a number" at the typed prompts, and the skip reason at the reason prompt. Other commands are swallowed for up to an hour. (The voice note is now optional, which removed the worst trap.)
  • Children's audio and names can leak into general chat. A voice note sent outside the exact voice-note step goes through the normal pipeline: transcription, the conversations table, and the chat model.
  • A shared prompt was changed for every region. The chat-loop fix edits the general-chat prompt all regions use. It is a good fix for Pakistan, but it points Palestine teachers to a /reading test that is disabled there, and Tanzania teachers to the English test.
  • Data hygiene. One real tester phone number is committed in the test file. The roster query pulls every Islamabad child's name just to count them.

Coach experience

  • Everything the coach reads is in English, hard-coded, including the Flow. Islamabad coaches work in Urdu, and our checking-form draft and the observation form are Urdu.
  • Two instructions would invalidate the reading subtests. "You will read out each item to the child" and "or read it out per the item type" would have the coach read letters and words aloud for the child.
  • No answer key is shown. Every item has one in the item bank, but no code sends it, so each coach decides what counts as right.
  • Fixed 28 Sep The listening steps were in the wrong order: the child answered inside the Flow before any recording. The Flow is gone; the coach reads the passage from a picture and the voice note is optional.
  • No sense of progress. The coach sees "Item 3/8" but never "subtest 6 of 14" or how much of the session is left, and gets no "child 2 of 5".
  • The end result is a text list of raw per-subtest scores, only if all 14 subtests are finished. There is no level, no benchmark, nothing for the teacher and no class view.
Coach chat at the start of a session
The start: the coach picks a class and a child is drawn. All text in English.
The listening comprehension Flow's question screen, since removed
The Flow's second screen as it was until 28 September (now removed). The item-bank typos it showed are still in the chat questions.
What the coach received when the grid picture failed
What arrived until 28 September when a grid started: no picture, no rules, just "Ready? Start". Fixed by signed links and a text fallback.

Design of the child's card

The pictures are well made as images: clean, correct Unicode, correct Nastaliq joining and right-to-left order. The problem is what they are designed as. They are the enumerator's score sheet, shown to the child on the coach's phone.

  • Every item carries a printed number. A real EGRA child card has none; numbers belong on the assessor's sheet.
  • The stories are cut into numbered word boxes, so the child is not reading connected text, and the result is not an oral reading fluency measure. Punctuation gets its own box.
  • The print is small on a phone. English story words are about 2 mm tall with the phone turned sideways, and 1.1 mm upright. On a printed A4 card each of the ten columns gets about 27 mm; on the phone it gets about 6 mm.
  • One phone cannot be the child's card and the coach's score sheet. The coach has to hold the phone up, keep time with no clock, remember which of up to 100 items were wrong, then take the phone back to tap Stop and type the numbers. The plan's printed card avoids all of this.
First two rows of the English story grid, showing numbered boxes and full stops in their own boxes
The first two rows of the English story as the child sees it. Items 7 and 14 are full stops, and each counts as a correctly read word.
The English story grid at true size on a phone held upright
The same story at true size on a 6.1-inch phone held upright.
One row of the Urdu letter grid
A row of the Urdu letter grid: correct Nastaliq and order, but with item numbers the child should not see.
The listening comprehension Flow's passage screen in Urdu, since removed
The Flow's passage screen, removed on 28 September. The passage now arrives as a picture for the coach to read aloud.

Randomization

What the call decided. The impact team's algorithm names the children from the class register; the coach never chooses. Each pick comes with named alternates for absent children (the Usser study took odd roll numbers and replaced an absent child with the next even number). Five children per visit.

What was built.

QuestionAs builtWhy it matters
Who chooses the class?The coach, from a list of every Islamabad class, capped at 9 rows in no set orderA coach can always pick the strongest class. The author flagged this herself.
How is the child chosen?One child per tap, uniformly at random, on the serverGood: the coach cannot type a name in the normal path.
Can a child be drawn twice?Yes. Each child is a separate drawFive draws from a class of 9 repeat a child 74% of the time; 62% for a class of 12.
Can the coach redraw?Since 28 Sep the drawn child must be confirmed; "Different student" asks for a reason (Absent or Other, any text) and saves it. /cancel then /egra still redraws with no recordRedraws through the button are now visible to a supervisor. The /cancel route and free-text "Other" still let a coach choose.
Absent children?Two reason codes (Absent, Other) and a re-pick that skips them within the session; no named alternates; "Type a name instead" still skips the draw, and an exhausted class falls back to typing a nameAbsent children are usually weaker. Quiet replacement pushes results up by an unknown amount.
Is the draw recorded?Skipped children are saved with their ID. Tested children are not: 0 of 6 results carry the child's IDNobody can check afterwards who was drawn against who was tested.
Repeat testing across visits?No memory of who was tested beforeGrowth over time needs the same child linked across tests.
How big is the pool?7 classes, 41 children (class sizes 1 to 12), no grade 3 classNo class can reach the call's 15-per-class benchmark. The Islamabad sections named on the call are not in this database yet.

The right way to do it

  1. Draw once per measurement cycle, on the server, before anyone sees a name. A cycle is one quarter, since the call recommends EGRA every three months. Snapshot every active child in the target grades at each school.
  2. Shuffle each class once, with a secret seed made from a key, the class and the cycle. Save every child's rank. The secret stops anyone predicting the order; saving it lets us re-run the shuffle for an audit.
  3. At each visit, send the next five in rank order, with roll numbers, plus the next two or three as named alternates, in one WhatsApp list.
  4. Absence is a reason, not a redraw. The coach taps absent, refused, left school or similar; the next alternate moves up. An absent child goes back in the queue and is tried again at the next visit before being marked absent for good.
  5. No redraws and no typed names in the coach's path. Reopening shows the same list. A manual entry, if kept at all, is for supervisors, needs a reason, and is left out of outcome figures.
  6. Record each child's chance of selection (class size at the draw), so regional figures can be weighted. Small classes are tested in full.
  7. Make "5 new" versus "3 new + 2 repeat" a setting, so the draw does not change when that decision is made. Repeat children come from the top of the previous cycle's random order, so they are random too.
  8. Reuse what exists. student-selection.service.js, already on staging, orders never-tested children first, picks several at once, checks attendance and labels roll numbers. It needs a seeded shuffle instead of Math.random, a saved order, and alternates with reason codes.
What the numbers can detect. About six visits per school per quarter at five children each gives 30 tests, or 15 in each of two classes. That is the right amount per class, but a 0.2 standard-deviation change becomes visible only across about 100 classes with new children, or about 20 classes if the same children are retested. A month-to-month change needs whole-class testing. These figures rest on assumed clustering and retest correlation; treat them as orders of magnitude.

What to do next, in order

  1. Finish the small fixes. Done on 28 September: public access revoked, signed pictures with a fallback, audio columns, the Flow removed, TESTING.md updated. Still to do: save student_id on every result, fix the letter formula and the punctuation count, limit the class list to the coach's own schools, make /cancel work at every prompt and record it as a skip, tell the coach when a save fails, and switch on row-level security as a second lock.
  2. Settle the five-minute test and the Urdu instrument choices before building more (plan decisions D1 and D3). The plan leaves D3, the Urdu choices, to Sabeena and Sameer, and says the marker cannot be built until they are fixed.
  3. Cut the battery to the plan's blocks, reusing her item bank and answer keys. Print child cards without numbers, with stories as connected text.
  4. Swap item-by-item marking for record-then-check. One voice note per block, then one checking Flow per child with the answer keys shown, reusing the plan's draft Flow and the NIETE observation pattern. This helps even before any AI: 64 messages and 11 typed answers become three form screens. Then add AI pre-fill as the evaluation recommends.
  5. Store results where the plan says, in the existing assessment tables with the child, school and grade, and the AI's and coach's answers as separate fields.
  6. Replace the draw with the seeded, recorded, per-cycle list above.
  7. Move coach text to Urdu through the string catalog.
  8. Run the stopwatch pilot with real children: two or three coaches, 25 children, before any wider release.

Evidence

Four reviewers worked in parallel on a frozen copy of the 42 changed files at staging head 554c6a8d on 27 September; the two later commits were rechecked directly on 30 September. Every headline claim on this page was then checked again against the code, the live database (read-only, counts only), the staging logs or Meta.

Recheck, 30 September

The two new commits, the status of every finding, usage since the review, and the test run.

Live state on staging (27 Sep)

Deploy, switches, Flow status, table, rows and usage, with how each was checked.

Architecture and correctness

Blockers, serious and minor findings with file and line, test run, what is unverified.

Plan conformance and coach experience

33-row conformance table, the message-by-message time estimate, the battery, and the strategic read.

Randomization and data

The draw against the call, the full proposed design with fields, the power arithmetic, and the data model.

Design

Stimulus images measured at phone size, the Flow rendered, and every coach-facing string checked.

Commits reviewed

783116cc · d31fd846 · 6757dcf6 · b1a2513e · 5fd2da1e · 3c024544 · 1901c5a3 · e81dbc03 (16–18 Sep), then 8050efa6 and 6f59e3f6 (28 Sep). All staging only; staging head e98a45cc.

Children's names, phone numbers and database IDs are left out of this page. Full-size stimulus images and the code snapshot are kept in the team's project folder.