For developers and scientists
How Luna reads a belief
Luna keeps a typed graph of correct ideas and wrong ones. A grader reads one essay against a slice of that graph. An estimator pools those readings across essays. This page is that model, and a sketch of an API we have not shipped.
The code is lib/domain for the graph, gradingContext for the filter, lib/notebook/grading.ts for one essay, and estimateWritingToLearn in lib/assessment/psychometrics.ts for the student estimate.
The belief web
The web is the authored catalog, not a student's state. A belief is a reading of one essay that points at a node in it.
Nodes are concepts, writing skills, levels, misconceptions, indicators, interventions, and techniques. Four domains: writing, chemistry, algebra, and LSAT. LSAT is shown only to an enrolled class.
The edges a belief depends on:
- distorts — a misconception to the concept it gets wrong.
- arises_from — a misconception to the more basic misconception it comes from.
- transfers_to — the same idea from one domain into another.
- surfaces_as — a misconception to the sentence pattern that shows it.
- mimicked_by — that sentence pattern to a writing skill that can produce the same words without the belief.
- remediated_by — a misconception to the revision that addresses it.
Writing is eight strands, scored from 1 to 5: Claim, Evidence, Reasoning, Mechanism, Precision, Representation, Qualification, and Organization. Levels run from Describer to Scientist. Chemistry runs from Observable to Symbolic. Algebra runs from Arithmetic to Structure.
The filter
The grader does not see the whole web. gradingContext cuts it to one prompt before the essay is read.
- Start with the prompt's target concepts.
- Take every misconception with a distorts edge into those targets.
- Add every writing misconception. Every essay is also a piece of writing.
- Grow the set until it stops. Follow arises_from from an included misconception to the one it comes from. Follow transfers_to backward: if another domain's idea transfers into one already included, include the source.
- For each misconception in the set, attach the indicators it surfaces as, the intervention that remediates it, and any writing skill that mimics an indicator.
The grader also receives the eight writing strands and the levels of this domain and of writing. A prompted model (Cerebras gpt-oss-120b) then sees the prompt, that slice, and the essay. It does not see a name, a class, or a student id. It may return at most four misconceptions, each with a quote, a reading, and a confidence from 0 to 1.
A second filter runs on the reply.
- A quote that is not in the essay is dropped.
- An id outside the slice is dropped. A proposal for a new idea is stored as a candidate for a teacher and is not shown to the student.
- Content means the reasoning depends on the wrong idea. Expression means the words sound like it and the rest of the essay has the right idea. Ambiguous means the text does not decide. The grader is told to prefer ambiguous over a guess.
- Only a content reading is evidence that a student holds the idea, weighed by its confidence. Expression and ambiguous are kept on the submission and left out of the estimate.
- A concept counts as demonstrated only when it is a target of the prompt.
The estimate
estimateWritingToLearn keeps one number per writing strand, concept, and misconception. Each dimension is estimated on its own. There is no shared ability, no item difficulty, and no discrimination parameter. This is not a text model trained on human ratings and not a Bayesian two-parameter logistic model. Misconceptions use the hidden-state model behind Bayesian knowledge tracing.
Strands
The estimate is the weighted mean of the 1–5 levels, clamped to that range. Only the newest graded revision of each piece counts. Weight is 0.5 raised to the age in days divided by 180.
Concepts
A weighted Beta(1, 1) rate over the newest revision of each piece. One unit of support and one unit of contradiction are the prior, so one essay cannot pin the value at 0 or 1.
(1 + support) / (2 + support + contradiction)
- A demonstrated target concept has weight equal to its recency. The estimator also accepts a non-target demonstration at 0.65 of that weight. The grader does not emit those.
- A content reading of at least 0.5 confidence counts against each concept its misconception distorts, with weight recency times confidence.
Misconceptions
A wrong idea is something a student holds and can let go of, so its number is the probability that the student holds it now, not how often it has appeared. A two-state filter reads every graded revision in time order. Before each essay it allows for change; after it, it weighs what the essay showed.
b⁻ = b(1 − letGo) + (1 − b)·relapse
b = b⁻·L(holds) / (b⁻·L(holds) + (1 − b⁻)·L(free))
- The starting value is 0.30 for a productive misconception, a normal waypoint, and 0.08 for any other.
- Between essays, a student lets go of the idea with probability 0.15, or 0.30 when the margin asked them to revise that idea. A student who was free of it picks it back up at 2% a month.
- A content reading is likely if the student holds the idea and rare if not: 0.60 against 0.03 at confidence 0.8 or more, 0.50 against 0.10 at 0.5 or more, 0.30 against 0.15 below.
- A correct explanation of the distorted concept in another piece is 0.15 if the student holds the idea and 0.60 if not. In the piece that held it, it is 0.40 against 0.60: fixing a draft in place is weak evidence.
- A resubmission that shows the same thing as that piece's last draft is not new evidence.
A teacher sees a wrong idea as held at 0.6 or more. It is resolved at 0.3 or less, and only once a different piece of writing has explained the concept since it last appeared. In between it is watched, not reported. These numbers are set by hand; teacher-confirmed readings will be used to fit them.
A future API
Not in this release. The notebook already computes the web and the filter. A later release would open those two functions to a caller that is not the notebook. The paths below are a sketch, not a contract, and they are not deployed.
The belief web
Read the authored graph for one domain, including transfers_to edges that cross into it.
GET /api/v1/domains/{domain}
The filter
Return the slice gradingContext already builds for a prompt: targets, misconceptions in scope, their indicators, the writing skills that can mimic those indicators, the strands, and the levels.
GET /api/v1/prompts/{id}/slice
Accept an essay and return strand levels, readings, and demonstrated targets. The same second filter applies: quotes must appear in the essay, ids must survive the slice, and expression and ambiguous come back labeled and are not beliefs. The body is the prompt and the essay. No student name, class, or id.
POST /api/v1/readings
The reason to wait is scope, not uncertainty about the model. The filter and the web are what the product runs today. An API is those functions with an authenticated caller outside the notebook.