case study · 01
cpractice
An AI-assisted C practice platform. The model explains; a validated test suite decides.
Python · FastAPI · SQLite · pytest · plain HTML/CSS/JS frontend. Started 10 Oct 2026.
01 — the problem
where feedback goes wrongYou want to practise C, and you want to know whether your program is actually right. The obvious way to get that feedback is to ask a model — and that is exactly where it goes wrong.
A model will tell you the code looks correct. It will also tell you the code that segfaults looks correct, because it is pattern-matching your text rather than running your program. It is a confident reader, not a referee.
So the question is not "can a model grade C". It is where do you put the model — and what is allowed to decide the verdict.
02 — the principle
design decision #3AI generates and explains.
Deterministic software grades.
The plan puts it as a hard rule: the AI never decides pass/fail. The model can write the problem, hint you when you are stuck, and explain a failure — but the verdict comes from compiling your code and comparing its output against a validated test case.
03 — the grading loop
no model in the path- You write the solution In your own editor, in plain C. No in-page IDE, no scaffolding that quietly fixes your braces.
-
It is compiled with a pinned flag set
-std=c11 -O2 -pipe -Wall -Wextra— the same flags every time, so a verdict is reproducible. - It runs in a throwaway workspace A dedicated temp directory, a wall-clock timeout, a memory cap and an output limit. A runaway loop gets killed, not the machine.
- Output is compared, not judged stdout is checked against the expected output of a declarative test case. Same input, same bytes, same verdict.
- You get a result and a diff Pass or fail, plus the failing case and what came out instead. That is the part that teaches you something.
- Only then does the model help Progressive hints and an explanation of the failure. It has no vote on whether the answer was right.
04 — the trust boundary
two different jobsWrites, explains, nudges
Generates candidate exercises behind a quality gate, explains a failing test, hands out progressive hints. Fast, useful, and occasionally wrong — which is fine, because it never signs the verdict.
Compiles, runs, compares
Pinned compiler flags, enforced time and output limits, byte-level comparison against expected output, deterministic pass/fail. Provider-independent — swap the model, the grading does not change.
One decision makes the rest work: tests are declarative data, not arbitrary AI-generated scripts. The model may propose a problem, but it cannot author its own test that trivially passes. The test cases live in versioned problem packages, and they are validated independently of whatever produced the exercise.
05 — the hard parts
found by experimentMost of the work was not the AI part. Building a sandbox that behaves the same on a phone, in a container, and on a laptop meant documenting a list of platform findings instead of guessing. These are the ones that changed the design:
| finding | why it hurts | what I did |
|---|---|---|
shared storage is noexec |
binaries compile but cannot run on Android shared storage | repo and grading workspaces live in an exec-capable filesystem |
RLIMIT_AS aborts the bionic linker |
the program dies at load, intermittently, at every limit | skip RLIMIT_AS on Android |
RLIMIT_NPROC corrupts signal reporting |
crashes report as exit 1, which reads as a wrong answer | skip it on Android; detect crashes via negative exit codes |
| no Docker, no root locally | strong sandboxing is not available on a phone | isolation = dedicated tmpdir + timeouts + output caps; containers when hosted |
Termux gcc is clang 21.1.8 |
some flags behave differently | pinned flag set; no -fsanitize, no -march=native |
no Android wheels for pydantic-core |
pip compiles Rust from source, 20–40 minutes | pay it once; wheels are cached afterwards |
One more trap, this time in the devcontainer: adding any
ghcr.io feature makes Codespaces fail the build and
silently fall back to a minimal recovery container — no Python, no
secrets, no setup script. It looks like a working codespace and is
not. The fix is written down in the plan so nobody "improves" it
back into a broken state.
06 — the stack
and how it is testedThe platform has its own pytest suite, and it runs with mocked AI provider responses — testing CPractice never needs a live API key. The frontend is plain HTML, CSS and JavaScript with no framework and no CDN, which keeps the same constraints I use here: nothing to install, nothing to trust at runtime.
07 — status
milestones 1–5The build order is strict: foundation → grading engine → problem-package contract → AI generation → learning feedback. The deterministic checker is proven before any AI generation is added, so the part that decides correctness never depends on the part that cannot be trusted.
That sequencing is the whole project in one sentence. The rest is plumbing, and plumbing is fixable.
say hello
let’s build something.
For work, collabs, or just to say hi — my inbox is open.