04

The Learner Frontier

A build retrospective on the adaptive knowledge graph behind Amal, a kids' Arabic-learning app used by 95,000+ families. • Day 4 is the heart of the engine: a single pure function that takes the shared graph plus one child's overlay and returns exactly the concepts that child is ready to learn next.

The question the whole engine answers

Days 2 and 3 built two things: a shared curriculum graph (concepts joined by prerequisite edges) and a per-child overlay (what each child has mastered, tracked by strength). The entire point of putting those together is to answer one deceptively small question: for this specific child, right now, what should we teach next?

The answer has a name — the frontier. In Amal it is produced by a component called LearnerFrontier, and the frontier is the set of concepts a child is ready for: everything whose prerequisites they have already mastered but which they have not yet mastered themselves. Not the stuff they already know (too easy), not the stuff whose foundations are missing (too hard) — the thin band right at the edge of what they can do.

The diagram shows the three zones every concept falls into for a given child: mastered below, locked above, and the frontier in between. The frontier is the "just-right" zone from Day 1, made computable — and computing it is exactly what LearnerFrontier does.

   THE FRONTIER = the band right at the edge of ability

   [ mastered ]          the child already knows these  (too easy)
        │
        ▼
   [ FRONTIER ]  ◄──────  prereqs met, not yet mastered  (JUST RIGHT)
        │
        ▼
   [ locked ]            prereqs NOT met yet             (too hard)

A pure function of two inputs

The most important design decision about LearnerFrontier is that it is a pure function — a function whose output depends only on its inputs, with no hidden state and no side effects (it reads; it never writes). Its two inputs are the graph and the child's state; its output is the frontier set. Same graph plus same state always yields the same frontier.

Why insist on purity? Because it makes the hardest part of an adaptive system — "what's next" — the easy part to trust. A pure function can be unit-tested exhaustively (feed it a graph and a state, assert the frontier), reasoned about without worrying what else it might touch, and run anywhere without a database write. Everything risky (generating a lesson, saving progress) is kept out of this function and handled elsewhere. The brain of the system is deliberately the boring, testable part.

   frontier = LearnerFrontier( graph , child_state )

   graph        : concepts + prerequisite edges   (shared, Day 2)
   child_state  : this child's mastery strengths   (overlay, Day 3)
   ─────────────────────────────────────────────────────────────
   returns      : the set of concepts this child is READY for

The rule, in plain steps

The frontier rule is short enough to state as a checklist a human could run by hand. For each concept in the graph, decide which zone it is in for this child:

  • Is it already mastered? If the child's strength for this concept is high enough (past a mastery threshold), it is in the mastered zone — skip it, it would be too easy.
  • Are all its prerequisites mastered? Look up every incoming prerequisite edge. If even one prerequisite is not yet mastered, this concept is locked — skip it, it would be too hard.
  • Otherwise it is on the frontier. Not yet mastered, but every prerequisite is — this is a just-right next step. Keep it.

Written as pseudocode, the whole engine brain is a dozen lines:

def learner_frontier(graph, state, mastery=0.8):
    frontier = []
    for concept in graph.concepts:
        strength = state.strength_of(concept)
        if strength >= mastery:
            continue                      # already mastered: too easy
        prereqs = graph.prerequisites_of(concept)
        if all(state.strength_of(p) >= mastery for p in prereqs):
            frontier.append(concept)      # ready: JUST right
        # else: a prereq is missing -> locked -> skip (too hard)
    return frontier

Notice what the function does not do: it does not pick one concept, generate a lesson, or save anything. It returns the whole set of ready concepts and stops. Choosing among them and building a lesson are separate jobs (Days 5–6), kept out on purpose so this core stays pure and testable.

Worked example: the same graph, two children

The frontier only becomes vivid when you run it for two different children over the identical graph and watch it return different answers. Take the small Arabic slice from Day 2 — letter ب → sound /b/ → word baab, with baab also needing "connected forms" — and a mastery threshold of 0.8.

Child A has mastered the letter and the sound (strength 0.9 each) but not connected forms (0.3). Child B is newer: the letter is solid (0.85) but the sound is still weak (0.5).

The diagram is the whole promise of the system in one picture: one graph, two overlays, two different frontiers. Child A is ready for connected forms; child B is ready for the sound /b/. Neither is served a lesson that is too easy or one whose foundation is missing — and nobody hand-authored two separate paths. The paths fell out of the graph plus each child's state.

   SAME GRAPH, TWO OVERLAYS -> TWO FRONTIERS

                 baa(ب) ──► /b/ ──► "baab" ◄── connected forms

   child A  :   [0.9]      [0.9]    [0.0]       [0.3]
     frontier -> { connected forms }      (its prereqs — none — are met;
                  "baab" LOCKED: needs connected forms + /b/, forms<0.8)

   child B  :   [0.85]     [0.5]    [0.0]       [0.0]
     frontier -> { /b/ }                  (letter done; sound is next;
                  "baab" and forms both LOCKED behind /b/ )

Why "a set", not "the next lesson"

A subtle but important choice: LearnerFrontier returns a set of ready concepts, not a single "next lesson." That looks like extra work — surely the child can only do one thing next? — but returning the set is what keeps the design flexible and honest.

  • Choice belongs to a later stage. Which frontier concept to serve first can depend on things the frontier itself should not care about: variety, a domain the child is focusing on, spaced-repetition items that are due (Day 3's HLR). Handing back the whole set lets Day 5's tree builder make that call.
  • The set is the child's readiness. A child usually has several concepts ready at once (different branches of the graph open up in parallel). Collapsing that to one number would throw away exactly the information the next stages need.
  • It stays pure. Ranking or picking often wants extra signals and sometimes writes state; keeping LearnerFrontier to "here is everything you're ready for" preserves the pure-function property that makes it trustworthy.

Day 5 takes this frontier set and turns it into something a child actually sees: a per-child, realtime subject tree — and, because two children have two different frontiers, two different trees from the one graph.

Key takeaways

  • The engine's core question is "what should this child learn next?", and its answer is the frontier: concepts whose prerequisites are all mastered but which the child has not mastered yet.
  • LearnerFrontier computes it as a pure function of two inputs — the shared graph and the child's overlay state — with no writes and no hidden state, which makes the hardest part of the system the most testable.
  • The rule per concept is a three-way sort: mastered (too easy, skip), a prerequisite missing (locked, too hard, skip), else on the frontier (just right, keep).
  • Run over the same graph with two children's overlays, the function returns different frontiers — the per-child path falls out of graph + state, with nothing hand-authored.
  • It returns a set, not a single lesson, so that later stages can choose among ready concepts (variety, focus, spaced-repetition due-ness) while the core stays pure.

Checklist

  • [ ] I can define the frontier and place it between the "mastered" and "locked" zones.
  • [ ] I can explain what a pure function is and why LearnerFrontier being pure makes it trustworthy and testable.
  • [ ] I can run the three-step frontier rule by hand for a concept given its strength and its prerequisites' strengths.
  • [ ] I can work the two-children example and explain why the same graph yields two different frontiers.
  • [ ] I can give a reason the frontier is returned as a set rather than a single next lesson.