How lemon-model blocks any model promotion without a holdout scored once, a statistical test, a forgetting guard and a cheating detector
Published: November 13, 2026
Reading time: 9 min
Series: lemon-code in 30 days, part 12 of 13
This function passes every Fibonacci test that asks for the twentieth number:
def fib(n):
if n == 20:
return 6765
...
In lemon-model it scores zero. Even with every test green.
Last post I showed an adapter whose loss dropped on a single example. It would be easy to write "my model got better". This post is about the system built to block that sentence until it's true.
A serious entrance exam keeps the questions in a vault until exam day. Each candidate sits it once, no retakes until they pass. Grading looks for cheating, not just right answers. And the cutoff is set before anyone sits the exam.
# minimal hard-coded value detector (illustrative, no dependencies)
import ast
def hardcodes_test_input(source: str) -> bool:
for node in ast.walk(ast.parse(source)):
if isinstance(node, ast.If) and isinstance(node.test, ast.Compare):
literal_check = any(isinstance(c, ast.Constant) for c in node.test.comparators)
returns_literal = any(isinstance(s, ast.Return) and isinstance(s.value, ast.Constant)
for s in node.body)
if literal_check and returns_literal:
return True
return False
def reward(source: str, tests_passed: int, tests_total: int) -> float:
if hardcodes_test_input(source):
return 0.0
return tests_passed / tests_total
print(reward("def fib(n):\n if n == 20:\n return 6765\n", 5, 5)) # 0.0
lemon-model's real detector also flags lookup tables keyed by test inputs, sys.exit(0), pytest.skip, writing or reading the test file and stack inspection, including aliased variants. Each scores exactly 0.0 even when every test passes, while legitimate small literals and paths that merely contain "test" aren't flagged. The recorded limitation: static detection is heuristic and doesn't replace hidden tests or sandboxing.
Free and paid products to turn ideas into a foundation you can actually ship.
13 products availableContinue exploring similar topics

Fine-tuning learns behavior; RAG provides context. In 2026, the choice isn't binary—it's architectural. Explore deep metrics, PEFT/LoRA code, and hybrid patterns.

Preço por token é o número do fornecedor. Custo por tarefa concluída é o seu. Entre os dois existem retries, tarefas que falham, contexto que cresce e um nível de esforço que ninguém varre.
My first attempts to train the 7B on the Mac hit the memory safety cutoff, even with MLX under 14 GB. The pilot that passed used 8 layers, rank 4 and finished in 200 seconds.
A practical checklist to validate security, cost, evals, and observability before you ship your AI app to production.
Production-tested templates trusted by developers. Save weeks of setup on your next project.
Holdout tasks live in their own directory with restricted permissions (0600 files, 0700 folders). Every read, write or export goes to an append-only log with who accessed it and why. A deny list of task hashes rejects training data that leaks from the holdout. Task identity is a hash of the defining content: relabeling doesn't change it, changing the prompt does.
A candidate (weights + adapter) can be scored on the holdout once. Rerunning with a different identity or task count fails with holdout_budget_exceeded; a changed adapter is a new candidate. That blocks the most common vice in model training: score, tweak, rescore until the number looks good, at which point the holdout has quietly become a dev set. The current suite has two holdout tasks, still short of the 200 required for promotion.
Recorded limitations: the candidate key uses the weights path, not a hash, so the same weights at another path count as new. A crashed run keeps its claim, which must be removed by hand.
| Requirement | Detail |
|---|---|
| Policy declared before training | versioned and hashed |
| Paired holdout comparison | at least 200 identical tasks, p < 0.05, 95% CI lower bound above zero |
| No regression | no drop in quality-gate pass rate |
| Data rights | cleared data manifest |
| Safety | passing report, zero violations |
| Real serving | p95 latency and peak memory within limits on declared hardware |
| Forgetting | per-category drop within tolerance |
Anything missing, malformed, hash-mismatched or from another candidate becomes a blocker. The evaluator does no I/O and has no path that writes a production pointer.
Adapting for code can hurt instruction following or appropriate refusals. The forgetting guard compares base and candidate answers on a fixed suite and blocks drops beyond tolerance: 0.1 for general, instruction and over-refusal, 0.0 for safety refusal. A policy may tighten these, never loosen them.
No real model has been evaluated on the coding suite, and no candidate has passed the gate. The curriculum import has six tasks: four dev and two holdout. Promotion requires at least 200 paired tasks; the gate tests use hand-written fixtures to exercise each branch. The README says coding-quality evaluation and a production-quality model are pending.
The gate is there to keep a pilot result from being treated as a promotion. Its existence doesn't mean a real candidate has been evaluated.
Evaluating a coding model is closer to running an entrance exam than running tests: a locked key, one sitting per candidate, grading that looks for cheating and a cutoff set in advance. Without that, any loss curve becomes an announcement.
Monday wraps the series with 30 days of real numbers: commits, ADRs, tests, evidence and the mistakes on record.
If your team trains or tunes models and needs an evaluation process that can be audited, I help design that work at LEMON.DEV.