Skip to content

Companion — MATx (Tom Kabel)

matx-hack teaches modeling — how to derive an equation from a word problem. MATx (github.com/tomkabel/MATx) teaches the technical solving of that equation. Two adjacent links in one chain that fit together naturally.

text ──▶ model (matx-hack) ──▶ equation ──▶ solution (MATx)

The full microskill-transition map is on the page Bridge to MATx.

Tom Kabel — author and developer. Estonian project for lower-secondary school (grades 7–9). Stack: TypeScript + React (frontend) + Express (API) + Postgres + Drizzle ORM. Self-hosted at matxdev.duckdns.org.

Collaboration status. At the time of writing the repository is public but without a LICENSE file. Use of screenshots and descriptions in this guide is agreed with the author over Slack. Any technical integration (see Phase 3 below) requires a separate PR with explicit consent.

The actual slugs in the database come from GET /api/topicsmatxdev.duckdns.org/api/topics (public endpoint, no auth required).

Learn and practise three formulas: square of sum, square of difference, difference of squares.

Competency (id, name)Formula
1 — Summa ruut(a+b)2=a2+2ab+b2(a + b)^2 = a^2 + 2ab + b^2
2 — Vahe ruut(ab)2=a22ab+b2(a - b)^2 = a^2 - 2ab + b^2
3 — Ruutude vahea2b2=(a+b)(ab)a^2 - b^2 = (a + b)(a - b)

Open topic →

Solve percentage problems via the cross-multiplication method.

Competency (id, name)What it does
4 — Osa leidmineFind the part given whole and percentage
5 — Terviku leidmineFind the whole given part and percentage
6 — Protsendi leidmineFind the percentage given whole and part

Open topic →

3. vorrandid — Linear equations with one unknown

Section titled “3. vorrandid — Linear equations with one unknown”

Solve equations step by step, writing out each transformation.

Competency (id, name)Form / description
7 — Lihtsad võrrandidax+b=cax + b = c
8 — Sulgudega võrrandidEquations where you must first expand parentheses
9 — Murdudega võrrandidEquations where you must first eliminate fractions

Open topic →

Additionally: docs/06-math-topics.md in Tom’s repo (theoretical description); the formal table schema — shared/schema.ts.

matx-hack (us)MATx (Tom)
DomainModelingComputation
Microskills9 (defining T1–T3 × three relation types)9 (3 topics × 3 competencies)
What the student answersQuantity expressions through xxNumeric / symbolic answer
What’s trackedBKT over 9 defining skillsPer-question results + error categories
StackAstro + React + Next.js (web/)React + Express + Postgres
AudienceEstonian school, grade 7Estonian school, grades 7–9
Content languageET / RU / ENET

Cognitively — our student finishes the phase “understand the problem and write down the equation” and naturally moves on to “solve the equation to a number”. Without MATx that second phase stays verbal in our book; without us, MATx assumes the equation has somehow already been derived.

Live dev instance: matxdev.duckdns.org. There are no public pages — sign-in is required (teacher/student role). Tom can spin up a demo account on request.

  • /dashboard — Your learning path Student home. Three topic cards (Multiplication formulas / Percentages / Linear equations), each with its own mastery% and progress bar. The Recommended-tag marks the current ZPD target. After PR-1, that tag will be driven by the student_skill_state posterior — today it’s a plain % correct.
  • /teacher/analytics — Competency heatmap Teacher’s overview: each row a student, each column a microskill, colors <50% / 50-74% / 75-89% / 90%+. Today the % is computed directly from results. After PR-1, every cell is a Bayesian posterior pKnownsmoother (doesn’t bounce on individual attempts) and auditable (anyone can replay (prior, isCorrect, params) from the log). Tabs Heatmap / Students / Alerts cover different analytics angles.
  • /topic/3/practice-flow — practice tutorial Step-by-step walkthrough of clearing fractions out of an equation. Gives a feel for how a problem looks on screen before the student enters an answer. PR-1’s createResult hooks into exactly that endpoint, where the answer is persisted and the BKT update runs.

