Docs
Documentation

Mathua Architecture

The five-layer stack — UI, API, core engine, grading and storage — and the computerised adaptive diagnostic.

Mathua is a single Go binary with a web delivery mode. The engine core serves a single presentation layer.

Five-layer architecture

The five layers are drawn above and described below. The diagram source is web/next-app/diagrams/architecture.d2. The website renders it as an interactive graph.

UI Layer — Web frontend, one engine

Mathua ships with a web frontend using React with Next.js, connecting to a shared Postgres database.

API Layer — Four services, one router

The web server exposes a REST API through Go's standard net/http package. No external HTTP framework. Four services: Session (practice management), Graph (concept DAG visualisation), Leaderboard (weekly scoring), Diagnostic (adaptive testing).

Core Engine — Five modules

  • DAG Loader — Loads data/concepts/*.json plus enrichment.json, then validates no cycles and no orphans.
  • Scheduler (SM-2) — Selects the next concept using priority scoring: recency, mastery, decay bonuses. Enforces prerequisite gating and 70/30 new/review balance.
  • Generators — Every problem is generated on demand by a parameterised Go function. No static question bank.
  • Mastery & Spaced Repetition — Tracks mastery via streak and response time. Simplified SM-2 algorithm schedules reviews.
  • Scoring — Two scores: lifetime topic score (permanent) and weekly score (resets every Monday).

Grading — nine types, six Go graders + SymPy

The grading_type on each concept selects the grader. One Router dispatches nine enum values. Six are pure Go. Three route to SymPy:

  • Numeric — integer, float, fraction, mixed number, scientific notation (pure Go).
  • Choice — case-insensitive multiple-choice matching.
  • Comparison — comparison operators (>, <, =, >=, <=).
  • Ordering — ordered sequences separated by delimiters.
  • Tuple — coordinate/n-tuple parsing.
  • Complex — a + bi forms.

Symbolic, Polynomial, Expression — routed through a long-lived Python subprocess running grading/sympy_service.py. The Go client sends a JSON expression pair over stdin. SymPy parses both into expression trees. SymPy then tests equivalence (simplify(expected - answer) == 0, with expand/factor/trigsimp/radsimp/powsimp and numerical verification). Python and SymPy are installed in the production image and pinned in grading/requirements.txt.

The image build verifies the installation, so grading is available in production. There is no pure-Go equivalence fallback for these types: the router sends all three straight to the subprocess.

Storage — SQLite and Postgres

SQLite for local development (mathua.db, WAL internal/platform/sqldb/sqlite.go:19). PostgreSQL for production web access via DATABASE_URL=postgres:// (internal/bootstrap/stores/postgres.go:17 PostgresStore via pgx/v5/stdlib, pgSchema internal/platform/sqldb/schema_postgres.go:3, GREATEST for XP floor, STRING_AGG for activity). cmd/sensim checks DATABASE_URL and opens pgx when postgres:// is set, otherwise SQLite. The data schema is identical across both databases, abstracted behind a Repository interface (internal/bootstrap/stores/store.go:14).

Code layout: ports and adapters

The Go code is split into five bounded contexts. Each context has three layers. ADR-060 records the decision. AGENTS.md §9 states the rule.

ContextWhat it owns
learningThe concept graph, mastery, spaced repetition, the scheduler, the diagnostic, planning and XP.
competitionThe weekly board, the leagues and the level titles.
identityAccounts, sign-in, the OAuth providers, guests, settings and avatars.
administrationStaff roles, question reports, invitations and the audit log.
assistantThe AI policy registry, the quota and the provider chain.
  • domain/ holds rules and records. It imports only the standard library.
  • app/ holds the use cases. It declares each port that it needs as a Go interface.
  • adapters/ holds the HTTP handlers (httpapi), the SQL stores (sqlstore) and the clients for outside services.

Math is a subject. The packages in internal/subjects/math implement the Subject port in internal/learning/domain/subject. A second subject, such as a language course, is a second implementation of that port.

internal/platform holds the code that every context uses: HTTP helpers, the database schema and mail. It imports no context. internal/bootstrap is the composition root. It builds every store, use case and handler and mounts the handlers on one mux. cmd/sensim reads the flags and the environment and calls it.

Two tests check this layout. internal/archtest/arch_test.go reads the imports of every package. internal/archtest/tables_test.go reads the SQL in every store and checks which context writes each table.

The frontend uses the same split. web/next-app/features/<context> holds the code of one context. web/next-app/subjects/math holds the math input and rendering. components/, lib/ and hooks/ are the shared layer. app/ holds the routes. test/featureBoundaries.test.ts checks the imports between these areas.

Administration

Administration has its own document: administration.md covers the four roles, the permission model, invitations, the last-owner invariant and the audit log.

Diagnostic — Computerised Adaptive Testing

Locates a student's knowledge frontier using a compressed covering set + info-gain CAT (internal/learning/domain/diagnostic/cat.go, internal/learning/domain/diagnostic/report.go:11). The engine builds a minimal covering set of the DAG. It repeatedly picks the concept that gives the maximal entropy reduction. It propagates +0.3 evidence to prerequisites on correct and -0.3 to dependents on incorrect and tracks per-concept KnowledgeConfidence 0–1. The frontier is the highest belief drop. A supplemental diagnostic runs when confidence <0.7.

minTotalQuestions=25 and maxTotalQuestions=45 bound the assessment (internal/learning/domain/diagnostic/cat.go:33-35). The engine does not test all 1250 concepts.

The engine asks two probes per concept in the cover. It stops at 25 questions once the cover is finished. A narrow scope runs out of cover first and stops sooner. A five-concept scope asks ten questions and still reports min_total: 25. The engine does not pad the difference.

The engine targets 80% difficulty via engine.computeDifficulty (internal/learning/app/engine/engine.go:486, weakness→difficulty 0.3–0.8).

Full documentation

For interactive D2 diagrams, internal module detail and formatted code blocks, see the Architecture docs in the web app.

Source: docs/architecture.md