Phase 1 — book pages (done in this release)

Section titled “Phase 1 — book pages (done in this release)”
Section titled “Phase 2 — cross-links and deep-links (done)”
  • Inline “Companion: MATx” cards on key pages: home, Multi-skill, Simulator, Q&A
  • Direct deep-links to the three topics by slug + the public API
  • iframe embed decided not to be done: connect.sid ships with SameSite=Lax, so the logged-in flow doesn’t survive in an iframe — without it the embed is useless. Better to open in a new tab.

Phase 3 — BKT model in MATx submit-handler (PR plan)

Section titled “Phase 3 — BKT model in MATx submit-handler (PR plan)”

Idea. Lift our BKT math into a pure-TypeScript package bkt-core (zero runtime deps), Tom adds it as a single dependency and a single call inside storage.createResult. No network hops, no ML infrastructure. Mergeable as two independent PRs.

Source in matx-hackWhatSize
web/lib/microskills.tsTypes BktParams, MicroSkill, Task, StudentState + the DEFAULT_BKT constant~25 lines, no deps
web/lib/bkt.tsbktUpdate, pSolve, ensureMastery, applyAttempt, scoreTaskForStudent, recommend~150 lines, imports types only

The package’s package.json carries only name, main, types, tsc. Zero production dependencies — pure math.

PR 1 — bookkeeping (minimum, ~70 lines, UI unchanged)

Section titled “PR 1 — bookkeeping (minimum, ~70 lines, UI unchanged)”

Each attempt updates the closed-form BKT for the corresponding competency. MATx’s user-visible behavior does not change — only a new field appears in the database.

1. Drizzle migration — new table:

CREATE TABLE student_skill_state (
user_id text NOT NULL REFERENCES users(id) ON DELETE CASCADE,
competency_id integer NOT NULL REFERENCES competencies(id) ON DELETE CASCADE,
p_known double precision NOT NULL DEFAULT 0.2,
attempts integer NOT NULL DEFAULT 0,
last_updated timestamp DEFAULT now(),
PRIMARY KEY (user_id, competency_id)
);

Zero-downtime (new table, nothing locks). Applied via the existing npm run db:push.

2. shared/schema.ts — add the table definition (~15 lines):

export const studentSkillState = pgTable("student_skill_state", {
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
competencyId: integer("competency_id").notNull().references(() => competencies.id, { onDelete: "cascade" }),
pKnown: doublePrecision("p_known").notNull().default(0.2),
attempts: integer("attempts").notNull().default(0),
lastUpdated: timestamp("last_updated").defaultNow(),
}, (t) => ({ pk: primaryKey({ columns: [t.userId, t.competencyId] }) }));

3. server/storage.ts.createResult (current, lines 150–152) — add an upsert after insert (~20 lines):

import { bktUpdate, DEFAULT_BKT } from "@matx-hack/bkt-core";
import { studentSkillState } from "@shared/schema";
async createResult(result: CreateResultRequest) {
const [savedResult] = await db.insert(results).values(result).returning();
// — BKT update —
const q = await this.getQuestion(result.questionId);
if (q?.competencyId) {
const [cur] = await db.select().from(studentSkillState).where(and(
eq(studentSkillState.userId, result.userId),
eq(studentSkillState.competencyId, q.competencyId),
)).limit(1);
const prior = cur?.pKnown ?? DEFAULT_BKT.pInit;
const posterior = bktUpdate(prior, result.isCorrect, DEFAULT_BKT);
await db.insert(studentSkillState).values({
userId: result.userId, competencyId: q.competencyId,
pKnown: posterior, attempts: 1, lastUpdated: new Date(),
}).onConflictDoUpdate({
target: [studentSkillState.userId, studentSkillState.competencyId],
set: { pKnown: posterior, attempts: sql`${studentSkillState.attempts} + 1`, lastUpdated: new Date() },
});
}
return savedResult;
}

server/routes.ts (submit-handler at lines 292–345) is not touched — the update is encapsulated in the storage layer.

PR 2 — smarter recommendation (~80 lines, optional)

Section titled “PR 2 — smarter recommendation (~80 lines, optional)”

Replace the heuristic in getNextRecommendedQuestion (current, lines 878–940) with BKT-driven selection via the ZPD target P(solve)0.7P(solve) \approx 0.7.

The current logic — three buckets notStarted / needsWork (mastery < 60%) / progressing (60–90%) with selection by mastery asc — survives as a mapping layer back to Tom’s Estonian explanations (Harjuta..., Kinnista..., Korda...) so the UI/UX stays put. Under the hood — our recommend(state, tasks):

import { recommend } from "@matx-hack/bkt-core";
async getNextRecommendedQuestion(userId, topicId) {
const skillStates = await db.select().from(studentSkillState)
.innerJoin(competencies, eq(studentSkillState.competencyId, competencies.id))
.where(eq(competencies.topicId, topicId));
const state = {
student_id: userId,
mastery: Object.fromEntries(skillStates.map(s => [String(s.competency_id), s.p_known])),
history: [],
};
const qs = await db.select().from(questions).where(eq(questions.topicId, topicId));
const tasks = qs.map(q => ({
id: String(q.id),
topic: String(topicId),
microskills: q.competencyId ? [String(q.competencyId)] : [],
difficulty: 0.5, prompt_et: q.text, answer: "",
}));
const top = recommend(state, tasks, 1)[0];
// … map score+pSolve back to existing reason buckets for UX continuity
}
PieceStatus
Extract bkt-core as a package✅ Done
Add submodule into tomkabel/MATx✅ Tom did this himself
PR-1 BKT bookkeeping✅ Merged + deployed
PR-2 ZPD-targeted recommendation✅ Merged + deployed
PR-3 Defineerimine + 28 tasks✅ Merged + deployed
PR-4 UI: heatmap pKnown, “Why recommended?”, 10 mock students✅ Merged + deployed
PR-5 audit_trace + sparkline + column sort✅ Merged + deployed
PR-6 student detail page + BKT alerts + microskill bookkeeping✅ Merged + deployed
PR-7 microskill UI (detail-page section + new heatmap tab + mock seed)✅ Merged + deployed
Teacher feedback iteration⏳ In progress

bktUpdate is a closed analytical formula. At any point an auditor can take (prior, observation, params) from the log and independently replay the posterior. No “black box” justification needed.

Optionally: add an audit_trace JSONB column to student_skill_state — it accumulates a history of (timestamp, prior, isCorrect, posterior) tuples. This closes Annex III traceability + explainability requirements without separate compliance infrastructure.

Education / vocational training is a high-risk Annex III category under Regulation (EU) 2024/1689. Requirements on traceability, documentation, human oversight and explainability come into force in stages from August 2026 to August 2027.

Our part of the pipeline is already fully deterministic — no LLM call exists in the production path:

  • BKT update — closed analytical Bayes expression.
  • Explanations — a template engine (web/lib/explain.ts) with a fixed set of phrases.
  • Selector — argmax over model confidence + topology rules (skill graph).

After Phase 3 MATx inherits the same profile automatically: a single line bktUpdate(...) in the submit-handler — and the audit trail for the next-question decision is built without separate compliance infrastructure.

On top of that, eatf.eu — model attestation, response signing, audit in one API call — can be layered for centralised artefact storage.

  • Progression matrix — our 9-microskill defining-skills model.
  • Bridge to MATx — the microskill ↔ MATx-competency table and the Mermaid transition graph.
  • Selector simulator — how BKT picks the next defining microskill; after Phase 3 it will also reference MATx competencies.
  • Glossary — terms (microskill, competency, BKT etc).